Active Record, Polymorphic Has Many Through with STI - activerecord

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">]>

Related

Rails Product model with a Recipe of other products, through join table

I have a model called Product, that represents items. What I now need is for Products to have a "Recipe" that represents a list of other products with a quantity. Basically, a Product will have many of self with X amount of it. I was thinking that creating a new table that has a recipe_id and a supply_id with an amount column would do. But I'm having trouble with the association syntax.
My new model would be:
create_table :recipe_supplies do |t|
t.integer :supply_id
t.integer :recipe_id
t.decimal :amount, default: 0
end
class RecipeSupplies < ActiveRecord::Base
belongs_to :recipe, class_name: 'Product'
belongs_to :supply, class_name: 'Product'
end
And so my Product model would have:
class Product < ActiveRecord::Base
has_many :recipe_supplies, class_name: 'RecipeSupply', foreign_key: 'recipe_id'
has_many :supplies, through: :recipe_supplies
end
What I need is to be able to do #product.recipe_supplies, to check supply(product) amount, but also have an easy way of getting the supplies, like #product.supplies and get all products marked as supplies. What I can't figure out is how to do a #product.recipe, to get the recipes where #product is a supply. Or maybe there is a better way of doing this, any help is appreciated. I have the feeling I'm not on the best/most efficient path here.
2.6.5 :006 > #product.recipe_supplies
RecipeSupply Exists (0.4ms) SELECT 1 AS one FROM "recipe_supplies" WHERE "recipe_supplies"."recipe_id" = $1 LIMIT $2 [["recipe_id", 109], ["LIMIT", 1]]
RecipeSupply Load (0.2ms) SELECT "recipe_supplies".* FROM "recipe_supplies" WHERE "recipe_supplies"."recipe_id" = $1 [["recipe_id", 109]]
[
[0] #<RecipeSupply> {
:id => 1,
:supply_id => 1,
:recipe_id => 109,
:amount => 10.5
},
[1] #<RecipeSupply> {
:id => 2,
:supply_id => 2,
:recipe_id => 109,
:amount => 22.3
}
]
2.6.5 :007 > #product.supplies
Product Exists (0.6ms) SELECT 1 AS one FROM "products" INNER JOIN "recipe_supplies" ON "products"."id" = "recipe_supplies"."supply_id" WHERE "recipe_supplies"."recipe_id" = $1 LIMIT $2 [["recipe_id", 109], ["LIMIT", 1]]
Product Load (0.3ms) SELECT "products".* FROM "products" INNER JOIN "recipe_supplies" ON "products"."id" = "recipe_supplies"."supply_id" WHERE "recipe_supplies"."recipe_id" = $1 [["recipe_id", 109]]
[
[0] #<Product> {
:id => 1,
...
},
[1] #<Product> {
:id => 2,
...
}
]

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 :)

ActiveSupport::Concern, has_secure password not update some columns

I have a Recoverable module for my Customer model. Customer model using has_secure method for authentication. Here is the Customer model:
class Customer < ActiveRecord::Base
include Recoverable
##
# Validations
validates :email, format: { with: REGEX_EMAIL }, allow_nil: false, allow_blank: false
validates_uniqueness_of :email
validates_presence_of :email
has_secure_password
validates :password, length: { minimum: 6 }, if: :password_digest_changed?
validates :password_confirmation, presence: true, if: :password_digest_changed?
end
And here is the Recoverable module:
# encoding: utf-8
module Recoverable
extend ActiveSupport::Concern
def reset_password!(new_password, new_password_confirmation)
self.password = new_password
self.password_confirmation = new_password_confirmation
if valid?
self.reset_password_token = nil
self.reset_password_sent_at = nil
end
save
end
end
My problem is after reset_password called reset_password_token, reset_password_sent_at are not null. It's not set to null. Update query is not set below columns. Why? Am I miss something? If you need more info let me know.
My environments: I'm using Rails 4 app.
UPDATE 1
When I puts self.inspect I get following outputs:
#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$U2knjpm5LF1V/sgXag0DcOpgZWHSpLw8nfCy4U8D57s6...", created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 10:04:45", reset_password_sent_at: nil, reset_password_token: nil>
UPDATE 2:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"PbUhgSPvQZWXflT5fA1WhqhHJX3c7NMapg6eeDQvpBI=", "token"=>"fiMXi2_4cYCHsFMop9TJBL2Qeqc41xWhHA", "q"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}}
Unpermitted parameters: utf8, _method, authenticity_token, q
Customer Load (0.4ms) SELECT "customers".* FROM "customers" WHERE "customers"."reset_password_token" IS NULL LIMIT 1
Unpermitted parameters: password_confirmation
Unpermitted parameters: password
Customer Exists (0.3ms) SELECT 1 AS one FROM "customers" WHERE ("customers"."email" = 'milk#yahoo.com' AND "customers"."id" != 79) LIMIT 1
----------------------------BEFORE:
#<ActiveModel::Errors:0xb593c280 #base=#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTK...", store_id: 124, created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 11:56:52", reset_password_sent_at: nil, reset_password_token: nil>, #messages={}>
(0.1ms) BEGIN
CACHE (0.0ms) SELECT 1 AS one FROM "customers" WHERE ("customers"."email" = 'milk#yahoo.com' AND "customers"."id" != 79) LIMIT 1
SQL (0.3ms) UPDATE "customers" SET "password_digest" = $1, "updated_at" = $2 WHERE "customers"."id" = 79 [["password_digest", "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTKUI001xu0O"], ["updated_at", Thu, 16 May 2013 19:58:25 ULAT +08:00]]
(16.5ms) COMMIT
---------------------------- SAVE:
true
----------------------------AFTER:
#<ActiveModel::Errors:0xb593c280 #base=#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTK...", store_id: 124, created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 11:58:25", reset_password_sent_at: nil, reset_password_token: nil>, #messages={}>
Ok so finally if your model is not valid after clearing variables you can do that:
save(validate: false)
It will skip validation and will allow you to save invalid model
Could you check if your model is really valid ?
I mean something like
if valid?
puts "valid"
self.reset_password_token = nil
self.reset_password_sent_at = nil
else
puts self.errors.inspect
end
Maybe you have some forgotten validation and you are not going to that block ?

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]]

Nested Attributes not updating

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".

Resources