sandre89
sandre89

Reputation: 5908

Rails `belongs_to through` via has_one trough where the join model has_many of the child models

Imagine a travel website where you have HotelOwners and Tourists. When they start a conversation, the app creates a join model named Conversation using has_many through. It's a classic many to many association:

class HotelOwner
  has_many :tourists, through: :conversations
  has_many :conversations
end

class Tourist
  has_many :hotel_owners, through: :conversations
  has_many :conversations
end

class Conversation
  belongs_to :hotel_owner
  belongs_to :tourist
end

Now we can use hotel_owner.tourists and tourist.hotel_owners. Also, the join model Conversation is also being used to keep some state on that association between them both (like, HotelOwner comments on Tourist and vice-versa).

But now we need a Reservation model. My initial ideia was this:

class Reservation
  belongs_to :hotel_owner
  belongs_to :tourist
end

But we also need to create the Conversation join model, since app logic requires that there cannot be a Reservation without a previous Conversation, even if a blank one. Also, the hotel_owner notes on tourist and vice-versa should be kept there and need to exist if a reservation exists.

After thinking about using manual callbacks to manually create the join model Conversation, I read that it would not be a good idea to add a belongs_to :conversation on Reservation because it could lead to database inconsistencies (like the problem if reservation.conversation.tourist pointed to a different tourist then reservation.tourist .. there should be a single source of truth to this association right?)

I then had the idea of using Conversation as a proxy to Reservations, like this:

class HotelOwner
  has_many :tourists, through: :conversations
  has_many :conversations
  has_many :reservations, through: :conversations
end

class Tourist
  has_many :hotel_owners, through: :conversations
  has_many :conversations
  has_many :reservations, through: :conversations
end

class Conversation
  belongs_to :hotel_owner
  belongs_to :tourist
  has_many   :reservations
end

class Reservation
  has_one :hotel_owner, through: :conversation
  has_one :tourist,     through: :conversation
  belongs_to :conversation
end

Since there is no belongs_to through in Rails to use in Reservation, other posts in SO suggest using has_one trough instead, just like I did above.

The problem is that conversation has_many reservations, and does not belong_to a reservation (like it does belong to a Tourist and HotelOwner).

It's not only semantics that bother me. If I do hotel_owner.reservations.create(tourist: Tourist.last), it does create the Reservation, but the join model Conversation is not created, leaving reservation.conversation nil.

After a simple hotel_owner.reload, hotel_owner.reservations return nil.

What is the correct database design and Rails association model for something like this?

Upvotes: 1

Views: 225

Answers (1)

sa77
sa77

Reputation: 3603

you can keep it simple like this:

class HotelOwner < ActiveRecord::Base
  has_many :reservations
  has_many :conversations
end

class Tourist < ActiveRecord::Base
  has_many :reservations
end

# hotel_owner_id, tourist_id
class Reservation < ActiveRecord::Base
  belongs_to :tourist
  belongs_to :hotel_owner

  has_one :conversation, dependent: :destroy
  after_create { self.conversation.create! }
end

# reservation_id
class Conversation < ActiveRecord::Base
  belongs_to :reservation
end

Then you can access these associations as follows:

# get all reservations and conversations for a hotel_owner
HotelOwner.last.reservations
HotelOwner.last.conversations

# all reservations made by a tourist
Tourist.last.reservations

# conversation associated to a reservation
Reservation.last.conversation

# get reservation, hotel_owner and tourist from a conversation
Conversation.last.reservation
Conversation.last.reservation.tourist
Conversation.last.reservation.hotel_owner 

Upvotes: 0

Related Questions