class CreateOrders < ActiveRecord::Migration[5.2] def change create_table :orders do |t| t.timestamps end end end class CreateItems < ActiveRecord::Migration[5.2] def change create_table :items do |t| t.timestamps end end end class CreateAssemblies < ActiveRecord::Migration[5.2] def change create_table :assemblies do |t| t.timestamps end end end class CreateOrderItems < ActiveRecord::Migration[5.2] def change create_table :order_items do |t| t.references :order, foreign_key: true t.references :item, foreign_key: true t.references :assembly, foreign_key: true t.timestamps end end end class CreateAssemblyItems < ActiveRecord::Migration[5.2] def change create_table :assembly_items do |t| t.references :assembly, foreign_key: true t.references :item, foreign_key: true t.timestamps end end end class Assembly < ApplicationRecord has_many :orders, through: :order_items has_many :assembly_items has_many :assemblies, through: :assembly_items has_many :items, through: :assembly_items end class AssemblyItem < ApplicationRecord belongs_to :assembly belongs_to :item end class Item < ApplicationRecord has_many :orders, through: :order_items has_many :assemblies, through: :assembly_items end class Order < ApplicationRecord has_many :order_items has_many :assemblies, through: :order_items has_many :items, through: :assemblies end class OrderItem < ApplicationRecord belongs_to :order belongs_to :item belongs_to :assembly end Running via Spring preloader in process 23837 Loading development environment (Rails 5.2.2) irb(main):001:0> o = Order.first Order Load (0.3ms) SELECT "orders".* FROM "orders" ORDER BY "orders"."id" ASC LIMIT $1 [["LIMIT", 1]] => # irb(main):002:0> a = Assembly.first Assembly Load (0.3ms) SELECT "assemblies".* FROM "assemblies" ORDER BY "assemblies"."id" ASC LIMIT $1 [["LIMIT", 1]] => # irb(main):003:0> i = Item.first Item Load (0.4ms) SELECT "items".* FROM "items" ORDER BY "items"."id" ASC LIMIT $1 [["LIMIT", 1]] => # irb(main):004:0> o.assemblies Assembly Load (0.7ms) SELECT "assemblies".* FROM "assemblies" INNER JOIN "order_items" ON "assemblies"."id" = "order_items"."assembly_id" WHERE "order_items"."order_id" = $1 LIMIT $2 [["order_id", 1], ["LIMIT", 11]] => #]> irb(main):005:0> o.items Item Load (1.4ms) SELECT "items".* FROM "items" INNER JOIN "assembly_items" ON "items"."id" = "assembly_items"."item_id" INNER JOIN "assemblies" ON "assembly_items"."assembly_id" = "assemblies"."id" INNER JOIN "order_items" ON "assemblies"."id" = "order_items"."assembly_id" WHERE "order_items"."order_id" = $1 LIMIT $2 [["order_id", 1], ["LIMIT", 11]] => #]> irb(main):006:0> a.orders Order Load (0.3ms) SELECT "orders".* FROM "orders" INNER JOIN "order_items" ON "orders"."id" = "order_items"."order_id" WHERE "order_items"."assembly_id" = $1 LIMIT $2 [["assembly_id", 1], ["LIMIT", 11]] => #]> irb(main):007:0> a.assemblies Assembly Load (0.3ms) SELECT "assemblies".* FROM "assemblies" INNER JOIN "assembly_items" ON "assemblies"."id" = "assembly_items"."assembly_id" WHERE "assembly_items"."assembly_id" = $1 LIMIT $2 [["assembly_id", 1], ["LIMIT", 11]] => #]> irb(main):008:0> a.items Item Load (0.4ms) SELECT "items".* FROM "items" INNER JOIN "assembly_items" ON "items"."id" = "assembly_items"."item_id" WHERE "assembly_items"."assembly_id" = $1 LIMIT $2 [["assembly_id", 1], ["LIMIT", 11]] => #]> irb(main):009:0> i.orders Order Load (0.4ms) SELECT "orders".* FROM "orders" INNER JOIN "order_items" ON "orders"."id" = "order_items"."order_id" WHERE "order_items"."item_id" = $1 LIMIT $2 [["item_id", 1], ["LIMIT", 11]] => #]> irb(main):010:0> i.assemblies Assembly Load (0.4ms) SELECT "assemblies".* FROM "assemblies" INNER JOIN "assembly_items" ON "assemblies"."id" = "assembly_items"."assembly_id" WHERE "assembly_items"."item_id" = $1 LIMIT $2 [["item_id", 1], ["LIMIT", 11]] => #]> irb(main):011:0> Notice that all of these relations (presumably) work and operate as expeted. However, when we try to use them in the way you described. irb(main):011:0> o.items Item Load (0.9ms) SELECT "items".* FROM "items" INNER JOIN "assembly_items" ON "items"."id" = "assembly_items"."item_id" INNER JOIN "assemblies" ON "assembly_items"."assembly_id" = "assemblies"."id" INNER JOIN "order_items" ON "assemblies"."id" = "order_items"."assembly_id" WHERE "order_items"."order_id" = $1 LIMIT $2 [["order_id", 1], ["LIMIT", 11]] => #]> irb(main):012:0> assemblies = o.assemblies Assembly Load (0.4ms) SELECT "assemblies".* FROM "assemblies" INNER JOIN "order_items" ON "assemblies"."id" = "order_items"."assembly_id" WHERE "order_items"."order_id" = $1 LIMIT $2 [["order_id", 1], ["LIMIT", 11]] => #]> irb(main):013:0> assemblies.items Traceback (most recent call last): 1: from (irb):13 Assembly Load (0.6ms) SELECT "assemblies".* FROM "assemblies" INNER JOIN "order_items" ON "assemblies"."id" = "order_items"."assembly_id" WHERE "order_items"."order_id" = $1 LIMIT $2 [["order_id", 1], ["LIMIT", 11]] NoMethodError (undefined method `items' for #) irb(main):014:0> We get that error because assemblies isn't actually a single thing, so we can't call items on it directly. irb(main):017:0> assemblies.map { |assembly| irb(main):018:1* assembly.items irb(main):019:1> } Item Load (0.5ms) SELECT "items".* FROM "items" INNER JOIN "assembly_items" ON "items"."id" = "assembly_items"."item_id" WHERE "assembly_items"."assembly_id" = $1 LIMIT $2 [["assembly_id", 1], ["LIMIT", 11]] => [#]>] irb(main):020:0> In that way, we can iterate over each of the assemblies and return a value. Much like your later comment suggest where it's assemblies.each. While my models do not have a name, we can pretend we wanted a specific data structure. irb(main):017:0> assemblies.map { |assembly| irb(main):018:1* assembly.items irb(main):019:1> } Item Load (0.5ms) SELECT "items".* FROM "items" INNER JOIN "assembly_items" ON "items"."id" = "assembly_items"."item_id" WHERE "assembly_items"."assembly_id" = $1 LIMIT $2 [["assembly_id", 1], ["LIMIT", 11]] => [#]>] irb(main):020:0> So, you could alternatively, define a method that does the above on order, however, while I may not have the exact API you desired, if I were to create another assembly, item, order_item, and assembly_item, I can achieve... irb(main):058:0> o.items Item Load (0.8ms) SELECT "items".* FROM "items" INNER JOIN "assembly_items" ON "items"."id" = "assembly_items"."item_id" INNER JOIN "assemblies" ON "assembly_items"."assembly_id" = "assemblies"."id" INNER JOIN "order_items" ON "assemblies"."id" = "order_items"."assembly_id" WHERE "order_items"."order_id" = $1 LIMIT $2 [["order_id", 1], ["LIMIT", 11]] => #, #]> irb(main):059:0> However, you can also achieve what you're after by using a scope. If you add the scope: scope :items, -> { AssemblyItem.where(assembly_id: self.ids).map { |ai| ai.item }} to your Assembly model, you should be able to do the following... irb(main):031:0> o = Order.first Order Load (0.3ms) SELECT "orders".* FROM "orders" ORDER BY "orders"."id" ASC LIMIT $1 [["LIMIT", 1]] => # irb(main):032:0> o.assemblies.items (0.3ms) SELECT "assemblies"."id" FROM "assemblies" INNER JOIN "order_items" ON "assemblies"."id" = "order_items"."assembly_id" WHERE "order_items"."order_id" = $1 [["order_id", 1]] AssemblyItem Load (0.3ms) SELECT "assembly_items".* FROM "assembly_items" WHERE "assembly_items"."assembly_id" IN ($1, $2) [["assembly_id", 1], ["assembly_id", 2]] Item Load (3.3ms) SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]] Item Load (0.3ms) SELECT "items".* FROM "items" WHERE "items"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]] => [#, #] irb(main):033:0>