Rails after_add association callback not triggering - ruby-on-rails

On my model, I have the line below included:
has_many :notes, as: :notable, dependent: :destroy, after_add: :create_informal_note
When I create a note associated to the model where the line above resides, I expect the create_informal_note method to fire.
However, the method is not being fired.
What's wrong here?
Potential Problem:
The note is being created from a HTTP request to POST /api/v1/notes. In the request body's JSON, it includes notable_id and notable_type. It's setting the actual fields, not setting notable as an object.
POST /api/v1/notes
{
"note": {
"text": "Test note",
"notable_id": 1,
"notable_type": "AnotherModel"
}
}
And the output logs from my running Rails web server:
Processing by NotesController#create as JSON
Parameters: {"note"=>{"notable_id"=>"1", "notable_type"=>"AnotherModel", "text"=>"Test note", "email"=>1, "documents_attributes"=>nil}}
(0.2ms) BEGIN
SQL (2.0ms) INSERT INTO `notes` (`text`, `notable_id`, `notable_type`, `created_at`, `updated_at`) VALUES ('Test note', 1, 'AnotherModel', '2016-02-19 11:32:56.401216', '2016-02-19 11:32:56.401216')
(1.1ms) COMMIT
Could Rails be ignoring the association and hence the callback not triggering due to this?

This callback only works when adding the association via the << as per Mareq's comment above and the thread he mentioned.

Might be worth pointing out to future readers that this functionality is documented in the Active Record docs:
https://guides.rubyonrails.org/association_basics.html#association-callbacks
# These callbacks are called only when the associated objects
# are added or removed through the association collection:
# Triggers `before_add` callback
author.books << book
author.books = [book, book2]
# Does not trigger the `before_add` callback
book.update(author_id: 1)

Related

Rails: using eager_loading scope gives an error

I'm working on a Rails project, and trying to create a scope in my model instead of an instance method and preload that scope when needed. But I'm not very experienced with scopes and having trouble getting it to work or may be I'am doing it all wrong. I had an instance method doing the same thing, but noticing some n+1 issues with it. I was inspired by this article How to preload Rails scopes and wanted to try scopes.
(As a side note, I'm using ancestry gem)
I have tried three different ways to create the scope. They all works for Channel.find_by(name: "Travel").depth, but errors out for Channel.includes(:depth) or eager_load.
first try:
has_one :depth, -> { parent ? (parent.depth+1) : 0 }, class_name: "Channel"
2nd try:
has_one :depth, -> (node) {where("id: = ?", node).parent ? (node.parent.depth+1) : 0 }, class_name: "Channel"
3rd try:
has_one :depth, -> {where("id: = channels.id").parent ? (parent.depth+1) : 0 }, class_name: "Channel"
All three works fine in console for:
Channel.find_by(name: "Travel").depth
Channel Load (0.4ms) SELECT "channels".* FROM "channels" WHERE "channels"."name" = $1 LIMIT $2 [["name", "Travel"], ["LIMIT", 1]]
=> 2
..but
Channel.includes(:depth) gives me three different errors for each scope (1st, 2nd, 3rd);
Error for first scope:
NameError (undefined local variable or method `parent' for #<Channel::ActiveRecord_Relation:0x00007fdf867832d8>)
Error for 2nd scope:
ArgumentError (The association scope 'depth' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.)
Error for 3rd scope:
Object doesn't support #inspect
What am I doing wrong? Or, what is the best approach? I appreciate your time and help.
I think the .depth method is returning an integer value not associated records. Eager loading is the mechanism for loading the associated records of the objects returned by Model.find using as few queries as possible.
If you want to speed the depth method you need to enable the :cache_depth option. According to ancestry gem documentation:
:cache_depth
Cache the depth of each node in the 'ancestry_depth' column(default: false)
If you turn depth_caching on for an existing model:
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
- Build cache: TreeNode.rebuild_depth_cache!
In your model:
class [Model] < ActiveRecord::Base
has_ancestry, cache_depth: true
end

How to get serialized attribute in rails?

I have serialized column of Post model
serialize :user_ids
after save I can see in my console:
> p = Post.last
=> #<Post id: 30, title: "almost done2", created_at: "2017-05-08 15:09:40", updated_at: "2017-05-08 15:09:40", attachment: "LOGO_white.jpg", user_ids: [1, 2]>
I have permitted params :user_ids at my controller
def post_params
params.require(:post).permit(:title, :attachment, :user_ids)
end
All I need is to store #user_ids and copy to other model as atrribute,
but can't get instance like #user_ids = #Post.last.user_ids
Get error
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: users.post_id: SELECT "users".id FROM "users" WHERE "users"."post_id" = ?
Thanks for help!
I think you've chosen really wrong name for your serialized attribute, unfortunately!
See, Rails is trying to be helpful and relation_ids method is allowing you to call ids of all related object. Consider those two models
class User
belongs_to :post
end
class Post
has_many :users
end
You can call something like
Post.last.user_ids
Which will then try to find all users that belong to this post and give you back their ids.
Look at query that you get:
SELECT "users".id FROM "users" WHERE "users"."post_id" = ?
This is exactly what Rails is trying to do. Select USERS that belong to the post and give you their ids. However User does not have post_id column so this fails.
You're better off changing the name of your serialized column to something that won't confuse Rails. However, if you want to keep your column, you can override the method by putting this on the Post model
def user_ids
self[:user_ids]
end
This is non-ideal however, but will leave it up for you to decide what to do. My preference would be column rename, really. Will save you a lot of headeache

Rails association= return value and behavior

The guide does not say what return value would be for association= methods. For example the has_one association=
For the simple case, it returns the assigned object. However this is only when assignment succeeds.
Sometimes association= would persist the change in database immediately, for example a persisted record setting the has_one association.
How does association= react to assignment failure? (Can I tell if it fails?)
Is there a bang! version in which failure raises exception?
How does association= react to assignment failure? (Can I tell if it fails?)
It can't fail. Whatever you assign, it will either work as expected:
Behind the scenes, this means extracting the primary key from this
object and setting the associated object's foreign key to the same
value.
or will save the association as a string representation of passed in object, if the object is "invalid".
Is there a bang! version in which failure raises exception?
Nope, there is not.
The association= should not be able to fail. It is a simple assignment to a attribute on your attribute. There are no validations called by this method and the connection doesn't get persisted in the database until you call save.
The return value of assignments is the value you pass to it.
http://guides.rubyonrails.org/association_basics.html#has-one-association-reference-when-are-objects-saved-questionmark
So another part of the guide does talk about the return behavior for association assignment.
If association assignment fails, it returns false.
There is no bang version of this.
Update
Behaviors around :has_many/has_one through seems to be different.
Demo repository: https://github.com/lulalalalistia/association-assignment-demo
In the demo I seeded some data in first commit, and hard code validation error in second commit. Demo is using rails 4.2
has_many through
class Boss < ActiveRecord::Base
has_many :room_ownerships, as: :owner
has_many :rooms, through: :room_ownerships
end
When I add a room, exception is raised:
irb(main):008:0> b.rooms << Room.first
Boss Load (0.2ms) SELECT "bosses".* FROM "bosses" ORDER BY "bosses"."id" ASC LIMIT 1
Room Load (0.1ms) SELECT "rooms".* FROM "rooms" ORDER BY "rooms"."id" ASC LIMIT 1
(0.1ms) begin transaction
(0.1ms) rollback transaction
ActiveRecord::RecordInvalid: Validation failed: foo
irb(main):014:0> b.rooms
=> #<ActiveRecord::Associations::CollectionProxy []>
has_one through
class Employee < ActiveRecord::Base
has_one :room_ownership, as: :owner
has_one :room, through: :room_ownership
end
When I add a room I don't get exception:
irb(main):021:0> e.room = Room.first
Room Load (0.2ms) SELECT "rooms".* FROM "rooms" ORDER BY "rooms"."id" ASC LIMIT 1
RoomOwnership Load (0.1ms) SELECT "room_ownerships".* FROM "room_ownerships" WHERE "room_ownerships"."owner_id" = ? AND "room_ownerships"."owner_type" = ? LIMIT 1 [["owner_id", 1], ["owner_type", "Employee"]]
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Room id: 1, created_at: "2016-10-03 02:32:33", updated_at: "2016-10-03 02:32:33">
irb(main):022:0> e.room
=> #<Room id: 1, created_at: "2016-10-03 02:32:33", updated_at: "2016-10-03 02:32:33">
This makes it difficult to see whether the assignment succeeds or not.

How do I keep has_many :through relationships when serializing to JSON and back in Rails 4.0.3?

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

Create nested records on put/post with accepts_nested_attributes_for

I have two models,
Landscape:
class Landscape < ActiveRecord::Base
has_many :images, :as => :imageable
accepts_nested_attributes_for :images, :allow_destroy => true
attr_accessible :id, :name, :city, :state, :zip, :gps, :images, :images_attributes, :address1
def autosave_associated_records_for_images
logger.info "in autosave"
images.each do |image|
if new_image = Image.find(image.id) then
new_image.save!
else
self.images.build(image)
end
end
end
end
Image:
class Image < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
I'm sending post and put requests from an iPhone to update/create a landscape record with json. Here's an example POST request to create a new landscape record and a new associated image record
{
"landscape":{
"id":0,
"name":"New Landscape",
"city":"The City",
"state":"LA",
"zip":"71457",
"images_attributes":[
{
"id":0,
"image_data":"image data image data image data",
"is_thumbnail":1
}
],
"address1":"1800 Fancy Road"
}
}
When the server receives this, it spits out
ActiveRecord::RecordNotFound (Couldn't find Image with ID=0 for Landscape with ID=0):
So this seems to be some kind of circular reference issue, but it's not clear how to go about fixing it. Also of interest is that the autosave_associated_records_for_images doesn't ever seem to be called (also there is almost no documentation for that function, I had to look at the rails source).
I've read just about every SO post on accepts_nested_attributes_for with no luck.
Update
I have the records creating now, but I can't get rails to pass the image data back to the Image model. Let me illustrate:
Started POST "/landscapes" for 127.0.0.1 at 2011-07-22 19:43:23 -0500
Processing by LandscapesController#create as JSON
Parameters: {"landscape"=>{"name"=>"asdf", "id"=>0, "address1"=>"asdf", "city"=>"asdf", "images_attributes"=>[{"id"=>0, "image_data"=>"Im a bunch of image data image data image data", "is_thumbnail"=>1}]}}
SQL (0.1ms) BEGIN
SQL (1.6ms) describe `landscapes`
AREL (0.2ms) INSERT INTO `landscapes` (`address1`, `city`, `gps`, `name`, `state`, `zip`, `updated_at`, `created_at`) VALUES (NULL, NULL, NULL, NULL, NULL, NULL, '2011-07-23 00:43:23', '2011-07-23 00:43:23')
SQL (1.0ms) describe `images`
AREL (0.1ms) INSERT INTO `images` (`image_caption`, `image_data`, `is_thumbnail`, `created_at`, `updated_at`, `imageable_id`, `imageable_type`) VALUES (NULL, NULL, NULL, '2011-07-23 00:43:23', '2011-07-23 00:43:23', 46, 'Landscape')
SQL (0.2ms) COMMIT
Completed 201 Created in 37ms (Views: 2.2ms | ActiveRecord: 14.5ms)
That's what happens when I don't define autosave_asociated_records_for_images. However, if I define it like so:
def autosave_associated_records_for_images
logger.info "in autosave"
logger.info images.to_s
end
I see the following output:
Started POST "/landscapes" for 127.0.0.1 at 2011-07-22 19:50:57 -0500
Processing by LandscapesController#create as JSON
Parameters: {"landscape"=>{"name"=>"asdf", "id"=>0, "address1"=>"asdf", "city"=>"asdf", "images_attributes"=>[{"id"=>0, "image_data"=>"Im a bunch of image data image data image data", "is_thumbnail"=>1}]}}
SQL (0.1ms) BEGIN
SQL (1.6ms) describe `landscapes`
AREL (0.2ms) INSERT INTO `landscapes` (`address1`, `city`, `gps`, `name`, `state`, `zip`, `updated_at`, `created_at`) VALUES (NULL, NULL, NULL, NULL, NULL, NULL, '2011-07-23 00:50:57', '2011-07-23 00:50:57')
in autosave
[#<Image id: nil, image_caption: nil, image_data: nil, is_thumbnail: nil, created_at: nil, updated_at: nil, imageable_id: nil, imageable_type: "Landscape">]
SQL (0.2ms) COMMIT
Completed 201 Created in 32ms (Views: 2.2ms | ActiveRecord: 2.1ms)
This is very strange! It's creating the relationship properly, but it's not actually populating the database with the data I'm sending. Any ideas?
I never got either of the solutions here to work. What I did instead was do this manually in the controller by looping through the params.
I guess the error comes from:
Image.find(image.id)
Indeed this spits an exception when nothing is found.
Simply replace_it with:
Image.find_by_id(image.id)
It will spit nil if nothing found.
The autosave_associated_records_for_images method includes too much responsibilities for that Class. The solution is to move this method logic into Image class. Every ActiveRecord class instance have default callbacks, such as before_validation, before_create. I suggest you to use them first

Resources