Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Referring to instance in has_many (Rails)

I have a Game model which has_many :texts. The problem is that I have to order the texts differently depending on which game they belong to (yes, ugly, but it's legacy data). I created a Text.in_game_order_query(game) method, which returns the appropriate ordering.

My favourite solution would have been to place a default scope in the Text model, but that would require knowing which game they're part of. I also don't want to create separate classes for the texts for each game - there are many games, with more coming up, and all the newer ones will use the same ordering. So I had another idea: ordering texts in the has_many, when I do know which game they're part of:

has_many :texts, :order => Text.in_game_order_query(self)

However, self is the class here, so that doesn't work.

Is there really no other solution except calling @game.texts.in_game_order(@game) every single time??

like image 827
Sprachprofi Avatar asked Aug 14 '12 09:08

Sprachprofi


2 Answers

I had a very similar problem recently and I was convinced that it wasn't possible in Rails but that I learned something very interesting.

You can declare a parameter for a scope and then not pass it in and it will pass in the parent object by default!

So, you can just do:

class Game < ActiveRecord
  has_many :texts, -> (game) { Text.in_game_order_query(game) }

Believe or not, you don't have to pass in the game. Rails will do it magically for you. You can simply do:

game.texts

There is one caveat, though. This will not work presently in Rails if you have preloading enabled. If you do, you may get this warning:

DEPRECATION WARNING: The association scope 'texts' is instance dependent (the scope block takes an argument). Preloading happens before the individual instances are created. This means that there is no instance being passed to the association scope. This will most likely result in broken or incorrect behavior. Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.

like image 92
Mike Avatar answered Nov 09 '22 09:11

Mike


Following up using PradeepKumar's idea, I found the following solution to work

Assuming a class Block which has an attribute block_type, and a container class (say Page), you could have something like this:

class Page
  ...

  has_many :blocks do
    def ordered_by_type
      # self is the array of blocks
      self.sort_by(&:block_type)
    end
  end

  ...
end

Then when you call

page.blocks.ordered_by_type

you get what you want - defined by a Proc. Obviously, the Proc could be much more complex and is not working in the SQL call but after there result set has been compiled.

UPDATE:
I re-read this post and my answer after a bunch of time, and I wonder if you could do something as simple as another method which you basically suggested yourself in the post.

What if you added a method to Game called ordered_texts

  def ordered_texts
    texts.in_game_order(self)
  end

Does that solve the issue? Or does this method need to be chainable with other Game relation methods?

like image 36
mr rogers Avatar answered Nov 09 '22 10:11

mr rogers