Include associated many_to_many records and access intermediate table attribute

I’m trying to return the attribute from an intermediate table with related record in an index request, based on a different SO response I have:

class Bus < ApplicationRecord
  has_many :buses_passengers, dependent: :destroy, inverse_of: :bus
  has_many :passengers, through: :buses_passengers, inverse_of: :buses
  has_many :passengers_with_ticket_type, -> { select('passengers.*, buses_passengers.ticket_type') },
           class_name: 'Passenger',
           through: :buses_passengers,
           source: :passenger

end

class BusesPassenger < ApplicationRecord
  belongs_to :bus, class_name: 'Bus', foreign_key: 'bus_id', inverse_of: :buses_passengers
  belongs_to :passenger

  enum ticket_type: { type_a: 0, type_b: 1, type_c: 2 }
end


class Passenger < ApplicationRecord
  has_many :buses_passengers, dependent: :destroy

  has_many :buses, through: :buses_passengers, class_name: 'Bus',
  foreign_key: 'bus_id',
  inverse_of: :passengers
end

In the controller I do

@buses = Bus.includes(:passengers_with_ticket_type).some_scope

And in the view I want to iterate on every bus to also include all its passengers and also return inside the passenger object, which ticket_type they have. Something like this but in a view so of course different:

@buses.each do |bus|
  puts bus.attribute_1
  ...

  bus.passengers_with_ticket_type do |passenger|
    puts passenger.attribute_1
    ...
    puts passenger.ticket_type
  end
end

This fails though, with

ActiveRecord::StatementInvalid - PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "buses_passengers"
LINE 1: SELECT passengers.*, buses_passengers.ticket_type FROM "users...

and I cannot find a way to add the from clause to the includes. This does work for individual entries though, so

Bus.last.passengers_with_ticket_type.last.ticket_type

works like a charm, but when trying to use includes it just breaks.

Any idea how to fix this? Or some other approach to this?

TIA

Let’s start by fixing the naming issues and removing all the cruft from the models:

class Bus < ApplicationRecord
  has_many :bus_passengers, dependent: :destroy
  has_many :passengers, through: :bus_passengers
end

class BusPassenger < ApplicationRecord
  belongs_to :bus
  belongs_to :passenger
  enum ticket_type: { type_a: 0, type_b: 1, type_c: 2 }
end

class Passenger < ApplicationRecord
  has_many :bus_passengers, dependent: :destroy
  has_many :buses, through: :bus_passengers
end
class RenameBusesPassengers < ActiveRecord::Migration[7.0]
  def change
    rename_table :buses_passengers, :bus_passengers
  end
end

When naming models you should use SingularSingular and not PluralSingular. That’s because when ActiveRecord derives a class name from a table name plural segments will be treated as module nesting and you will get a constant missing error when it looks for Buses::Passenger. That way it just works even without extra configuration.

Of course using a name that actually fits the domain like Ticket would be superior to the fallback of just moshing the names of the two things it joins together.

You do not need to set inverses, foreign keys etc. when they can be derived from the name of the association or class name. Having less noise lets you focus on the things that should actually stick out when reading the code.

The reason Postgres can’t find the table is because .includes tries to be smart and if you don’t reference the joined tables it will use .preload to load the records in two queries:

# This fires SELECT * FROM buses ...
buses = Bus.includes(bus_passengers: :passenger)

buses.each do |bus|
  # the additional query is fired here on the first iteration
  bus.buses_passengers do |buses_passenger|
    # ...
  end
end

You can think of it like lazy loading an association. If you want to do it in a single query upfront use .eager_load instead.

However even if you didn’t get the missing table error Postgres would not let you do select('passengers.*, buses_passengers.ticket_type') as it’s ambigous. When you select columns of a joined table they must be aggregates or included in the GROUP BY clause of the query.

But you don’t even need this in the first place. Just let includes / eager_load get all the columns and optimize it when it actually becomes an issue.

See:

  • Convention over Configuration in Active Record
  • Preload, Eagerload, Includes and Joins

1

This is what I thought of after taking a step back and rethinking it.

  1. Remove the extra has_many, I will not be trying to use it anymore
class Bus < ApplicationRecord
  has_many :buses_passengers, dependent: :destroy, inverse_of: :bus
  has_many :passengers, through: :buses_passengers, inverse_of: :buses
end

class BusesPassenger < ApplicationRecord
  belongs_to :bus, class_name: 'Bus', foreign_key: 'bus_id', inverse_of: :buses_passengers
  belongs_to :passenger

  enum ticket_type: { type_a: 0, type_b: 1, type_c: 2 }
end


class Passenger < ApplicationRecord
  has_many :buses_passengers, dependent: :destroy

  has_many :buses, through: :buses_passengers, class_name: 'Bus',
  foreign_key: 'bus_id',
  inverse_of: :passengers
end
  1. In the controller, include the intermediate table and then the associated record.
@buses = Bus.includes(buses_passengers: [:passenger]).some_scope
  1. Iterate over the intermediate table and just get the other record while doing so.
@buses.each do |bus|
  puts bus.attribute_1
  ...

  bus.buses_passengers do |buses_passenger|
    passenger = buses_passenger.passenger
    puts passenger.attribute_1
    ...
    puts passenger.attribute_n
    puts buses_passenger.ticket_type

  end
end

This seems to work, I’ll mark it as correct in a couple of days unless I get a different response or find that it doesn’t work.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật