Nested Attributes not updating - has-many

With the following models:
class Location < ActiveRecord::Base
has_many :group_locations
has_many :groups, :through => :group_locations
accepts_nested_attributes_for :group_locations
end
class GroupLocation < ActiveRecord::Base
belongs_to :group
belongs_to :location
end
class Group < ActiveRecord::Base
has_many :group_locations
has_many :locations, :through => :group_locations
end
the following commands in rails console does not update the associated records:
>> l = Location.find(1)
=> #<Location id: 1, phone: "(949) 788-9999", ... created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-07 23:57:32">
\>\> l.group_locations
=> [#<GroupLocation group_id: 4, location_id: 1, created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-02 00:58:07">, #<GroupLocation group_id: **37**, location_id: 1,
created_at: "2011-06-02 00:58:07", updated_at: "2011-06-02 00:58:07">]
>> l.update_attributes(:phone => "(949) 788-9998", :group_locations_attributes =>
[{:group_id => 4, :location_id => 1}, {:group_id => **38**, :location_id => 1}])
=> true
>> l
=> #<Location id: 1, phone: "(949) 788-9998", ... created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-08 02:05:00">
>> l.group_locations
=> [#<GroupLocation group_id: 4, location_id: 1, created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-02 00:58:07">, #<GroupLocation group_id: **37**, location_id: 1,
created_at: "2011-06-02 00:58:07", updated_at: "2011-06-02 00:58:07">]
Note that the update_attributes call attempts to change the second GroupLocation to have group_id = 38, but the change is not made (even though the phone number did change). After looking at the code generated when this was implemented in the controller and view, changing the array to a hash (which is what is created in that case) has no different results (and the form/controller) have the same effect of not updating the associated records even though the main record is updated.
Any idea what I need to do to get the nested attributes to update?

From the logs you've displayed, it doesn't appear that your GroupLocation model has an :id primary key on it. While the join table for a HABTM has just the foreign keys (group_id, location_id) on it, the model used for a has_many :through association does need a primary key as well, :id by default. Otherwise, there is no way to determine which of the child objects to update in the case of an update.
Think of it this way - you are creating your association through another discrete model that should be able to stand entirely on its own.
The convention for nested attributes is if the hash passed to the nested_attributes includes an :id, then it is considered an update, if it doesn't then it's considered a create. In your case, you're not passing in an :id, so you get new GroupLocation records where you just wanted to update existing.
I believe, also, that once you have this in place correctly, you will be able to get rid of the attr_accessible, I don't think that should be necessary.
For good info on the nested attributes functionality that covers most of this, check out this page.

The actual answer is that the nested attributes must be accessible via attr_accessible. "accepts_nested_attributes" will only do what I want if it is accompanied by "attr_accessible :group_locations".

Related

Active Record, Polymorphic Has Many Through with STI

I'm having some trouble with a Polymorphic Has Many Through association with STI. Let me explain what I'm trying to do:
Let's say I have a Contract. A Contract can have many Companies as parties to the agreement, namely, a Contract can have more than one Licensor (the party granting rights in the Contract) and more than one Licensee (the party receiving rights under the Contract). Both Licensors and Licensees are Companies that can be parties of more than one Contract.
So far I have the following code:
#contract.rb
class Contract < ApplicationRecord
has_many :relationships, dependent: :destroy
has_many :companies, through: :relationships
has_many :licensors, through: :relationships, source: :party, source_type: "Licensor"
has_many :licensees, through: :relationships, source: :party, source_type: "Licensee"
end
#relationship.rb
class Relationship < ApplicationRecord
belongs_to :contract
belongs_to :party, polymorphic: true
end
#company.rb
class Company < ApplicationRecord
has_many :relationships, as: :party, dependent: :destroy
has_many :contracts, through: :relationships
end
#licensor.rb
class Licensor < Company
end
#licensee.rb
class Licensee < Company
end
I think I'm very close on getting this to work. So far, the above code allows me to create a new contract and add licensors and licensees, as follows:
c = Contract.new(nickname:"Test Contract")
lor = c.licensors.new(name:"The Licensor Company")
lee = c.licensees.new(name:"Some Licensee Company")
c.save
Then the following will work:
c.licensors # results in...
Licensor Load (1.1ms) SELECT "companies".* FROM "companies" INNER JOIN "relationships" ON "companies"."id" = "relationships"."party_id" WHERE "relationships"."contract_id" = $1 AND "relationships"."party_type" = $2 LIMIT $3 [["contract_id", 1], ["party_type", "Licensor"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Licensor id: 1, name: "The Licensor Company", created_at: "2018-02-14 19:46:19", updated_at: "2018-02-14 19:46:19">]>
c.licensees # results in...
Licensee Load (1.3ms) SELECT "companies".* FROM "companies" INNER JOIN "relationships" ON "companies"."id" = "relationships"."party_id" WHERE "relationships"."contract_id" = $1 AND "relationships"."party_type" = $2 LIMIT $3 [["contract_id", 1], ["party_type", "Licensee"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Licensee id: 2, name: "Some Licensee Company", created_at: "2018-02-14 19:46:19", updated_at: "2018-02-14 19:46:19">]>
So the Licensor and Licensee are being correctly created and their party_type is being correctly set.
Unfortunately, what doesn't work is the following:
lor = Licensor.first
lor.contracts # which results in...
Contract Load (0.9ms) SELECT "contracts".* FROM "contracts" INNER JOIN "relationships" ON "contracts"."id" = "relationships"."contract_id" WHERE "relationships"."party_id" = $1 AND "relationships"."party_type" = $2 LIMIT $3 [["party_id", 1], ["party_type", "Company"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
As you can see, I'm unable to query a list of Contracts for a specific Licensor or Licensee. It appears this is because my current setup results in query with a party_type of "Company". I assume this is because both Licensor and Licensee inherit from Company.
Is there a way to set the party_type in the Has Many Through association in the Licensor or Licensee models?
Any help would be greatly appreciated.
You could use rewhere. In Company define relationships with an association scope like this:
has_many :relationships, ->(x) { rewhere(party_type: x.class.name) }, as: :party, dependent: :destroy
You can see the party_type of Licensor being queried in the join:
>> l = Licensor.first
Licensor Load (1.4ms) SELECT "companies".* FROM "companies" WHERE "companies"."type" IN ('Licensor') ORDER BY "companies"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<Licensor id: 1, name: "The Licensor Company", type: "Licensor", created_at: "2018-02-23 00:38:13", updated_at: "2018-02-23 00:38:13">
>> l.contracts
Contract Load (1.9ms) SELECT "contracts".* FROM "contracts" INNER JOIN "relationships" ON "contracts"."id" = "relationships"."contract_id" WHERE "relationships"."party_id" = $1 AND "relationships"."party_type" = $2 LIMIT $3 [["party_id", 1], ["party_type", "Licensor"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Contract id: 1, name: "Test Contract", created_at: "2018-02-23 00:38:13", updated_at: "2018-02-23 00:38:13">]>

Add Single Table Inheritance (STI) to existing models

I currently have multiple tables that are very similar. I should probably have created them with STI.
TypeOne < ActiveRecord::Base
TypeTwo < ActiveRecord::Base
TypeThree < ActiveRecord::Base
TypeOne(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime)
TypeTwo(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime)
TypeThree(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime)
I am now trying to add STI to these. I created a BaseModel and added a type to that model.
BaseModel(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime, type: string)
I also ran a migration and added to all of the types, a type column.
class AddTypeToTables < ActiveRecord::Migration
def change
add_column :type_ones, :type, :string
add_column :type_twos, :type, :string
add_column :type_threes, :type, :string
end
end
I want to combine all of the type tables into a STI. There is existing data in the models. If I were to combine them into a single table, I imagine the id's for the respective tables would conflict. For example:
#<TypeOne id: 4, parent_id: 1, created_at: "2015-05-08 18:39:09", updated_at: "2015-09-07 19:42:03">
#<TypeTwo id: 4, parent_id: 1, created_at: "2015-04-08 17:48:59", updated_at: "2015-09-07 14:17:48">
If I try to use becomes, it appears to change the class, but I cannot find the record in the BaseModel
TypeOne.last.becomes!(BaseModel)
#<BaseModel id: 4, parent_id: 1, created_at: "2015-05-08 18:39:09", updated_at: "2015-09-07 19:42:03">
BaseModel.all
=> []
I have also tried to change the type column of the inherited table to the basemodel
to = TypeOne.first
to.type = "BaseModel"
to.save
BaseModel.all
=> []
I have tried to change the classes for each to be a child of the BaseModel
TypeOne < BaseModel
TypeTwo < BaseModel
TypeThree < BaseModel
When I do this, I lose the connection to the existing data and each of the models appear empty.
How can I combine the existing tables?
As you've tagged it with PostgreSQL I'll include how to do what I suggested in my comment as an answer:
INSERT INTO base_model SELECT * FROM type_one ORDER BY id ASC;
INSERT INTO base_model SELECT * FROM type_two ORDER BY id ASC;
INSERT INTO base_model SELECT * FROM type_three ORDER BY id ASC;
To safely do this on a production dataset, put the SQL in a migration in db/migrate (i.e. in a file like db/migrate/20150907185938_integrate_tables.rb) and test it on your local database first. This should get you pretty close:
class IntegrateTables < ActiveRecord::Migration
def up
execute "INSERT INTO base_model SELECT * FROM type_one ORDER BY id ASC;"
execute "INSERT INTO base_model SELECT * FROM type_two ORDER BY id ASC;"
execute "INSERT INTO base_model SELECT * FROM type_three ORDER BY id ASC;"
end
def down
raise ActiveRecord::IrreversibleMigration, "It is unclear where original data stops and inserted data begins, can't migrate down"
end
end
Please mark this answer as accepted if it worked for you :)

How to I insert an array into a Postgresql table using ActiveRecord?

I have active_record setup to use a Postgresql database. One of the columns is a character varying[] (basically a varchar array).
Anyway, my import routine then reads a tab-delimited text file and inserts records. All is fine until I get to an array. The column that should be converted to an array is comma separated. But the line itself is tab separated.
A sample of the data I'm importing looks like (tab delimited):
Col1 Col2 Col3 Col4
----------------------------------------------
Apple Pear Sweet,Round,Green Fruit
Col3 is imported like (ruby): col3.split(/,/) which gives me an array in Ruby. But active_record bombs out with:
PG::Error: ERROR: array value must start with "{" or dimension information (ActiveRecord::StatementInvalid)
How can I insert that column correctly?
Also, sometimes, col3 will be NULL.
I was able to insert using the following Ruby code:
alternatenames = '{' + s[3].split(/,/).map {|n| '"' + n + '"'}.join(",") + '}'
Check Postgres' docs on this: http://www.postgresql.org/docs/9.2/static/arrays.html
You can instantiate a model using an array like [:My, :symbols] or ["My", "Strings"], however it (in my experience and form what it seems in the docs) will save the elements as strings.
Search.create(tokens: [{hash: 'value'}, {test: "fails"}])
=> TypeError: can't cast Hash to string
Where as:
[15] pry(main)> Search.create(tokens: [:G, :F])
=> #<Search id: 78, tokens: [:G, :F], created_at: "2013-12-18 06:29:36", updated_at: "2013-12-18 06:29:36">
[16] pry(main)> Search.last
=> #<Search id: 78, tokens: ["G", "F"], created_at: "2013-12-18 06:29:36", updated_at: "2013-12-18 06:29:36">
In my tests, I have a SearchEngine, Search, and Term.
class SearchEngine < ActiveRecord::Base
has_and_belongs_to_many :terms
has_many :searches, through: :terms
end
class Term < ActiveRecord::Base
has_and_belongs_to_many :searches
has_and_belongs_to_many :searche_engines
end
class Search < ActiveRecord::Base
has_many :rankings
has_many :results, through: :rankings
has_and_belongs_to_many :terms
has_many :search_engines, through :terms
end
# These work:
# these next two are the way postgrespl says to query against the array. You get the
Search.where(tokens: '{A,B}')
Search.where(tokens: '{C,D}').first_or_create
[3] pry(main)> Search.where(tokens: ['C','D']).first
ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR: array value must start with "{" or dimension information
[4] pry(main)> Search.where(tokens: '{C,D}').first
=> #<Search id: 77, tokens: ["C", "D"], created_at: "2013-12-18 06:27:24", updated_at: "2013-12-18 06:27:24">
term = "accident"
Search.where("? = ANY (tokens)", term).first
=> #<Search id: 8, tokens: ["accident", "prevention", "safety"], created_at: "2013-12-18 07:48:13", updated_at: "2013-12-18 07:48:13">
Search.create(tokens: [:Aortic, :Any, :Other, :Elements])
Search.where("'Aortic' = ANY (tokens)").first
Parent.first.first_relationships.first.second_.where("'smelly' = ANY (tokens)").first
# The next one will create one with an empty array for tokens and push it into Term.searches anyway. Same thing with 'smelly'
Term.first.searches.where("':smelly' = ANY (tokens)").first_or_create do |s| Term.first.searches << s
end
# These error
Search.where(tokens: "Aortic").first
Search.where(tokens: [:Aortic, :Any, :Other, :Elements]).first
Also, if you have nested arrays, you could do a where search with this: '{{1,2,3},{4,5,6},{7,8,9}}' to find an row with column value [[1,2,3],[4,5,6],[7,8,9]]

Rails 3.0.9 : ActiveRecord Uniqueness Constraint failing on every updated, doesn't matter if the unique column isn't touched

I have a Profile model
class Profile < ActiveRecord::Base
attr_accessible :user_id, :race_id, :nickname, :first_name, :last_name, :gender, :birth_date, :eighteen,
:time_zone, :metric_scale, :referral_code, :referrer_id, :tag_line
# Relationships
belongs_to :user
belongs_to :race
belongs_to :referred_by, :class_name => "Profile", :foreign_key => "referral_code"
has_many :referrals, :class_name => "Profile", :foreign_key => "referrer_id"
# Validations
validates :user_id, :race_id, :nickname, :first_name, :last_name, :time_zone, :gender, :presence => true
validates :referral_code, :nickname, :uniqueness => { :case_sensitive => false }
# Instance Methods
def full_name
first_name + " " + last_name
end
# Class Methods
def self.search(search)
search_condition = "%" + search + "%"
find(:all, :conditions => ['nickname LIKE ?', search_condition])
end
def self.find_by_referral_code(referrer_code)
find(:one, :conditions => ['referral_code LIKE ?', referrer_code])
end
end
No matter which column I am updated the Uniqueness Constraint on 'referral_code' false and I cannot update the model and I can't figure out why. From what I read online as of Rails 3 ActiveRecord was supposed to be tracking dirty objects and only generating update queries containing the altered columns leaving all others alone. Because it should only be performing update queries on columns other than the Unique ones the validation should not be failing. Unfortunately it is. Here is Rails Console session displaying this:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > update = {"tag_line"=>"Changed to this"}
=> {"tag_line"=>"Changed to this"}
ruby-1.9.2-p180 :003 > profile.update_attributes(update)
=> false
ruby-1.9.2-p180 :004 > profile.errors
=> {:referral_code=>["has already been taken"]}
Even performing an update directly on a single column which is not unique causes the uniqueness constraint to fail and the record will not be updated, here is a console session:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > profile.tag_line = "changed to this"
=> "changed to this"
ruby-1.9.2-p180 :003 > profile.save
=> false
ruby-1.9.2-p180 :004 > profile.errors
=> {:referral_code=>["has already been taken"]}
I also ran a check to see if ActiveRecord was actually tracking the dirty object and it appears to be, here is the console session:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > profile.tag_line = "change to this"
=> "change to this"
ruby-1.9.2-p180 :003 > profile.changes
=> {"tag_line"=>["This is what its like.", "change to this"]}
ruby-1.9.2-p180 :004 > profile.save
=> false
ruby-1.9.2-p180 :005 > profile.errors
=> {:referral_code=>["has already been taken"]}
I honestly am at a loss, I have spent quite a bit of time digging into it as well as searching Google and I cannot find an answer as to why this is happening.
You are right, Rails does only "track" the dirty columns and generates the minimum update statement necessary. If you look in your log/development.log file you will see the actual SQL that is being generated, and you'll see that the update statement is only touching the columns you have edited. At least you would if your code was getting that far.
Before saving your model, Rails will run all the validations on it, and that includes seeing if the referral code is unique. To do this it will run a select SQL statement against the database to check; if you look in the development.log file you will definitely see this query.
So Rails is working correctly here.
If your referral codes are supposed to be unique, why aren't they? My guess would be that you are trying to save models with a nil or blank code. If that is the case, try adding :allow_nil => true or :allow_blank => true to the :uniqueness hash.

ActiveRecord: filtering children without hitting the database

I'm looking for a clean way to filter the children of a parent in a has_many relationship without hitting the database and thus reloading the db's view of the objects back into the app.
For example:
class Parent < ActiveRecord::Base
has_many :children, ...
...
end
My understanding (correct me if I'm wrong) is that
parent.children.find_all_by_attr('foo')
returns all the parent's children that have a attr value of 'foo' in the db but because it hits the database again, any children that have had their attr values set to foo before being saved will have their db values restored thus overwriting any changes.
I've hacked around this with
parent.children.reject { |child| child.attr != 'foo' }
but this seems very sloppy and more difficult to read. Does anybody have a cleaner suggestion on how to do this?
After doing some poking around, it looks like it is a little more complicated than that.
My poking around went like this:
I created a pair of models, Parent
and Child with a has_many
relationship in a throwaway rails
app.
I opened up script/console and
poked around.
I created a new parent with a child and saved them
>> p = Parent.new;p.children << Child.new(:foo=>'bar');p.save
=> true
See the child is in the db and findable by_foo
>> p.children.find_by_foo('bar')
=> #<Child id: 1, foo: "bar", parent_id: 1, created_at: "2009-12-14 22:08:05", updated_at: "2009-12-14 22:08:05">
I added another child to the collection,it shows up in p.children but not in collection methods that hit the db.
>> p.children << Child.new(:foo=>'bar')
=> [#<Child id: 1, foo: "bar", parent_id: 1, created_at: "2009-12-14 22:08:05", updated_at: "2009-12-14 22:08:05">, #<Child id: 2, foo: "bar", parent_id: 1, created_at: "2009-12-14 22:08:25", updated_at: "2009-12-14 22:08:25">]
>> p.children.find_by_foo('bar')
=> #<Child id: 1, foo: "bar", parent_id: 1, created_at: "2009-12-14 22:08:05", updated_at: "2009-12-14 22:08:05">
I change the child that is in the db.
>> p.children[0].foo = 'baz'
=> "baz"
When I search for it, it gives me the db version.
>> p.children.find_by_foo('bar')
=> #<Child id: 1, foo: "bar", parent_id: 1, created_at: "2009-12-14 22:08:05", updated_at: "2009-12-14 22:08:05">
But, the local collection is unchanged.
>> p.children
=> [#<Child id: 1, foo: "baz", parent_id: 1, created_at: "2009-12-14 22:08:05", updated_at: "2009-12-14 22:08:05">, #<Child id: 2, foo: "bar", parent_id: 1, created_at: "2009-12-14 22:08:25", updated_at: "2009-12-14 22:08:25">]
So if you save p again, it will pass on the changes.
If you want to get all the local association objects, including ones that have been changed, you can't use the ActiveRecord finders because they hit the db, instead use array methods like you did above. Though, using find_all or select would be easier to understand
parent.children.select{|c| c.attr == 'foo'}
You could do the opposite of reject, which is find_all:
parent.children.find_all {|child| child.attr == 'foo' }

Resources