How do I query a database with polymorphic association? - ruby-on-rails

Let's say, on a rails app (with postgresql), I have this database with a polymorphic association between work_steps and sub_assemblies / parts:
Part.rb
belongs_to :sub_assembly, optional: true
has_many :work_steps, as: :component
belongs_to :site
belongs_to :car
Sub_assembly.rb
has_many :work_steps, as: :component
has_many :parts
work_step.rb
belongs_to :component, polymorphic: true
Now, I want to query my database: I want a list of all the work_steps filtered with a specific array of sites and a specific array of cars.
For instance, all the work_steps linked to parts that belongs to Site A and Car X & Z.
Because of the polymorphic association with the sub_assemblies and this same table which is linked to parts table, I cannot figure out how to do it.
At the end, I need an ActiveRecord Relation. How would you do it simply? I tried a lot of things but without success.
Any one to help me?
Thank you in advance.

Okay, so work_step is what you want to be able to be used by multiple models.
So inside the migration for CreateWorkSteps:
t.references :component, polymorphic: true
This basically does
t.integer :component_id # Refers to id (of sub-assembly or part)
t.string :component_type # Refers to type of object (sub-assembly or part)
add_index :work_steps, [:component_type, component_id]
Now in your model:
work_step.rb
belongs_to :component, polymorphic: true
sub_assembly.rb
has_many :work_steps, as: :component
has_many :parts
part.rb
has_many :work_steps, as: :component
belongs_to :site
belongs_to :car
belongs_to :sub_assembly, optional: true
Okay, so you wish to query your database and obtain
work_steps
linked to parts
belongs to site A and Car X and Z
So, here the list of steps you should do in ActiveRecord (I'm not going to give a single-query answer as that's too much effort. Also, the step order is in logical order, but remember that ActiveRecord queries use lazy evaluation and SQL queries are evaluated in a certain order.)
#Join parts w/ site and cars
Use #Where siteName is in [A], carName is in [X, Z]
Now, you LEFT join with sub_assembly (So even if the part has no sub_assembly_id, it isn't removed).
Now this is the annoying step. For your first join, you'll want to join parts_id with component_id AND component_type == "Part" (Remember to prefix your tables when joining, e.g work_steps.component_id and do this from the beginning.)
For your second join, you'll want to join sub_assembly_id with component_id AND component_type == "Sub_assembly"
Now you have a HUGE ActiveRecord object, select only what makes a work_step.
Good luck! I won't hold your hand with a pre-written answer, through...because I'm too lazy to boot into Rails, make migrations, make models, seed the models, then write the query myself. ;)
Oh ya, and I ended up giving a single query answer anyway.

Related

Rails EXCEPT query (filter out records present in a joint table)

I'm trying to list the model instances that do not have the association with another model created yet.
Here is how my models are related:
Ticket.rb:
has_one :purchase
has_one :user, through: :purchase
User.rb:
has_many :purchases
has_many :tickets, through: :purchases
Purchase.rb:
belongs_to :ticket
belongs_to :user
I have an SQL query but have troubles when translating it to rails:
SELECT id FROM tickets
EXCEPT
SELECT ticket_id FROM purchases;
It works great as it returns all ids of the tickets that are not purchased yet.
I've tried this:
Ticket.joins('LEFT JOIN ON tickets.id = purchases.ticket_id').where(purchases: {ticket_id: nil})
but it seems not to be the right direction.
If you're just trying to get the list of Ticket records with no associated purchases, use .includes instead. In my experience a join will fail with no associated records, and this will keep you from needing to write any actual SQL.
Ticket.includes(:purchase).where(purchases: { ticket_id: nil} )
The generated SQL query is a bit more difficult to read as a human, but I've used it several times and not seen any real difference in performance.

Can a Rails Model have an array of associations?

I've looked everywhere, and can't find anything (maybe cause it's not possible)
I have a Meeting model and a Language model (which has a string column called language). Each Meeting has 2 Languages.
Is there a way to make an association, such as:
rails g migration AddLanguageToMeetings language:references
And then store an array of 2 language_ids in the reference?
For example, Meeting.language_id = [1,2]
And then be able to call the Language, like:
meeting.language_id[0].language
How can I se up this association? Do I need to have 2 different columns with each associated id?
Thanks!!
What you want is a N-to-N relation. Create another model called MeetingLanguage with two columns:
create_table :meeting_languages do |t|
t.references :meetings
t.references :languages
end
and associations:
class MeetingLanugage < ActiveRecord::Base
belongs_to :language
belongs_to :meeting
end
And then in Meeting module:
has_many :meeting_languages
has_many :languages, through: :meeting_languages, source: :language
Now you can have as many languages as you want for a single meeting.
What you're describing is a has_many, where the foreign key exists in the languages table, or in a joining table between the languages and meetings tables.
If you want each Meeting to point two exactly two Languages, then you can use two foreign keys in the meetings table, give each language a real name, and then have two belongs_to associations in your Meeting.

ActiveRecord custom has_one relations

I'm using Rails 5.0.0.1 ATM and i've come across issue with ActiveRecord relations when optimizing count of my DB requests.
Right now I have:
Model A (let's say 'Orders'), Model B ('OrderDispatches'), Model C ('Person') and Model D ('PersonVersion').
Table 'people' consists only of 'id' and 'hidden' flag, rest of the people data sits in 'person_versions' ('name', 'surname' and some things that can change over time, like scientific title).
Every Order has 'receiving_person_id' as for the person which recorded order in DB and every OrderDispatch has 'dispatching_person_id' for the person, which delivered order. Also Order and OrderDispatch have creation time.
One Order has many dispatches.
The straightforward relations thus is:
has_many :receiving_person, through: :person, foreign_key: "receiving_person_id", class_name: 'PersonVersion'
But when I list my order with according dispatches I have to deal with N+1 situation, because to find accurate (according to the creation date of Order/OrderDispatch) PersonVersion for every receiving_person_id and dispatching_person_id I'm making another requests.
SELECT *
FROM person_versions
WHERE effective_date_from <= ? AND person_id = ?
ORDER BY effective_date_from
LIMIT 1
First '?' is Order/OrderDispatch creation date and second '?' is receiving/ordering person id.
Using this query I'm getting accurate person data for the time of Order/OrderDispatch creation.
It's fairly easy to write query with subquery (or subqueries, as Order comes with OrderDispatches on one list) in raw SQL, but I have no idea how to do that using ActiveRecord.
I tried to write custom has_one relation as this is as far as I've come:
has_one :receiving_person. -> {
where("person_versions.id = (
SELECT id
FROM person_versions sub_pv1
WHERE sub_pv1.date_from <= orders.receive_date
AND sub_pv1.person_id = orders.receiving_person_id
LIMIT 1)")},
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version
It works if I use this only for receiving or dispatching person. When I try to eager_load this for joined orders and order_dispatches tables then one of 'person_versions' has to be aliased and in my custom where clause it isn't (no way to predict if it's gonna be aliased or not, it's used both ways).
Different aproach would be this:
has_one :receiving_person, -> {
where(:id => PersonVersion.where("
person_versions.date_from <= orders.receive_date
AND person_versions.person_id = orders.receiving_person_id").order(date_from: :desc).limit(1)},
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version
Raw 'person_versions' in where is OK, because it's in subquery and using symbol ':id' makes raw SQL get correct aliases for person_versions table joined to orders and order_dispatches, but I get 'IN' instead of 'eqauls' for person_versions.id xx subquery and MySQL can't do LIMIT in subqueries which are used with IN/ANY/ALL statements, so I just get random person_version.
So TL;DR I need to transform 'has_many through' to 'has_one' using custom 'where' clause which looks for newest record amongst those which date is lower than originating record creation.
EDIT: Another TL;DR for simplification
def receiving_person
receiving_person_id = self.receiving_person_id
receive_date = self.receive_date
PersonVersion.where(:person_id => receiving_person_id, :hidden => 0).where.has{date_from <= receive_date}.order(date_from: :desc, id: :desc).first
end
I need this method converted to 'has_one' relation so that i could 'eager_load' this.
I would change your schema as it's conflicting with your business domain, restructuring it would alleviate your n+1 problem
class Person < ActiveRecord::Base
has_many :versions, class_name: PersonVersion, dependent: :destroy
has_one :current_version, class_name: PersonVersion
end
class PersonVersion < ActiveRecord::Base
belongs_to :person, inverse_of: :versions,
default_scope ->{
order("person_versions.id desc")
}
end
class Order < ActiveRecord::Base
has_many :order_dispatches, dependent: :destroy
end
class OrderDispatch < ActiveRecord::Base
belongs_to :order
belongs_to :receiving_person_version, class_name: PersonVersion
has_one :receiving_person, through: :receiving_person_version
end

Nested relationships within Join Tables - Rails

I'm rather new to Rails and have a question about how to successfully add sub-categories to an existing Join Table relationship.
For example, assume I'm building a job board. Each job has 5 major filter categories ( Job_Type [General Management, Finance & Operations, etc.], Industry [ Technology, Healthcare, etc.], Region [Northwest, Southeast, etc.], and so on ). I want each of those to have subcategories as well ( ex. This job post has a Region > Southeast > South Carolina > Greenville ).
Setting up an initial Join Table association for the 5 major filter types made sense for future filtering and searchability.
EDIT
Here is my current Posting Model
class Posting < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
belongs_to :recruiter, class_name: "User"
belongs_to :company
has_many :interests
has_many :comments, as: :commentable
has_and_belongs_to_many :job_types
has_and_belongs_to_many :industries
has_and_belongs_to_many :regions
has_and_belongs_to_many :market_caps
has_and_belongs_to_many :ownerships
has_many :users, through: :interests
acts_as_followable
end
I'm currently using join tables instead of an array directly on the ActiveRecord for speed sake and for filtering/searching capabilities later on. It also allows me to use these join table in conjunction with a plethora of other necessary ActiveRecord associations.
Here is a snippet of what job_type looks like:
class JobType < ActiveRecord::Base
has_and_belongs_to_many :postings
has_and_belongs_to_many :news_items
has_and_belongs_to_many :companies
has_and_belongs_to_many :users
has_and_belongs_to_many :mkt_ints
end
This allows me access to a simple array of associated models, but I'm confused as to how to move past that to an array of arrays with potential further nested arrays. It feels clunky to add additional join tables for the first join tables. I'm sure there's a better solution and would love to get any insight you might have.
SECOND EDIT
Here's a representational picture of what I am trying to do if it helps.
Thanks!
SOLVED
The data for this table will, most likely, not be changing which eases the complexity and presented a more straightforward solution.
I created separate roles within a single table to limit queries and join tables. The resulting Region ActiveRecord looks like this:
class CreateRegions < ActiveRecord::Migration
def change
create_table :regions do |t|
t.string :role # role [ region, state, city ]
t.integer :parent # equal to an integer [ state.id = city.parent ] or null [ region has no parent ]
t.string :name # [ ex: Southeast, South Carolina, Charleston ]
t.timestamps null: false
end
end
end
This gives me all of the fields necessary to create relationships in a single table and easily sort it into nested checkboxes using the parent.id.
Thank you to everyone who viewed this question and to Val Asensio for your helpful comment and encouragement.

Rails 4 and referencing parent id in 'has_many' SQL

TL;DR: How do I use the ID of the respective parent object in a has_many SQL clause to find child objects?
Long version:
I have the following example code:
class Person < AR::Base
has_many :purchases, -> {
"SELECT * from purchases
WHERE purchase.seller_id = #{id}
OR purchase.buyer_id = #{id}"
}
This was migrated from Rails 3 which worked and looked like
has_many :purchases, :finder_sql => proc { #same SQL as above# }
I want to find all purchases associated with a Person object in one association, no matter whether the person was the one selling the object or buying it.
Update: I corrected the SQL, it was inside out. Sorry! Also: The association only needs to be read-only: I am never going to create records using this association, so using id twice should be OK. But I do want to be able to chain other scopes on it, e.g. #person.purchases.paid.last_5, so creating the sum of two associations in a method does not work (at least it didn't in Rails 3) since it doesn't return an AM::Relation but a simple Array.
When using this above definition in Rails 4.2, I get
> Person.first.purchases
undefined method `id' for #<Person::ActiveRecord_Relation:0x...>
The error is clear, but then how do I solve the problem?
Since this is only an example for much more complicated SQL code being used to express has_many relationships (e.g. multiple JOINS with subselects for performance), the question is:
How do I use the ID of the parent object in a has_many SQL clause?
I don't think your code will work at all. You are defining an association with two foreign keys ... that'd mean that in case you want to create a new Person from a present Purchase, what foreign key is to be used, seller_id or buyer_id? That just don't make sense.
In any case, the error you are getting is clear: you are calling a variable id which is not initialized in the block context of the SQL code.
A better approach to the problem I understand from your question would be to use associations in the following way, and then define a method that gives you all the persons, both buyers and sellers that a product has. Something like this:
class Purchase < ActiveRecord::Base
belongs_to :buyer, class_name: 'Person'
belongs_to :seller, class_name: 'Person'
def persons
ids = (buyer_ids + seller_ids).uniq
Person.where(ids: id)
end
end
class Person < ActiveRecord::Base
has_many :sold_purchases, class_name: 'Purchase', foreign_key: 'buyer_id'
has_many :buyed_purchases, class_name: 'Purchase', foreign_key: 'seller_id'
end
Im my approach, buyer_id and seller_id are purchase's attributes, not person's.
I may have not understood correctly, in that case please clarify.

Resources