简体   繁体   中英

Rails query using more then one has_many through associations

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM