Referring to instance in has_many (Rails)
Asked Answered
D

6

6

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??

Dieppe answered 14/8, 2012 at 9:57 Comment(0)
A
12

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.

Albur answered 11/1, 2016 at 21:16 Comment(1)
Hey Mike :) thanks for your answer, have you by any chances find a way to avoid N+1 queries using this method?Asteria
W
4

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?

Wingfield answered 3/1, 2013 at 20:12 Comment(0)
T
1

Would an Association extension be a possibility?

It seems that you could make this work:

module Legacy
  def legacy_game_order
    order(proxy_association.owner.custom_texts_order)
  end
end

class Game << ActiveRecord::Base
  includes Legacy
  has_many :texts, :extend => Legacy

  def custom_texts_order
    # your custom query logic goes here
  end
end

That way, given a game instance, you should be able to access instance's custom query without having to pass in self:

g = Game.find(123)
g.texts.legacy_game_order
Tullusus answered 24/8, 2012 at 21:4 Comment(0)
E
0

Here is a way where you can do it,

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

This is another way which I usually wont recommend(but will work),

has_many :texts do
  def game_order(game)
    find(:all, :order => Text.in_game_order_query(game))
  end
end

and you can call them by,

game.texts.game_order(game)
Embow answered 14/8, 2012 at 10:12 Comment(2)
try with , has_many :texts, lambda { {:order => Text.in_game_order_query(self)} }Embow
"wrong number of arguments (1 for 0)" - I'm starting to think it's not possible :-( I also tried to move this function to the Game model, so that I can do self.in_game_order_query without having to pass an argument, but no luck.Dieppe
B
0

Im not sure what your order/query looks like in the in_game_order_query class method but i believe you can do this

has_many :texts, :finder_sql => proc{Text.in_game_order_query(self)}

Just letting you know that I have never used this before but I would appreciate it if you let me know if this works for you or not.

Check out http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many for more documentation on :finder_sql

Bedtime answered 15/8, 2012 at 2:59 Comment(0)
S
-1

I think if you want runtime information processed you should get this done with:

has_many :texts, :order => proc{ {Text.in_game_order_query(self)} }
Southerly answered 14/8, 2012 at 10:8 Comment(2)
The double curly braces give me a syntax error, and with only one set of {} around it says "Cannot visit proc".Dieppe
sorry typo. Only one set of braces. But I'm not sure, if this works with :order at all. I use it with :conditions. Maybe if proc does not work, try lambda or Proc.newJewry

© 2022 - 2024 — McMap. All rights reserved.