I'm interested in adding two objects to a serialized meta-data column in my activity feed model (Rails 3.1) to cut down on db calls.
Example: I have an Activity model with a data:text column which is serialized. I know I can add a Book object to this model and get it back as so:
test = Activity.create(:data => Book.find(1))
test.book.author # => James Joyce
Can I add two objects to this column (e.g. a Book and a User)? I tried using hashes/arrays but couldn't get them to work properly. Thanks in advance.
Here's how you might use a hash:
test = Activity.create(:data => {:book => Book.find(1), :user => User.find(1)})
test.data[:book] # => #<Book id:1 ...>
test.data[:user] # => #<User id:1 ...>
Related
I have a model House that belongs to a User. House has_many cars. In my models, i've declared the necessary accepts_nested_attributes_for, and my nested attributes have the appropriate _attributes and id fields.
I want to be able to disassociate my Car model from House, but not User.
Let's say I have a saved House object like this:
"house" => {"owner" => "bob, "car_attributes" => [{"id" => 1, "type" => "toyota"},{"id" => 2, "type" => "honda"}]}
I want to remove (but not delete) a car object from House and update it so it looks like this:
"house" => {"owner" => "bob, "car_attributes" => [{"id" => 1, "type" => "toyota"}]}
Obviously when I call #house.update_attributes({"owner" => "bob, "car_attributes" => [{"id" => 1, "type" => "toyota"}]}), the belongs_to association is not removed. Is there any simple way to disassociate a car object from its parent?
Looking through the Rails documentation, there is a collection method delete that does just that -i.e sets the parent foreign keys to null. The problem is, I would have to mark each object I want to dissassociate in the client, send it over, then walk through each nested attribute and call delete() manually where necessary, which doesn't seem very railsy to me.
Is there an easier way to update a parent model by disassociating (without destroying) its nested attributes in rails?
How do I convert to JSON and back and keep the relationships? It thinks they don't exist when I un-parcel the object!
irb(main):106:0* p = Post.last
=> #<Post ...
irb(main):107:0> p.tags
=> #<ActiveRecord::Associations::CollectionProxy [#<Tag id: 41, ...
irb(main):109:0* p.tags.count
=> 2 #### !!!!!!!!!!!!
irb(main):110:0> json = p.to_json
=> "{\"id\":113,\"title\":... }"
irb(main):111:0> p2 = Post.new( JSON.parse(json) )
=> #<Post id: 113, title: ...
irb(main):112:0> p2.tags
=> #<ActiveRecord::Associations::CollectionProxy []>
irb(main):113:0> p2.tags.count
=> 0 #### !!!!!!!!!!!!
Here is the model
class Post < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :tags, :through => :taggings
What someone suggested, but doesn't work
irb(main):206:0* Post.new.from_json p.to_json(include: :tags)
ActiveRecord::AssociationTypeMismatch: Tag(#60747984) expected, got Hash(#15487524)
I simulated the exact same scenario like yours and found out:
Whenever a model(Post) has a has_many through association then upon creating an instance of that Model i.e., Post passing a Hash for eg: Post.new( JSON.parse(json) ) or Post.new(id: 113) seems like Rails treats them differently although they are pointing to the same record.
I ran the following commands in the sequence as given below:
p = Post.last
p.tags
p.tags.count
json = p.to_json
p2 = Post.new( JSON.parse(json) )
p2.tags
p2.tags.count ## Gives incorrect count
p3 = Post.find(JSON.parse(json)["id"]) ### See notes below
p3.tags
p3.tags.count ## Gives the correct count
Instead of creating a new instance of Post using Hash directly, I fetched the record from database using the id obtained from deserializing json. In this case, the instance p3 and instance p2 refer to the same Post but Rails is interpreting them differently.
Disclaimer: This is not, in any way, an ideal solution (and I would call it down-right cheesy), but its about the only thing I've been able to come up with for your scenario.
What Kirti Thorat said is correct; when you have a dependent object, Rails expects the association in the hash to be of that specific class (in your case, a Tag object). Hence the error you're getting: Tag expected...got Hash.
Here comes the cheesy part: One way to properly deserialize a complex object is to leverage the accepts_nested_attributes_for method. By using this method, you'll allow your Post class to properly deserialize the dependent Tag key-value pairs to proper Tag objects. Start with this:
class Post < ActiveRecord::Base
accepts_nested_attributes_for :tags
# rest of class
end
Since accepts_nested_attributes_for searches for a key with the word _attributes for the given association, you'll have to alter the JSON when it is rendered to accommodate this by overriding the as_json method in your Post class, like so:
def as_json(options={})
json_hash = super.as_json(options)
unless json_hash["tags"].nil?
json_hash["tags_attributes"] = json_hash["tags"] # Renaming the key
json_hash.delete("tags") # remove the now unnecessary "tags" key
end
json_hash # don't forget to return this at the end
end
Side note: There are lots of json building gems such as acts_as_api that will allow you to remove this as_json overriding business
So now your rendered JSON has all the Post attributes, plus an array of tag attribute key-value pairs under the key tags_attributes.
Technically speaking, if you were to deserialize this rendered JSON in the manner suggested by Kirti, it would work and you would get back a properly populated active record object. However, unfortunately, the presence of the id attributes in both the parent Post object, and the dependent tag objects means that active record will fire off at least one SQL query. It will do a quick lookup for the tags to determine if anything needs to be added or deleted, as per the specifications of the has_many relationship (specifically, the collection=objects part).
Since you said you'd like to avoid hitting the database, the only solution I've been able to find is to render to JSON in the same way leesungchul suggested, but specifically excluding the id fields:
p_json = p.to_json(except: [:id], include: {tags: {except: :id}})
If you then do:
p2 = Post.new(JSON.parse(p_json))
You should get back a fully rendered Post object without any DB calls.
This, of course, assumes you don't need those id fields. In the event you do...frankly I'm not certain of a better solution other than to rename the id fields in the as_json method.
Also note: With this method, because of the lack of id fields, you won't be able to use p2.tags.count; it will return zero. You'll have to use .length instead.
You can try
p2.as_json( :include => :tags )
When you call
p2.tags
you get correct tags but p2 is not saved in the database yet. This seems the reason for
p2.tags.count
giving a 0 all the time.
If you actually do something like:
p2.id = Post.maximum(:id) + 1
p2.tags #Edit: This needs to be done to fetch the tags mapped to p from the database.
p2.save
p2.tags.count
You get the correct count
I have a system that occasionally spits out 2 objects. Is there any magic to create! that would allow it to create two objects? E.G. if I say
self.class.create! make_up_attributes
and make_up_attributes passes a 2 item hash, can this create 2 objects of type self?
(note, create is probably an ActiveRecord method, in Rails)
Yep, simply pass in an Array of attribute hashes, like so:
self.class.create!([{:name => "John", :age => 26},
{:name => "Fred", :age => 50}])
See the docs
I'm looking for a way to store a serialized value of eg. IDs in a column. In before claims that this is not an optimal design: the column is used for IDs of associated records, but will only be used when displaying the record - so no queries are made with selection on the column and no joins will be made on this column either.
In Rails I can serialize the column by using:
class Activity
serialize :data
end
This encodes the column as YAML. For legacy sake and since I'm only storing one dimensional arrays containing only integers, I find it more suitable to store it as a comma-separated value.
I've successfully implemented basic accessors like this:
def data=(ids)
ids = ids.join(",") if ids.is_a?(Array)
write_attribute(:data, ids)
end
def data
(read_attribute(:data) || "").split(",")
end
This works pretty fine. However I'd like to add array-like methods to this attribute:
activity = Activity.first
activity.data << 42
...
How would I do this?
You can do it with composed_of feature as explained in this post.
It should be something like:
composed_of :data, :class_name => 'Array', :mapping => %w(data to_csv),
:constructor => Proc.new {|column| column.to_csv},
:converter => Proc.new {|column| column.to_csv}
after_validation do |u|
u.data = u.data if u.data.dirty? # Force to serialize
end
Haven't tested it though.
You can use serialize with a custom coder in rails 3.1.
See my answer to this question. :-)
I have a method in rails that is doing something like this:
a = Foo.new("bar")
a.save
b = Foo.new("baz")
b.save
...
x = Foo.new("123", :parent_id => a.id)
x.save
...
z = Foo.new("zxy", :parent_id => b.id)
z.save
The problem is this takes longer and longer the more entities I add. I suspect this is because it has to hit the database for every record. Since they are nested, I know I can't save the children before the parents are saved, but I would like to save all of the parents at once, and then all of the children. It would be nice to do something like:
a = Foo.new("bar")
b = Foo.new("baz")
...
saveall(a,b,...)
x = Foo.new("123", :parent_id => a.id)
...
z = Foo.new("zxy", :parent_id => b.id)
saveall(x,...,z)
That would do it all in only two database hits. Is there an easy way to do this in rails, or am I stuck doing it one at a time?
Since you need to perform multiple inserts, database will be hit multiple times. The delay in your case is because each save is done in different DB transactions. You can reduce the latency by enclosing all your operations in one transaction.
class Foo
belongs_to :parent, :class_name => "Foo"
has_many :children, :class_name => "Foo", :foreign_key=> "parent_id"
end
Your save method might look like this:
# build the parent and the children
a = Foo.new(:name => "bar")
a.children.build(:name => "123")
b = Foo.new("baz")
b.children.build(:name => "zxy")
#save parents and their children in one transaction
Foo.transaction do
a.save!
b.save!
end
The save call on the parent object saves the child objects.
You might try using Foo.create instead of Foo.new. Create "Creates an object (or multiple objects) and saves it to the database, if validations pass. The resulting object is returned whether the object was saved successfully to the database or not."
You can create multiple objects like this:
# Create an Array of new objects
parents = Foo.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
Then, for each parent, you can also use create to add to its association:
parents.each do |parent|
parent.children.create (:child_name => 'abc')
end
I recommend reading both the ActiveRecord documentation and the Rails Guides on ActiveRecord query interface and ActiveRecord associations. The latter contains a guide of all the methods a class gains when you declare an association.
insert_all (Rails 6+)
Rails 6 introduced a new method insert_all, which inserts multiple records into the database in a single SQL INSERT statement.
Also, this method does not instantiate any models and does not call Active Record callbacks or validations.
So,
Foo.insert_all([
{ first_name: 'Jamie' },
{ first_name: 'Jeremy' }
])
it is significantly more efficient than
Foo.create([
{ first_name: 'Jamie' },
{ first_name: 'Jeremy' }
])
if all you want to do is to insert new records.
One of the two answers found somewhere else: by Beerlington.
Those two are your best bet for performance
I think your best bet performance-wise is going to be to use SQL, and bulk insert multiple rows per query. If you can build an INSERT statement that does something like:
INSERT INTO foos_bars (foo_id,bar_id) VALUES (1,1),(1,2),(1,3)....
You should be able to insert thousands of rows in a single query. I didn't try your mass_habtm method, but it seems like you could to something like:
bars = Bar.find_all_by_some_attribute(:a)
foo = Foo.create
values = bars.map {|bar| "(#{foo.id},#{bar.id})"}.join(",")
connection.execute("INSERT INTO foos_bars (foo_id, bar_id) VALUES
#{values}")
Also, if you are searching Bar by "some_attribute", make sure you have that field indexed in your database.
OR
You still might have a look at activerecord-import. It's right that it doesn't work without a model, but you could create a Model just for the import.
FooBar.import [:foo_id, :bar_id], [[1,2], [1,3]]
Cheers
you need to use this gem "FastInserter" -> https://github.com/joinhandshake/fast_inserter
and inserting a large number and thousands of records is fast because this gem skips active record, and only uses a single sql raw query
You don't need a gem to hit DB fast and only once!
Jackrg has worked it out for us:
https://gist.github.com/jackrg/76ade1724bd816292e4e