Rails association= return value and behavior - ruby-on-rails

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.

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

Incrementing a field with a Ruby on Rails call back

I have a model Graph with fields name and version. I want the name and version to be unique so have declared
validates_uniqueness_of :name, scope: :version
When a new Graph object is created, if it has the same name as a previous Graph object, then I want the version to be incremented. However, I do not want the version incremented upon update. So far I have implemented this with a call back
class Graph < ApplicationRecord
enum preservation_status: [:unlocked, :locked]
validates_presence_of :name
validates_presence_of :version
validates_uniqueness_of :name, scope: :version
has_many :graph_points, dependent: :destroy
belongs_to :data_set
validates_uniqueness_of :name, scope: :version
before_validation :set_version, on: :create
private
def set_version
graphs = Graph.where(name: name)
return if graphs.empty?
self.version = graphs.maximum(:version) + 1
end
This does not work. If a graph of that name exists, then the code appears to enter an infinite loop and I have to restart the server. How to I fix this?
For instance, if I have have one existing graph with name 'Plot_quick Male', with version = 1, then try to create a new graph with the same name, the sql that results is below:
(0.9ms) SELECT COUNT(*) FROM "graphs" WHERE "graphs"."name" = $1 [["name", "Plot_quick Male "]]
(0.7ms) SELECT MAX("graphs"."version") FROM "graphs" WHERE "graphs"."name" = $1 [["name", "Plot_quick Male "]]
Graph Exists (0.5ms) SELECT 1 AS one FROM "graphs" WHERE "graphs"."name" = $1 AND "graphs"."version" = $2 LIMIT $3 [["name", "Plot_quick Male "], ["version", 2], ["LIMIT", 1]]
and then the server hangs.
The default value for version is set in the schema i.e.
t.integer "version", default: 0
I managed to eliminate the problem from development by deleting all versions of graph and recreating new ones. It might have been something strange in some of the graphs that were created with earlier versions of the code.
I will mark this is the answer for the time being, but if anyone comes up with a answer that make sense, I will have a look.

Rails after_add association callback not triggering

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)

Ruby ActiveRecord fundamentals : appending an object to an array with << might update its attributes?

I just noticed that one of the attributes of an object was being updated when this object was appended to an array. This behavior looks very surprising to me and I thought I might be missing something fundamental about ActiveRecord.
In my app, every idea has a father (through its father_id attribute). This association is set in models/idea.rb with the following :
class Idea < ActiveRecord::Base
belongs_to :father, :class_name => "Idea" # , :foreign_key => "idea_id"
has_many :children, :class_name => "Idea", :foreign_key => "father_id", :dependent => :destroy
[...]
Here is what happens in rails console :
I first select a given idea :
irb(main):003:0> n = Idea.find(1492)
Idea Load (1.1ms) SELECT "ideas".* FROM "ideas" WHERE "ideas"."id" = $1 LIMIT 1 [["id", 1492]]
=> #<Idea id: 1492, father_id: 1407, [...]>
I then retrieve its children through the associations :
irb(main):004:0> c = n.children
Idea Load (0.5ms) SELECT "ideas".* FROM "ideas" WHERE "ideas"."father_id" = 1492
=> []
It doesn't have any, which is ok.
I then want to append the idea itself to the 'c' variable but this triggers an unwanted UPDATE action in the database :
irb(main):005:0> c << n
(0.1ms) BEGIN
(0.9ms) UPDATE "ideas" SET "father_id" = 1492, "updated_at" = '2013-12-06 12:57:25.982619' WHERE "ideas"."id" = 1492
(0.4ms) COMMIT
=> [#<Idea id: 1492, father_id: 1492, [...]>]
The father_id attribute, which had the value 1407 now has the value 1492, i.e. the idea's id.
Can anyone explain me why this happens and how I can create an array that includes an object's children and the object itself without altering the object's attributes ?
NB : I'm using ruby 1.9.3p448 (2013-06-27 revision 41675) [x86_64-darwin13.0.0]
This is expected behavior. You're adding a new idea to the set of ideas belonging to a specific father. This happens because it's not an array you're appending to, it's an ActiveRecord association. In your console, try n.children.class.
If you want a flat array which won't modify objects appended to it, you want:
c = n.children.to_a
c << n

Rails ActiveRecord: legacy table without primary key shows nil for result?

I've got a Rails app that'll be sitting on top of a legacy database with some ugly tables that I'm having to deal with. One is a feature_attributes table related to features. Problem is that this feature_attributes table doesn't have a primary key. I wouldn't think that'd be a problem, but evidently it is. I've got my model name which is different from the table name, but I'm using set_table_name to specify the right one.
class Feature < ActiveRecord::Base
has_many :feature_attributes
end
class FeatureAttribute < ActiveRecord::Base
set_table_name 'feature_attribute'
belongs_to :feature
end
Once I load a feature that I know has related feature_attributes and call feature.feature_attributes on it, I get nil. Matter of fact, even FeatureAttribute.first gives me nil. The result of FeatureAttribute.any? returns false. I'm worried that ActiveRecord isn't reading any of the data from the table because there isn't a primary key. Is that what's going on here?
The feature_attribute table has the following columns.
feature_id
attribute_name
attribute_value
created_date
modified_date
Help!
I'll also note that running the generated SQL directly against the MySQL server actually gets me the rows I want.
SELECT `feature_attribute`.* FROM `feature_attribute` WHERE `feature_attribute`.`feature_id` = 24;
EDIT: I am so sorry. Next time I'll learn to check my database.yml. Evidently I was reading from a test database that had an identical feature table, but the feature_attribute table was totally empty.
I feel like a moron. Thanks for your help, everyone; I'm up voting you all for your troubles. I did like just about everybody's answer. (Can I down vote myself? :))
Try to also set the primary key:
class FeatureAttribute < ActiveRecord::Base
set_table_name 'feature_attribute'
set_primary_key 'feature_id'
belongs_to :feature
end
UPDATE
I think your problem lies anywhere else. I just tested and ActiveRecords works fine with tables without a primary key:
For a simple table:
class CreateThings < ActiveRecord::Migration
def change
create_table :things, :id => false do |t|
t.string :name
t.timestamps
end
end
end
in console:
Loading development environment (Rails 3.1.1)
irb(main):001:0> Thing.create(:name=>'A name for the thing')
(0.1ms) BEGIN
SQL (0.3ms) INSERT INTO `things` (`created_at`, `name`, `updated_at`) VALUES ('2011-11-02 16:33:48', 'A name for the thing', '2011-11-02 16:33:48')
(40.3ms) COMMIT
=> #<Thing name: "A name for the thing", created_at: "2011-11-02 16:33:48", updated_at: "2011-11-02 16:33:48">
irb(main):002:0> Thing.first
Thing Load (0.7ms) SELECT `things`.* FROM `things` LIMIT 1
=> #<Thing name: "A name for the thing", created_at: "2011-11-02 16:33:48", updated_at: "2011-11-02 16:33:48">
irb(main):003:0>
UPDATE 2
Not very fine:
irb(main):003:0> Thing.create(:name=>'Another thing')
(0.2ms) BEGIN
SQL (0.4ms) INSERT INTO `things` (`created_at`, `name`, `updated_at`) VALUES ('2011-11-02 16:40:59', 'Another thing', '2011-11-02 16:40:59')
(35.4ms) COMMIT
=> #<Thing name: "Another thing", created_at: "2011-11-02 16:40:59", updated_at: "2011-11-02 16:40:59">
irb(main):004:0> Thing.first
Thing Load (0.5ms) SELECT `things`.* FROM `things` LIMIT 1
=> #<Thing name: "A name for the thing", created_at: "2011-11-02 16:33:48", updated_at: "2011-11-02 16:33:48">
irb(main):005:0> Thing.last
Thing Load (11.8ms) SELECT `things`.* FROM `things` ORDER BY `things`.`` DESC LIMIT 1
Mysql2::Error: Unknown column 'things.' in 'order clause': SELECT `things`.* FROM `things` ORDER BY `things`.`` DESC LIMIT 1
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'things.' in 'order clause': SELECT `things`.* FROM `things` ORDER BY `things`.`` DESC LIMIT 1
If a certain attribute can occur only once for a feature, you assume attribute_name is the key, in combination with the feature_id. Rails does not support composite primary keys out of the box, but there is a gem called composite_primary_keys that supports just that.
In your model you can then write
set_primary_keys :feature_id, :attribute_name
If an attribute-name could occur multiple time for a single feature, the combination of feature_id, attribute_name and attribute_value is the key, and you should write
set_primary_keys :feature_id, :attribute_name, :attribute_value
Hope this helps.
[edit] Alternative approach:
The above clearly is not enough, so you could also do the following:
class Feature
has_many :feature_attributes, :finder_sql => 'select * from feature_attributes where feature_id=\'#{id}\''
end
Hope that this does help :)
class Feature < ActiveRecord::Base
self.table_name = 'legacy_table_name'
self.primary_key = nil
end
Then query using Feature.find_by field:'value' or other ActiveRecord queries.
If it isn't possible to update the table to just add a autoincrement primary key, then you might be better served dealing with it manually:
class Feature < ActiveRecord::Base
# get rid of has_many feature_attributes and use this instead
def feature_attributes
FeatureAttribute.find_by_sql(["select * from feature_attribute where feature_id = ?", self.id])
end
end

Resources