Appending data to JSON column - Rails 4, Postgres - json

Model Listing has a json column named vendors, which defaults to {}:
add_column :listings, :vendors, :json, null: false, default: {
hd: {}
...
}
end
Inside the Listing class, I am trying to merge! in a hash to hd: {..} like so:
def self.append_vendor_attrs(sku)
listing = find_by(sku: sku) || Listing.new(sku: sku)
listing.vendors[:hd].merge!({ foo: 'bar' })
listing.vendors_will_change!
listing.save! # throws no exceptions in the log
end
but vendors[:hd] remains empty. if I try to replicate this manually with:
l = Listing.create...
l.vendors[:hd].merge!({ foo:'bar' })
my hash persists.

Calling listing.vendors_will_change! tells Active Record that the value of the attribute "was" the new value. From AR's perspective, then, the old and new values match, and so the record is not dirty when you call save!.
The fix is to move listing.vendors_will_change! above the line where you modify the attribute. So the new code will look like this:
def self.append_vendor_attrs(sku)
listing = find_by(sku: sku) || Listing.new(sku: sku)
listing.vendors_will_change!
listing.vendors[:hd].merge!({ foo: 'bar' })
listing.save! # throws no exceptions in the log
end

Related

SQLALchemy update ARRAY column [duplicate]

I'm working on a project using Flask and a PostgreSQL database, with SQLAlchemy.
I have Group objects which have a list of User IDs who are members of the group. For some reason, when I try to add an ID to a group, it will not save properly.
If I try members.append(user_id), it doesn't seem to work at all. However, if I try members += [user_id], the id will show up in the view listing all the groups, but if I restart the server, the added value(s) is (are) not there. The initial values, however, are.
Related code:
Adding group to the database initially:
db = SQLAlchemy(app)
# ...
g = Group(request.form['name'], user_id)
db.session.add(g)
db.session.commit()
The Group class:
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.dialects.postgresql import ARRAY
class Group(db.Model):
__tablename__ = "groups"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
leader = db.Column(db.Integer)
# list of the members in the group based on user id
members = db.Column(ARRAY(db.Integer))
def __init__(self, name, leader):
self.name = name
self.leader = leader
self.members = [leader]
def __repr__(self):
return "Name: {}, Leader: {}, Members: {}".format(self.name, self.leader, self.members)
def add_user(self, user_id):
self.members += [user_id]
My test function for updating the Group:
def add_2_to_group():
g = Group.query.all()[0]
g.add_user(2)
db.session.commit()
return redirect(url_for('show_groups'))
Thanks for any help!
As you have mentioned, the ARRAY datatype in sqlalchemy is immutable. This means it isn’t possible to add new data into array once it has been initialised.
To solve this, create class MutableList.
from sqlalchemy.ext.mutable import Mutable
class MutableList(Mutable, list):
def append(self, value):
list.append(self, value)
self.changed()
#classmethod
def coerce(cls, key, value):
if not isinstance(value, MutableList):
if isinstance(value, list):
return MutableList(value)
return Mutable.coerce(key, value)
else:
return value
This snippet allows you to extend a list to add mutability to it. So, now you can use the class above to create a mutable array type like:
class Group(db.Model):
...
members = db.Column(MutableList.as_mutable(ARRAY(db.Integer)))
...
You can use the flag_modified function to mark the property as having changed. In this example, you could change your add_user method to:
from sqlalchemy.orm.attributes import flag_modified
# ~~~
def add_user(self, user_id):
self.members += [user_id]
flag_modified(self, 'members')
To anyone in the future: so it turns out that arrays through SQLAlchemy are immutable. So, once they're initialized in the database, they can't change size. There's probably a way to do this, but there are better ways to do what we're trying to do.
This is a hacky solution, but what you can do is:
Store the existing array temporarily
Set the column value to None
Set the column value to the existing temporary array
For example:
g = Group.query.all()[0]
temp_array = g.members
g.members = None
db.session.commit()
db.session.refresh(g)
g.members = temp_array
db.session.commit()
In my case it was solved by using the new reference for storing a object variable and assiging that new created variable in object variable.so, Instead of updating the existing objects variable it will create a new reference address which reflect the changes.
Here in Model,
Table: question
optional_id = sa.Column(sa.ARRAY(sa.Integer), nullable=True)
In views,
option_list=list(question.optional_id if question.optional_id else [])
if option_list:
question.optional_id.clear()
option_list.append(obj.id)
question.optional_id=option_list
else:
question.optional_id=[obj.id]

Retrieve all records by table name and variable key in rails

I have a controller to which I pass the name of the table/model(: object), an attribute (: key) and the search criteria (: id):
def getAll
obj = params.require(:object)
datarecord = obj.classify.constantize
key=params[:key] + "= :i";
render json: { result: datarecord.find(:all, :conditions => [ key, {:i =>params[:id]}])}
end
Unfortunately, I don't get the expected result. The error message is 0 records found but 2 records expected.
For example, I pass following parameters to my controller:
object: "Opportunity"
key: "account_id"
id: 2
As a result, I want to retrieve all records in the opportunities table which belong the account with account_id=2.
Any idea what is wrong in my code?
Thanks,
Michael
Solution:
def getAll
obj = params.require(:object)
datarecord = obj.classify.constantize
render json: { result: datarecord.where(params[:key] => params[:id]) }
end
Recommendation:
def get_all
model = params.require(:model).safe_constantize
conditions = params[:conditions].to_h
records = model.where(conditions)
render json: { result: records }
end
Example request:
model = 'Opportunity'
conditions = { 'account_id' => 2 }
.find will always return 1 record what you want (if I understood correctly) is datarecord.where(key => params[:id])
where will return all records and you already have the key and the value that the key should be

Flask-SQLAlchemy won't update or delete a row

I want to either update a row in the database, if it exists, or create it if it doesn't.
I have a class that first sets the instance variable user:
self.user = models.User.query.filter_by(entity=self.entityUrl).first()
# can be None if not found
Then, later on in another class method I do this:
if self.user is not None:
self.user.foo = bar # etc. Change the attributes of self.user
else:
self.user = models.User(bar, ... ) # create a new model
db.session.add(self.user)
db.session.commit()
Problem is, the corresponding row in the database doesn't get updated. I've also tried this approach:
if self.user is not None:
self.user.foo = bar
else:
self.user = models.User(bar, ... )
db.session.add(self.user) # add it to the session either way
db.session.commit()
Here, the db.session.add() call fails with sqlalchemy.exc.InvalidRequestError: Object '<User at 0x7f4918172890>' is already attached to session '1' (this is '2')
And the first thing I tried was to delete the existing model in all cases, then create a new one, i.e.:
if self.user is not None:
db.session.delete(self.user)
self.user = models.User(bar, ... )
db.session.add(self.user)
db.session.commit()
In this case the db.session.delete() call fails with the same already attached to session '1' message as above.
Why is the object attached to a different session and not the same one? How do I do this correctly?
Make sure the foo attribute in your class existes. Next, maybe there is something wrong in the way you use it. Because I see you use "self.user....". Try the simplest thing first. Then step by step.
The following code is wrong:
if self.user is not None:
self.user.foo = bar
else:
self.user = models.User(bar, ... )
db.session.add(self.user) # add it to the session either way
db.session.commit()
NO need to db.session.add, if you want to update the record.
To update existing record using Flask-SQLAlchemy, you do not need to re-create the entire User object and add it to the session. You just update the specific field (e.g. foo) and thats it. You can then do the db commit.
You can do your exact requirement as below:
Step 1: query the existing user object
user = models.User.query.filter_by(entity=self.entityUrl).first()
Step 2:
if user is not None:
user.foo = bar
else:
user = User(...)
db.session.add(user)
Step 3: commit the db session.
db.session.commit()

RoR - different object_id inside after_initialize callback after obj.reload

I've written a method for my project which extends ActiveRecord models behaviour, I've stripped out most of it, consider the following code:
class ActiveRecord::Base
def self.has_translations
after_initialize :clear_translations_cache
def clear_translations_cache
binding.pry
#_translations = {}
end
end
end
Basically, I want the #_translations instance variable to get cleared when I .reload the instance from the database, but for some reason, after fetching an existing object from the database, executing a method which populates #_translations, and then executing object.reload, #_translations still contains the same data.
I know for sure that the callback gets executed when first fetching the object from database and when calling .reload. I used binding.pry to halt execution inside the callback method, but for some reason self.object_id inside .reload has a different object_id than my original object, and therefore #_translations in the original object doesn't get cleared.
Attached is the console output:
1.9.3p194 :008 > s = TranslatedItem.first
76: def clear_translations_cache
=> 77: #_translations = {}
78: end
[1] pry(#<TranslatedItem>)> self.class
=> TranslatedItem(id: integer, created_at: datetime, updated_at: datetime)
[2] pry(#<TranslatedItem>)> self.object_id
=> 70254243993580
[3] pry(#<TranslatedItem>)> exit
1.9.3p194 :009 > s.object_id
=> 70254243993580
1.9.3p194 :010 > s.reload
76: def clear_translations_cache
=> 77: #_translations = {}
78: end
[1] pry(#<ServiceLevel>)> self.class
=> TranslatedItem(id: integer, created_at: datetime, updated_at: datetime)
[2] pry(#<TranslatedItem>)> self.object_id
=> 70254259259120
I'm guessing the behavior you're seeing is related to how ActiveRecord reload works:
fresh_object = self.class.unscoped { self.class.find(self.id, options) }
#attributes.update(fresh_object.instance_variable_get('#attributes'))
You'll notice that it is creating a fresh object by finding it from the database, which explains why you are seeing two different object_id values in your callback method. The object that is initialized during the reload is only used for its attributes and then goes out of scope.
It's not clear from your question whether you were just curious why it behaved this way or if you're looking for an alternative way to do it.
Update:
You've got a few options if you're just looking for a way to clear the instance variable when you reload the model.
1) Add your own reload method that you can explicitly call:
class ActiveRecord::Base
def reload_everything
reload
#_translations = {}
end
end
object.reload_everything
2) Change the behavior of reload
module ReloadTranslations
def reload(*args)
super
#_translations = {}
end
end
ActiveRecord::Base.send(:include, ReloadTranslations)

Ruby libxml parsing and inserting to database

I am currently trying to read from an xml file which records the jobs on a PBS. I have succesfullly managed to parse the code, but am unable to insert the objtects into my database, i receive this error:
"You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.delete"
This is my Model:
require 'xml/libxml'
class Job < ActiveRecord::Base
JOB_DIR = File.join('data', 'jobs')
attr_reader :jobid, :user, :group, :jobname, :queue, :ctime
attr_reader :qtime, :etime, :start, :owner
def initialize(jobid, user, group, jobname, queue, ctime, qtime, etime, start, owner)
#jobid, #user, #group, #jobname, #queue = jobid, user, group, jobname, queue
#ctime, #qtime, #etime, #start, #owner = ctime, qtime, etime, start, owner
end
def self.find_all()
jobs = []
input_file = "#{JOB_DIR}/1.xml"
doc = XML::Document.file(input_file)
doc.find('//execution_record').each do |node|
jobs << Job.new(
node.find('jobid').to_a.first.content,
node.find('user').to_a.first.content,
node.find('group').to_a.first.content,
node.find('jobname').to_a.first.content,
node.find('queue').to_a.first.content,
node.find('ctime').to_a.first.content,
node.find('qtime').to_a.first.content,
node.find('etime').to_a.first.content,
node.find('start').to_a.first.content,
node.find('owner').to_a.first.content
)
end
jobs
end
end
An my Model Controller:
class JobController < ApplicationController
def index
#jobs = Job.find_all()
end
def create
#jobs = Job.find_all()
for job in #jobs
job.save
end
end
end
I would appreciate any help...Thank you!
I'm not sure on the causes of the error message you're seeing because I can't see anywhere that you're trying to invoke a delete method, however this does seem like a slightly confused use of ActiveRecord.
If you have a jobs database table with fields jobid, user, group, jobname etc. then ActiveRecord will create accessor methods for these and you should not be using attr_reader or overriding initialize. You should also not be setting values value instance variables (#jobid etc.) If you don't have such fields on your table then there is nothing in your current code to map the values from the XML the database fields.
Your def self.find_all method should probably be along the lines of:
def self.build_from_xml
jobs = []
input_file = "#{JOB_DIR}/1.xml"
doc = XML::Document.file(input_file)
doc.find('//execution_record').each do |node|
jobs << Job.new(
:jobid => node.find('jobid').to_a.first.content,
:user => node.find('user').to_a.first.content,
...
Rails used to have a method of its own find_all to retrieve all existing records from the database so your method name is probably a bit misleading. Rails tends to use the build verb to mean create a new model object but don't save it yet so that's why I've gone with a name like build_from_xml.