Rails query using more then one has_many through associations - ruby-on-rails

Hi I have 6 models with relations with each other:
Spell model which can have many tags and elements and one ring(aka level)
class Spell < ActiveRecord::Base
belongs_to :spell_ring
has_many :element_of_spells, dependent: :destroy
has_many :spell_elements, through: :element_of_spells
has_many :tag_of_spells, dependent: :destroy
has_many :spell_tags, through: :tag_of_spells
validates_presence_of :name
end
Spell element which can have many spells
class SpellElement < ActiveRecord::Base
has_many :element_of_spells, dependent: :destroy
has_many :spells, through: :element_of_spells
end
Spell tag, which can have many spells:
class SpellTag < ActiveRecord::Base
has_many :tag_of_spells, dependent: :destroy
has_many :spells, through: :tag_of_spells
end
Spell ring:
class SpellRing < ActiveRecord::Base
has_many :spells
end
And join models:
class ElementOfSpell < ActiveRecord::Base
belongs_to :spell
belongs_to :spell_element
end
class TagOfSpell < ActiveRecord::Base
belongs_to :spell
belongs_to :spell_tag
end
Ok now I want to put them to good use :)
What I know:
That if I take any spell_tag or spell_element or spell_ring object, I can get all associated spells.
element = SpellElement.first
spells_of_element = element.spells >> give me all associated spells
I know I can scope this with spell_ring_id since it is part of the spell object.
spell_ring = SpellRing.first
spells_of_element_and_ring = spells_of_element.where( spell_ring_id: spell_ring.id ) >> returns spells of given element and ring
What I don`t know:
How to scope spells_of_element_and_ring with given tag.
tag = SpellTag.first
spells_of_element_ring_and_tag = ??
Updated
What I want?
Is to be able to query spells:
by the spell_tags
by the spell_rings
by the spell_elements
and any combination of those three models.

It's a good idea, when posting to StackOverflow, to weed out as much code as possible, and really boil your question down to the simplest possible example. This will get quicker answers, and also be useful to more people.
Let's start out with the simpler example of a school, which has many classrooms, and each classroom has many students.
Let's create our models:
rails generate model school name:string
rails generate model classroom school_id:integer grade:integer
rails generate model student name:string classroom_id:integer
Now let's create our associations:
class School < ActiveRecord::Base
has_many :classrooms
has_many :students, through: :classrooms
end
class Classroom < ActiveRecord::Base
belongs_to :school
has_many :students
end
class Student < ActiveRecord::Base
belongs_to :classroom
end
Finally, we'll create three quick records:
school = School.create name: 'City Elementary'
classroom = school.classrooms.create grade: 4
student = classroom.students.create name: 'Bob'
Now we can get a list of all students at the school like so:
school.students
This works because a school has_many students, through classrooms.
I think what you actually want is a little more complicated - a spell can have many elements, and an element can belong to many spells. In this case, you need a "join table". Let's simplify your example by eliminating everything except spells and elements.
We start by creating our models:
rails generate model spell name:string
rails generate model element name:string
Now we create a join table, which keeps track of which spells and elements belong to each other:
rails generate migration create_elements_spells element_id:integer spell_id:integer
Now we define our associations (relationships):
class Element < ActiveRecord::Base
has_and_belongs_to_many :spells
end
class Spell < ActiveRecord::Base
has_and_belongs_to_many :elements
end
has_and_belongs_to_many automatically looks for a table with the combined name of the two models, in plural form, in alphabetical order. Now we can do things like:
spell = Spell.create name: 'set on fire'
flint = Element.create name: 'flint'
steel = Element.create name: 'steel'
spell.elements << flint
spell.elements << steel
Now, spell.elements lists both flint and steel. flint.spells will list our 'set on fire' spell. steel.spells will also list our spell. You can expand from there.
But what if you need to know more than just what element - what if you need to know how much? now you have extra data that doesn't belong in the Spell record or the Element record. It belongs on the association itself. We might call an element/amount combo an "ingredient", and create a table for it like so:
rails generate model ingredient spell_id:integer element_id:integer amount:string
And we update our associations:
class Element < ActiveRecord::Base
has_many :ingredients
has_many :spells, through: :ingredients
end
class Spell < ActiveRecord::Base
has_many :ingredients
has_many :elements, through: :ingredients
end
class Ingredient < ActiveRecord::Base
belongs_to :element
belongs_to :spell
end
Now we can add ingredients to our spell:
spell.ingredients.create element: flint, amount: '1 gram'
spell.ingredients.create element: steel, amount: '1 piece'
So spell.ingredients will list both flint and steel, and the amounts for each. This should get you well on your way to building you application.

#Jaime explain querying through the whole process very good. But I wanted my query to be more flexible.
I don't know it this is the Rails way. But I found something like this to fit me the best.
Because SpellElement.spells, SpellRing.spells, SpellTag.spells all returns an array. My idea is to just compare them and return only matched elements as a result.
So
spell_element_ring_and_tag = some_spell_element.spells & some_spell_ring.spells & some_spell_tag.spells
Will return only spell objects shared by all three arrays.

Related

Rails ActiveRecord equivalent of Laravel ORM `attach()` method for polymorphic has_many :through

I'm transitioning from "Laravel ORM" to "Rails Active Record" and I couldn't find how do you do something like this:
$this->people()->attach($person['id'], ['role' => $role]);
Explanation for Laravel code snippet
People is a polymorphic association to the class that is being accessed via $this via the Role class. The function above, creates a record in the middle table (roles/peopleables) like this:
id: {{generically defined}}
people_id: $person['id']
role: $role
peopleable_type: $this->type
peopleable_id: $this->id
How the association is defined on the Laravel end:
class XYZ {
...
public function people()
{
return $this->morphToMany(People::class, 'peopleable')->withPivot('role','id');
}
...
}
My efforts in Ruby
Here is how I made the association in Ruby:
class Peopleable < ApplicationRecord
belongs_to :people
belongs_to :peopleable, polymorphic: true
end
class People < ApplicationRecord
has_many :peopleables
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
I have seen the operation << but I don't know if there is any way to set an additional value on the pivot table while triggering this operation. [in this case the roles or peopleables tables; I use these two terms interchangeably in this app.]
PS. So, basically the question is how to define additional values on the pivot table in a polymorphic-many association in ActiveRecord and dynamically set those values while initiating an attachment relationship
Description of Functionality
Our application has a limitless [generally speaking, not that there is no computational limits!] content type: post, novel, poem, etc.
Each of these content types can be associated to individuals who play certain roles: editor, author, translator, etc.
So, for example:
X is the translator of Post#1. X, Y and Z are authors of Post#1.
There is a distinct People model and each content type has its own unique model [for example: Post, Poem, etc].
The idea of :through is referring to the 'Role class' or 'the pivot table' [whichever way you want to understand it] that the polymorphic association is recorded on it.
In addition to the information regarding a simple polymorphic relationship, there is also the kind of role that is recorded on the pivot table.
For example, X is both the author and the translator for Post#1, so there are two rows with the same people_id, peopleable_type and peopleable_id, however they have different values for role.
From what I understand given your description, I think you have this models (I'll change the names to what I understand they are, hope it's clear enough):
class Person < ApplicationRecord # using singular for models
has_many :person_roles
end
class Poem < ApplicationRecord
has_many :person_roles, as: :content
end
class Novel < ApplicationRecord
has_many :person_roles, as: :content
end
etc...
class PersonRole < ApplicationRecord
belongs_to :person
belongs_to :content, polymorphic: true
# you should have a "role" column on your table
end
So a Person is associated to a "content" (Novel, Poem, etc) via the join model PersonRole with a specific role. A Person that is the author of some novel and the editor of some peom would have two PersonRole records.
So, if you have a person and you want to assign a new role on some content, you can just do:
person.person_roles.create(role: :author, content: some_poem)
or
PersonRole.create(person: person, role: :author, content: some_poem)
or
some_poem.person_roles.create(person: person, role: :author)
You have two things in play here: "belongs_to :content, polymorphic: true" is covers the part of this being a polymorphic association. Then you have the "PersonRole" table that covers the part you know as "pivot table" (join table/model on rails).
Note that :through in rails has other meaning, you may want to get all the poems that a user is an author of, you could then have a "has_many :poems, through: :person_roles" association (that won't actually work, it's more complex than that in this case because you have a polymorphic association, you'll need to configure the association with some extra options like source and scope for this to work, I'm just using it as an example of what we understand as a has many :through association).
Rails is 'convention over configuration'. Models' must be in singular 'Person'.
ActiveRecord has has_many ... through and polymorphic association
"Assignable" and "Assignments" are more natural to read than "peoplable"
class Person < ApplicationRecord
has_many :assignments, as: :assignable
has_many :roles, through: :assignments
end
class Role < ApplicationRecord
has_many :assignments
has_many :people, through: :assignments
end
class Assignment
belongs_to :role
belongs_to :assignable, polymorphic: true
end
You can read more Rails has_many :through Polymorphic Association by Sean C Davis

How to create a many to many relation in Rails

So currently i have in a app:
class Post < ApplicationRecord
belongs_to :category
end
and
class Category < ApplicationRecord
has_many :posts
end
which works fine as expected. However, i need to add multiple categories to a post . I thought about using has_many_and_belongs_to for each of them to acquire this but some trouble implementing this. It seems like a need to add an join table? If so, how would one look like with the setup shown above?
Any ideas?
I appreciate any input! Thanks in advance!
The table should be named categories_posts (categories comes first because of alphabetical sequence) and contain post_id and category_id integer columns (indexed, most probably). The it's as simple as:
class Post < ApplicationRecord
has_and_belongs_to_many :categories
end
class Category < ApplicationRecord
has_and_belongs_to_many :posts
end
You can create join table adding migration using:
rails g migration CreateJoinTableCategoryPost category post
Alternatively you can use has_many :through to have more control over a join table.
Advantages of using :through for many to many relationships
With has_many :through relationship you can have a model which will allow you to add validation, callbacks.
If you initially take some extra efforts to setup many to many relationship using through it can save a lot of time and headache in future
What if in future you want save the more information on join table like some custom sort, information about how the tables are associated which will not be allowed with has_and_belongs_to_many
Example
class Post < ApplicationRecord
has_many :categories, through: :post_categories
has_many :post_categories
end
class Category < ApplicationRecord
has_many :posts, through: :post_categories
has_many :post_categories
end
Adding relationship model with rails generator command
rails g model post_category category_id:integer post_id:integer custom:text
class PostCategory < ApplicationRecord
belongs_to :category
belongs_to :post
end

Rails has_many through association issue

I have 3 models:
sophead
od
od_item
sophead has many ods and
od has many od_items.
each od_item belongs to one od and each od belongs to one sophead
I want to be able to return all od_items for a specific sophead like this:
all_od_items_for_first_sophead = Sophead.first.od_items
what is the correct association for getting all the od_items for a sophead?
I had tried:
has_many :od_items, through: :ods
but I believe that this is incorrect as it doesn't really match this diagram - in that diagram's example (with different model names), the arrow from patients to appointments would run the other direction.
Thanks in advance
Your try was correct way to do it.
Your models must have associations like this:
models/sophead.rb
class Sophead < ApplicationRecord
has_many :ods
has_many :od_items, through: :ods
end
models/od.rb
class Od < ApplicationRecord
belongs_to :sophead
has_many :od_items
end
models/od_item.rb
class OdItem < ApplicationRecord
belongs_to :od
end

Belongs to many association?

I guess this is a pretty conceptual question. I'm looking through the possible associations available through Rails, but cannot seem to wrap my head around how to build a "belongs_to_many" and "has_many" association.
Specifically, I want readers to have many books, and each book to belong to many readers.
The closest I can find is the "has_many_and_belongs_to" association, but based on all of the examples I found, it is not exactly accurate.
Likewise, according to the documentation, the "belongs to" and "has many" association is meant as a one to many.
Is there an association available that matches a belongs to many style or some sort of model structure I could use?
Update: Jul 2022
has_and_belongs_to_many is not recommended anymore.
Please use has_many :through approach.
You need to use either
has_and_belongs_to_many
class Book < ActiveRecord::Base
has_and_belongs_to_many :readers
end
class Reader < ActiveRecord::Base
has_and_belongs_to_many :books
end
with this approach, you will need to create a join table named books_readers
rails g migration CreateJoinTableBooksReaders books readers
OR
has_many :through
class Book < ActiveRecord::Base
has_many :book_readers
has_many :readers, through: :book_readers
end
class Reader < ActiveRecord::Base
has_many :book_readers
has_many :books, through: :book_readers
end
class BookReader < ActiveRecord::Base
belongs_to :reader
belongs_to :book
end
with this approach, you will need to create a new model BookReader
rails g model BookReader book:references reader:references

How to use ActiveRecord to create a has_many relationship without a bridge table

Sorry, this one is hard to phrase in the title. So here's what I'm trying to do. A workshop has many districts. Each district has exactly one district_contact (actually a district_contact_id). How can I use ActiveRecord to model the relationship between workshop and district_contact? I want to be able to do this:
Workshop.district_contacts
And get a collection of the actual user objects. Right now, I've done it using a short function:
def district_contacts
district_ids = []
self.districts.each do |district|
if district.contact_id
district_ids << district.contact_id
end
end
User.find(district_ids)
end
Define associations in the Workshop model:
has_many :districts
has_many :district_contacts, through: disctricts
Your model associations should look something like this.
class Workshop < ActiveRecord::Base
has_many :districts
has_many :district_contacts, through: disctricts
end
class District < ActiveRecord::Base
belongs_to :workshop
has_one :district_contract
end

Resources