Rails has_one association with multiple primary keys - ruby-on-rails

Ruby 2.6.5 on Rails 5.2.4.1
Current Situation
I've got a table of Products, and I'd like to associate products as compatible with one another. For example, Product 1 is compatible with Product 2.
When creating the first ProductCompatibility, I populate it as follows:
#<ProductLibrary::ProductCompatibility id: 1, product_id: 1, compatible_product_id: 2>
At the moment, I can perform the following:
0> ProductLibrary::Product.find(1).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 2>]
But I would also like to be able to perform the following, without creating an additional record:
0> ProductLibrary::Product.find(2).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 1>]
Currently, the above returns the following:
0> ProductLibrary::Product.find(2).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 2>]
Current Code
My models look like this:
module ProductLibrary
class Product < ApplicationRecord
has_many :product_compatibilities, ->(p) {
unscope(where: :product_id)
.where(product_id: p.id)
.or(ProductLibrary::ProductCompatibility.where(compatible_product_id: p.id))
}
has_many :compatible_products, through: :product_compatibilities
end
end
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
has_one :compatible_product,
primary_key: :compatible_product_id,
foreign_key: :id,
class_name: 'ProductLibrary::Product'
end
end
Intention
The primary_key in compatible_product is why I'm getting Product 2 when I request Product 2's compatible products (instead of Product 1).
What I'd like is for the has_one :compatible_product association to return products where the primary key is both :compatible_product_id and :product_id, but I can't figure out how to do that without writing multiple associations and compiling them in a helper method (which feels clunky and unconventional).
I'm not even sure it's possible, but it seems like it's along the lines of a
ProductLibrary::Product.where(id: [:product_id, :compatible_product_id])
which I couldn't get to work as association logic.

You should be using belongs_to instead of has_one.
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
belongs_to :compatible_product,
class_name: 'ProductLibrary::Product'
end
end
The semantics of has_one and belongs_to are a really common source of confusion but the difference is with belongs_to the foreign key column is on this models table and with has_one the FKC is on the other model.
What you are creating here is really just a join table and join model with the slight difference that both foreign keys happen to point to the same table instead of two different tables.

Here's what I ended up with, thanks to some help from #max
module ProductLibrary
class Product < ApplicationRecord
has_many :product_compatibilities, ->(p) {
unscope(where: :product_id)
.where(product_id: p.id)
.or(ProductLibrary::ProductCompatibility.where(compatible_product_id: p.id))
}
has_many :compatible_products, through: :product_compatibilities
has_many :inverse_compatible_products, through: :product_compatibilities
def all_compatible
(self.compatible_products + self.inverse_compatible_products).uniq.sort - [self]
end
end
end
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
belongs_to :compatible_product,
class_name: 'ProductLibrary::Product'
belongs_to :inverse_compatible_product,
foreign_key: :product_id,
class_name: 'ProductLibrary::Product'
end
end
I'll probably rename some things, and we may need to implement a boolean to drive whether a product can be compatible with itself (for now I assume not).
It's kind of what I was trying to avoid, but it looks like this is a correct solution.

Related

How do I write a Rails finder method through a chain of belongs_to associations?

I'm using Rails 5.1. How do I write a finder method when there is a chain of "belongs_to" associations? I have the following models ...
class Plan < ApplicationRecord
...
has_many :plan_items, :dependent => :destroy
class PlanItem < ApplicationRecord
...
belongs_to :offer, :optional => false
class Offer < ApplicationRecord
belongs_to :package, :optional => false
class Package < ApplicationRecord
has_and_belongs_to_many :items
I want to write a finder that gets all Plans with an Item with id = "blah". But the below is failing ...
[19] pry(main)> Plan.joins(plan_items: :offer).joins(packages: :item).where(:item => {:id => "abac"}).count
ActiveRecord::ConfigurationError: Can't join 'Plan' to association named 'packages'; perhaps you misspelled it?
from /Users/davea/.rvm/gems/ruby-2.5.1/gems/activerecord-5.2.2.1/lib/active_record/associations/join_dependency.rb:188:in `find_reflection'
How do I write a finder when there is a chain of belongs_to associations?
First, maybe your table name is wrong. Second, to pass method between belong_to association, you can use delegate
I'm assuming PlanItem is a join table between Plan and Item (that would be inline with the Rails naming convention). This could be done neatly with through associations and scopes. I would do it like this...
class Plan < ApplicationRecord
has_many :plan_items, dependent: :destroy
has_many :items, through: :plan_items
scope :blah_items { items.id_of_blah }
class PlanItem < ApplicationRecord
belongs_to :offer, optional: false
belongs_to :item
class Item < ApplicationRecord
scope :id_of_blah { where(id: 'blah') }
Then you can call it like so... Plan.with_blah_items or if you had an active record collection of plans you could use the scope to narrow it down plans.with_blah_items.
Since ActiveRecord associations will return ActiveRecord relations, you can chain them with any other active record methods (e.g. Plan.first.items.where(item: { id: 'blah' }) Scopes just make it nice and neat. : )
If PlanItem is not a join table between Plan and Item, first thing you should do is rename it. This is not just a best practice, rails spends a lot of time assuming what things are named, and it could cause a bug. After you rename it you should create a join table between Plan and Item called PlanItem. If a join between these tables doesn't make sense with your application architecture, you could always string through associations together, but that would be a code smell.
If you didn't want to mess with scopes, you could always just do a query like this plan.items.where(items: { id: 'blah' }).
Hope that helps!

How to Query Multiple Associated Tables in Rails

I have two associated table in my application and I cannot figure it how to join them together in rails, below is my Model:
class Lead < ApplicationRecord
has_many :employee_leads
has_many :employees, :through => :employee_leads
end
class EmployeeLead < ApplicationRecord
belongs_to :employee
belongs_to :lead
end
class Employee < ApplicationRecord
has_many :employee_leads
has_many :leads, :through => employee_leads
has_many :emp_stores
has_many :stores, :through => emp_stores
end
class EmpStore < ApplicationRecord
belongs_to :store
belongs_to :employee
end
class Store < ApplicationRecord
has_many :emp_stores
has_many :employees, :through => :emp_stores
end
My application required me to find out which store each lead belongs to. I know how to join the lead to the employee, which is:
Lead.joins(employee_leads: :employee)
and I also know how to join the employee to the store
Employee.joins(emp_stores: :store)
Those are working for me without issue. When I try to get the lead join to store, I used:
Lead.joins(employee_leads: :employee { emp_stores: :store })
This gave me a syntax error, I refer to the link of Active Record regarding Joining Nested Associations (Multiple Level) and I still can't figure it out. I'm very new to this, please someone take some time to explain and help me out. Thank you.
Try this:
Lead.joins(employee_leeds: [employee: [amp_stores: :store] ])
I believe these variants should also work:
Lead.joins(employee_leeds: [{employee: [{amp_stores: :store}] }])
Lead.joins(employee_leeds: {employee: {amp_stores: :store} })
Although, depending on how you're using this you might want to consider using includes rather than joins. Joins are lazy-loaded whereas includes are eager-loaded.
As an explanation to why your syntax was wrong ... :something is a symbol. And in the old (pre ruby 1.9) world, hash key/value pairs were written like so: :key => :value. However, from Ruby 1.9 the newer syntax of key: :value was provided.
In your attempt to get this working you had a key/value pair followed (without a separator) by another hash. In order to fix it, you need to provide just one value to the first key:
# Old syntax
:employee_leads => [ :employee => [ :emp_stores => :store ] ]
Which when you consider the new syntax for hashes, hopefully makes this more logical to understand:
# New syntax
employee_leads: [ employee: [ emp_stores: :store ] ]

How to create an association that sets join table attributes automatically?

I am totally confused about how I should go about "the rails way" of effectively using my associations.
Here is an example model configuration from a Rails 4 app:
class Film < ActiveRecord::Base
# A movie, documentary, animated short, etc
has_many :roleships
has_many :participants, :through => :roleships
has_many :roles, :through => :roleships
# has_many :writers........ ?
end
class Participant < ActiveRecord::Base
# A human involved in making a movie
has_many :roleships
end
class Role < ActiveRecord::Base
# A person's role in a film. i.e. "Writer", "Actor", "Extra" etc
has_many :roleships
end
class Roleship < ActiveRecord::Base
# The join for connecting different people
# to the different roles they have had in
# different films
belongs_to :participant
belongs_to :film
belongs_to :role
end
Given the above model configuration, the code I wish I had would allow me to add writers directly to a film and in the end have the join setup correctly.
So for example, I'd love to be able to do something like this:
## The Code I WISH I Had
Film.create!(name: "Some film", writers: [Participant.first])
I'm not sure if I'm going about thinking about this totally wrong but it seems impossible. What is the right way to accomplish this? Nested resources? A custom setter + scope? Something else? Virtual attributes? thank you!
I created a sample app based on your question.
https://github.com/szines/hodor_filmdb
I think useful to setup in Participant and in Role model a through association as well, but without this will work. It depends how would you like to use later this database. Without through this query wouldn't work: Participant.find(1).films
class Participant < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
class Role < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
Don't forget to give permit for extra fields (strong_parameters) in your films_controller.rb
def film_params
params.require(:film).permit(:title, :participant_ids, :role_ids)
end
What is strange, that if you create a new film with a participant and a role, two records will be created in the join table.
Update:
You can create a kind of virtual attribute in your model. For example:
def writers=(participant)
#writer_role = Role.find(1)
self.roles << #writer_role
self.participants << participant
end
and you can use: Film.create(title: 'The Movie', writers: [Participant.first])
If you had a normal has_and_belongs_to_many relationship i.e. beween a film and a participant, then you can create a film together with your examples.
As your joining model is more complex, you have to build the roleships separately:
writer= Roleship.create(
participant: Participant.find_by_name('Spielberg'),
role: Role.find_by_name('Director')
)
main_actor= Roleship.create(
participant: Participant.find_by_name('Willis'),
role: Role.find_by_name('Actor')
)
Film.create!(name: "Some film", roleships: [writer, main_actor])
for that, all attributes you use to build roleships and films must be mass assignable, so in a Rails 3.2 you would have to write:
class Roleship < ActiveRecord::Base
attr_accessible :participant, :role
...
end
class Film < ActiveRecord::Base
attr_accessible :name, :roleships
...
end
If you want to user roleship_ids, you have to write
class Film < ActiveRecord::Base
attr_accessible :name, :roleship_ids
...
end
Addendum:
Of cause you could write a setter method
class Film ...
def writers=(part_ids)
writer_role=Role.find_by_name('Writer')
# skiped code to delete existing writers
part_ids.each do |part_id|
self.roleships << Roleship.new(role: writer_role, participant_id: part_id)
end
end
end
but that makes your code depending on the data in your DB (contents of table roles) which is a bad idea.

Rails Active Record: modifying and saving the child of an object from the result of joins?

So, this:
p = Person
.joins('join organization o on o.id = organization_id')
.where('o.id' => 1)
.select('person.*')
.first!
p.name = 'hi!'
p.save!
works as expected, saving the person's name.
But, how would I do this:
p.organization.name = 'bye!'
p.save!
I can't figure out the right projection to get the organization fields to map (or if it's possible). I've tried '*' and 'organization.name as "person.organization.name"'.
In order for what you're doing to work, you have to set the autosave option to true in your belongs_to :organization association.
belongs_to :organization, autosave: true
or just call save on the organization
p.organization.name = 'Org Name'
p.organization.save
You have to declare association in your Person class, using
belongs_to
has_one
has_many
has_many :through
has_one :through
or has_and_belongs_to_many,
and Rails will do the join by it self and link your both class together.
Let me paste here a section of Rails guide:
With Active Record associations, we can [..] tell Rails that there is a connection between the two models. Here’s the revised code for setting up customers and orders:
class Customer < ActiveRecord::Base
has_many :orders, :dependent => :destroy
end
class Order < ActiveRecord::Base
belongs_to :customer
end
With this change, creating a new order for a particular customer is easier:
#order = #customer.orders.create(:order_date => Time.now)
I suggest you read the complete guide here: http://guides.rubyonrails.org/association_basics.html

Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations

I have 3 models: Question, Option, Rule
Question has_many options;
Option needs a foreign key for question_id
Rule table consists of 3 foreign_keys:
2 columns/references to question_ids -> foreign keys named as 'assumption_question_id' and 'consequent_question_id'
1 column/reference to option_id -> foreign key named as option_id or condition_id
Associations for Rule:
Question has_many rules; and
Option has_one rule
I want to understand how to write up migrations for this, and how that associates to the 'has_many'/'belongs_to' statements I write up in my model, and the ':foreign_key' option I can include in my model.
I had this for my Option migration, but I'm not sure how the "add_index" statement works in terms of foreign keys, and how I can use it for my Rule migration: (my Question and Options models have appropriate has_many and belongs_to statements - and work fine)
class CreateOptions < ActiveRecord::Migration
def change
create_table :options do |t|
t.integer :question_id
t.string :name
t.integer :order
t.timestamps
end
add_index :options, :question_id
end
end
Thank you for the help!
Note: I have found this way to solve the problem.Kindness from China.
If you have RailsAdmin with you,you may notice that you can see all rules of one question as long as one field of both question fields(assumption_question_id,consequent_question_id) equals to id of the question.
I have done detailed test on this and found out that Rails always generates a condition "question_id = [current_id]" which make to_sql outputs
SELECT `rules`.* FROM `rules` WHERE `rules`.`question_id` = 170
And the reason that the following model
class Question < ActiveRecord::Base
has_many :options
# Notice ↓
has_many :rules, ->(question) { where("assumption_question_id = ? OR consequent_question_id = ?", question.id, question.id) }, class_name: 'Rule'
# Notice ↑
end
makes Question.take.rules.to_sql be like this
SELECT `rules`.* FROM `rules` WHERE `rules`.`question_id` = 170 AND (assumption_question_id = 170 OR consequent_question_id = 170)
Is that we have not yet get ride of the annoy question_id so no matter how we describe or condition properly, our condition follows that "AND".
Then,we need to get ride of it.How?
Click here and you will know how,Find sector 8.1,and you can see
Article.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "articles".* FROM "articles" WHERE trashed = 0
Then lets do it:
class Question < ActiveRecord::Base
has_many :options
# Notice ↓
has_many :rules, ->(question) { unscope(where: :question_id).where("assumption_question_id = ? OR consequent_question_id = ?", question.id, question.id) }, class_name: 'Rule'
# Notice ↑
end
class Rule < ActiveRecord::Base
belongs_to :option
belongs_to :assumption_question, class_name: "Question", foreign_key: :assumption_question_id, inverse_of: :assumption_rules
belongs_to :consequent_question, class_name: "Question", foreign_key: :consequent_question_id, inverse_of: :consequent_rules
end
class Option < ActiveRecord::Base
belongs_to :question
has_one :rule
end
All done.
Finally
This is my first answer here at stackoverflow,and this method is never found anywhere else.
Thanks for reading.
add_index adds an index to column specified, nothing more.
Rails does not provide native support in migrations for managing foreign keys. Such functionality is included in gems like foreigner. Read the documentation that gem to learn how it's used.
As for the associations, just add the columns you mentioned in your Question to each table (the migration you provided looks fine; maybe it's missing a :rule_id?)
Then specify the associations in your models. To get you started
class Question < ActiveRecord::Base
has_many :options
has_many :assumption_rules, class_name: "Rule"
has_many :consequent_rules, class_name: "Rule"
end
class Rule < ActiveRecord::Base
belongs_to :option
belongs_to :assumption_question, class_name: "Question", foreign_key: :assumption_question_id, inverse_of: :assumption_rules
belongs_to :consequent_question, class_name: "Question", foreign_key: :consequent_question_id, inverse_of: :consequent_rules
end
class Option < ActiveRecord::Base
belongs_to :question
has_one :rule
end
Note This is just a (untested) start; options may be missing.
I strongly recommend you read
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
http://guides.rubyonrails.org/association_basics.html
Edit: To answer the question in your comment
class Option < ActiveRecord::Base
belongs_to :question
# ...
The belongs_to tells rails that the question_id column in your options table stores an id value for a record in your questions table. Rails guesses the name of the column is question_id based on the :question symbol. You could instruct rails to look at a different column in the options table by specifying an option like foreign_key: :question_reference_identifier if that was the name of the column. (Note your Rule class in my code above uses the foreign_key option in this way).
Your migrations are nothing more than instructions which Rails will read and perform commands on your database based from. Your models' associations (has_many, belongs_to, etc...) inform Rails as to how you would like Active Record to work with your data, providing you with a clear and simple way to interact with your data. Models and migrations never interact with one another; they both independently interact with your database.
You can set a foreign key in your model like this:
class Leaf < ActiveRecord::Base
belongs_to :tree, :foreign_key => "leaf_code"
end
You do not need to specify this in a migration, rails will pull the foreign key from the model class definition.

Resources