Two-to-one mappings

Hey all,

I have a pretty simple question, but I'm not sure of a good solution to it. Essentially, I want to provide a two-to-one mapping of models. I'm working on an application for a contest, where every (unordered) combination of two Rounds is supposed to be assigned to one Room. Any Room might have many different combinations of Rounds, however.

What is the Right Way of doing this in Rails? Maybe create a model that holds the associated foreign keys? Also, what would be a good way to scale this out, if I wanted to be able to map unordered n-element collections of Rounds to obtain a Room?

Thanks,

Greg

Imagining that every round must belong to a room, here's a simple and straightforward implementation:

class Room < ActiveRecord::Base   # this class needs a max_rounds and rounds_count integer columns   has_many :rounds

  def accepts_more_rounds?     max_rounds < rounds_count   end

end

class Round < ActiveRecord::Base   # this class needs a room_id integer column   belongs_to :room, :counter_cache => true

  validate_on_create :validate_rounds_count

  protected

  def validate_rounds_count     self.room.reload     unless self.room.accepts_more_rounds?       errors.add( :room, "has already met it's rounds limit" )     end   end

end

room = Room.create( :max_rounds => 2 ) round_1 = room.rounds.create( :name => 'round 1' ) round_2 = room.rounds.create( :name => 'round 2' ) round_3 = room.rounds.create( :name => 'round 3' ) # this one isn't going to be created round_3.new_record? == true

That's a clean solution, however I don't know if it satisfies the fact that "any Room might have many different combinations of Rounds"

Not sure I understand correctly, but if a room can have many combination of rounds, and each combination of rounds has more than one round, you could try this:

Room and Round models I assume you already have. A room can have many round_combinations (create the RoundCombination model with a room_id. Room :has_many :room_combinations, and RoomCombination :belongs_to :room). Then create a join table between round_combinations and rounds (HABTM).

You can use callbacks or validation to limit the relationship to two rounds maximum.

Cool! Harold, your solution strikes me as being exactly the way to do it. I've implemented it, and things seem to be sailing smoothly. Thanks a lot to both of you.

Sincerely,

Greg

Great! Glad it worked.

Ah, so one follow up question: each Competitor in the competition is signed up for two rounds, and needs to know which room he or she is assigned. This is a has_one relationship; however, I'm not quite sure of the right way to map it. Thoughts?

Greg

Does a user have many round_combinations? and a round_combination has many users? Seems like it. If so, first thought is to create another HABTM table between round_combinations and users. Then @user.round_combinations.each { |rc| rc.room } gives you the rooms...

Actually, by user I mean competitor, but you get the point…

Ah, sorry, my specification was not very good. The Big Picture here is that we assign each Competitor to a room based on the particular combination of Rounds he or she is signed up for. So for example, we could have

r = RoundCombination.new r.room = room_1 r.rounds = [round_1, round_2] r.save

Now if c is a Competitor who is signed up for round_1 and round_2, I would like to be able to do

c.room #=> room_1

So essentially, a HABTM table does not seem to be the right way of capturing this relationship. A Competitor "knows" its RoundCombination because the Competitor is signed up for a particular combination of Rounds. I've managed a provisional, rather inefficient implementation, of c.room as

class Competitor < ActiveRecord::Base   has_many :rounds

  def room     RoundCombination.all.each do |rc|       return rc.room if rc.rounds.sort_by {|r| r.id} == rounds.sort_by {|r| r.id}     end     nil   end end

I feel this should be done with some SQL joins or something. Does Rails provide a relationship to manage this?

Thanks again,

Greg

From what I understand, one competitor will have as many rooms as RoundCombinations. Your room definition below would return the first one it finds.

I think you can do something like:

@competitor.rounds.each { |round| round.round_combination.room }

Would return an array of rooms for each of @competitor’s rounds.

That sound right?

Not quite, unfortunately. The point here (that is making things complex), is that

round.round_combination

has no semantic. Rather, the semantic is more like

[round_1, round_2].round_combination

That is, an (unordered) set of two rounds determines the round_combination.

On the flip side, a Competitor has one Room. As motivation, what's going on here is that Competitors are competing in a two-round math competition. They each sign up for two rounds ahead of time, and we put them in a room determined by which two rounds they signed up for. (Hence, a Competitor also has one RoundCombination; it's just not clear to me how the join query should work... I'm starting to think maybe I should just write that by hand.) In any case, the code I provided has the right functionality, but it's bad in the sense that it loads all of the RoundCombinations as AR objects each time its called, which is a lot of overhead...

Greg