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??
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.
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?
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
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)
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
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)} }
Related
I am adding a feature to an old app that was not made by me, this along with being relatively new to RoR is leading to some confusion for me.
I have models called reponse, activity_point, and report
response has two parents, it belongs_to activity_point and report.
I am trying to access activity_points for a do block like so:
report.responses.activity_points.activity.each do |a|
Obviously that isn't working. I am getting the error message:
undefined method `activity_points' for []:ActiveRecord::Relation
Thanks to anyone who can help me with this little problem.
Or you can add something like this to your Report model
has_many :responses
has_many :activity_points, :through => :responses
has_many :activities, :through => :activity_points
then you can do this
report.activities.each do |a|
Another way to do this kind of thing, add a method to Report and joins from the other side (to get activity objects)
def activities
Activity.joins(:activity_points => :responses).where('responses.report_id = ?', id)
end
The point of doing all this, you don't want to create Ruby objects if you don't need to. Nested loops are also a potential problem with unique items and sorting.
Each response have several activity_points so you should iterate through responses. Also each activity_point has several activities, so:
report.responses.each do |r|
r.activity_points.each do |ap|
ap.activity.each do |a|
# Do your thing
end
end
end
First, when you write report.responses, this will return an ActiveRecord array. Since activity_points is an undefined method for arrays, you can't call it. So to call this method there is two conditions:
You have to tell your app which element of the array will call the method. For instance, report.responses.first.activity_points or report.responses.second.activity_points ...
Response model has to have a has_many: activity_points to call this method.
You could still also use a loop, but that will take multiple DB calls. Therefore, my solution involves direct database call for efficiency.
Activity.includes(activity_point: {responses: :report}).where(reports: {id: report.id}).each do |a|
#...
#...
end
I'm trying to do a pretty simple join in my model to list all 'Locations' in a 'Post' with a certain id.
Currently, each post has_many :locations, :through => :location_post. I'm using the 'blogit' gem, which puts posts in a module named 'Blogit::Posts'.
I'm getting a wrong argument type Class (expected Module) error when I try to run the following in my Post.rb model:
module Blogit
class Post < ActiveRecord::Base
before_save :reuse_existing_locations
def reuse_existing_locations
existing_locations = Location.include(Blogit::Post).first
end
How can I do a join through a module?
Thanks in advance!
I'm not sure I understand what you're trying to accomplish so just some notes and observations:
By looking at the code, it's clear that Blogit::Post is a class, not a module.
The include method takes modules (not classes), that's the error you're seeing.
You are calling the include method on the Location model and that seems kind
of strange to me. Did you mean to call includes? But then again that
wouldn't make much sense since it seems like you've got a many to many
relationship between Location and Blogit::Post.
In the Location model (which doesn't need to be in the Blogit namespace), you can simply reference the Blogit::Post model as
follows:
has_many :posts, class_name: "Blogit::Post", ...
If existing_locations is in fact an attribute on the model and you want to assign to it, you need to put self in front of it (as in self.existing_locations). Otherwise you're just creating a local variable.
You probably wanted to use ActiveModels includes instead of Rubys include, which is to include methods from another module.
I'm pretty new to ruby and ruby-on-rails, so I need to learn best practices.
I have Tag model, each tag can have many sub-tags and many super-tags:
has_many :super_tags, :through => :tag_hier_rels, :source => :super_tag
has_many :sub_tags, :through => :reverse_tag_hier_rels, :source => :sub_tag
has_many :tag_hier_rels, :foreign_key => "sub_tag_id"
has_many :reverse_tag_hier_rels, :foreign_key => "super_tag_id", :class_name => "TagHierRel"
I need to prevent user to create circular references. But with auto-generated methods :super_tags and :sub_tags I can't do this: everyone can do something like:
tag.super_tags.push another_tag, and I have no control on this.
Rails validation mechanism isn't useful here: this mechanism prevents user to save invalid objects to database, but I need to prevent him even to modify object incorrectly: if I have circular reference, and I need to get all the sub-tags or super-tags recursively, I will run into stack overflow.
So I have done the following:
Declared these associations as private ones:
private :sub_tags=, :sub_tags
private :super_tags=, :super_tags
Added methods with _copy postfix:
def sub_tags_copy
return sub_tags.clone
end
def super_tags_copy
return super_tags.clone
end
And added methods that actualy modify arrays:
def sub_tags_push(tag)
sub_tags.push tag if !self.all_sub_tags.include? tag and !self.all_super_tags.include? tag
end
def super_tags_push(tag)
super_tags.push tag if !self.all_sub_tags.include? tag and !self.all_super_tags.include? tag
end
# TODO: more methods (at least we need to remove tags)
(methods all_sub_tags and all_super_tags generate arrays recursively)
It works, but I don't really like this solution: at least, it's not obvious for user that he should use ..._copy methods.
Probably I am doing this wrong?
UPD:
Or, is it bad practice in general to disallow user to change an object in wrong way? Probably I should allow user to change an object in wrong way, but only validate it before saving?
At least, I already figured out that it's hard to supply error messages if user does something wrong: currently, model just silently does not modify an object, and no error message is generated. I have to implement my own error messages engine, and this fact is an evidence that approach is really bad... I seem to struggle against the framework instead of using it.
You are always referring to user where you mean programmers?
If that is the case, you should try to assert this kind of stuff through
Education (developer to developer)
Validation (via tests or rails validations)
Constraints (on the database level through db constraints or triggers)
I would try to do it top down.
Updated
Appears to be a precedence error and nothing to do with the question I originally asked. See discussion below.
Original question
Is it possible to use active record associations in callbacks? I've tested this code in the console and it works fine as long as it isn't in a callback. I'm trying to create callbacks that pull attributes from other associated models and I keep getting errors of nil.attribute.
If callbacks are not the correct approach to take, how would one do a similar action in rails? If the associations are simple, you could use create_association(attributes => ), but as associations get more complex this starts to get messy.
For example...
class User < ActiveRecord::Base
belongs_to :b
before_validation_on_create {|user| user.create_b} #note, other logic prevents creating multiple b
end
class B < ActiveRecord::Base
has_many :users, :dependent => destroy
after_create{ |b| b.create_c }
has_one :c
end
class C < ActiveRecord::Base
belongs_to :b
after_create :create_alert_email
private
def create_alert_email
self.alert_email = User.find_by_b_id(self.b_id).email #error, looks for nil.email
end
end
Off course associations are available in your callbacks. After all, the create_after_email is simply a method. You can even call it alone, without using a callback. ActiveRecord doesn't apply any special flag to callback methods to prevent them from working as any other method.
Also notice you are running a User#find query directly without taking advantage of any association method. An other reason why ActiveRecord association feature should not be the guilty in this case.
The reason why you are getting the error should probably searched somewhere else.
Be sure self.b_id is set and references a valid record. Perhaps it is nil or actually there's no User record with that value. In fact, you don't test whether the query returns a record or nil: you are assuming a record with that value always exists. Are you sure this assumption is always statisfied?
The business logic is this: Users are in a Boat through a join table, I guess let's call that model a Ticket. But when a User instance wants to check who else is on the boat, there's a condition that asks if that user has permission see everyone on the Boat, or just certain people on the Boat. If a User can see everyone, the normal deal is fine: some_user.boats.first.users returns all users with a ticket for that boat. But for some users, the only people that are on the boat (as far as they're concerned) are people in, let's say the dining room. So if User's ticket is "tagged" (using an acts_as_taggable style system) with "Dining Room", the only Users returned from some_user.boats.first.users should be Users with tickets tagged "Dining Room".
Just for the record, I'm not trying to design something to be insane from the getgo - I'm trying to wedge this arbitrary grouping into a (mostly) existent system.
So we've got:
class User
has_many :tickets
has_many :boats, :through => :tickets
end
class Ticket
belongs_to :user
belongs_to :boat
end
class Boat
has_many :tickets
has_many :users, :through => :tickets
end
Initially, I thought that I could conditionally modify the virtual class like:
singleton = class << a_user_instance ; self ; end
singleton.class_eval(<<-code
has_many :tickets, :include => :tags, :conditions => ['tags.id in (?)', [#{tag_ids.to_s(:db)}]]
code
)
That gets all the way down to generating the SQL, but when generated, it generates SQL ending in:
LEFT OUTER JOIN "tags" ON ("tags"."id" = "taggings"."tag_id") WHERE ("tickets"._id = 1069416589 AND (tags.id in (5001,4502)))
I've tried digging around the ActiveRecord code, but I can't find anywhere that would prefix that 'id' in the SQL above with an underscore. I know that associations are loaded when an ActiveRecord class is loaded, and I'd assume the same with a singleton class. shrug.
I also used an alias_method_chain like:
singleton = class << a_user_instance ; self ; end
singleton.class_eval(<<-code
def tickets_with_tag_filtering
tags = Tag.find(etc, etc)
tickets_without_tag_filtering.scoped(:include => :tags, :conditions => {:'tags.id' => tags})
end
alias_method_chain :tickets, :tag_filtering
code
)
But while that approach produces the desired Tickets, any joins on those tickets use the conditions in the class, not the virtual class. some_user.boats.first.users returns all users.
Any type of comment will be appreciated, especially if I'm barking up the wrong tree with this approach. Thanks!
So a wild guess about your underscore issue is that Rails is generating the assocation code based on the context at the time of evaluation. Being in a singleton class could mess this up, like so:
"#{owner.table_name}.#{association.class.name}_id = #{association.id}"
You could get in there and define a class name property on your singleton class and see if that fixes the issue.
On the whole I don't recommend this. It creates behavior that is agonizing to track down and impossible to extend effectively. It creates a landmine in the codebase that will wound you or someone you love at a later time.
Instead, consider using a named_scope declaration:
class User
has_many :taggings, :through => :tickets
named_scope :visible_to, lambda { |looking_user|
{ :include => [ :tickets, :taggings ],
:conditions => [ "tickets.boat_id in (?) and taggings.ticket_id = tickets.id and taggings.tag_id in (?)", looking_user.boat_ids, looking_user.tag_ids ]
}
}
end
While you may have to go back and edit some code, this is much more flexible in the ways it can be used:
Boat.last.users.visible_to( current_user )
It's clear that a restriction is being placed on the find, and what the purpose of that restriction is. Because the conditions are dynamically calculated at runtime, you can deal with the next weird modification your client hits you with. Say some of their users have xray vision and clairvoyance:
class User
named_scope :visible_to, lambda { |looking_user|
if looking_user.superhuman?
{}
else
{ :include => [ :tickets, :taggings ],
:conditions => [ "tickets.boat_id in (?) and taggings.ticket_id = tickets.id and taggings.tag_id in (?)", looking_user.boat_ids, looking_user.tag_ids ]
}
end
}
end
By returning an empty hash, you can effectively nullify the effect of the scope.
Why not just grab all users on the boat and include their tags.
Then run a quick filter to include & return only the users with the same tag as the inquiring user.
What version of Rails are you using? Have you tried upgrading to see if the underscore issue is fixed? It's like it can't find the foreign key to put in as "tag_id" or somethin'.
My ruby-fu is limited, so I'm not sure how to dynamically include the correct method options at run-time.
Just to help you clarify, you have to worry about this two places. You want to filter a user's viewable users so they only see users with the same tags. Your structure is:
user <--> tickets <--> boats <--> tickets <--> users
... right?
So, you need to filter both sets of tickets down to the ones with the current_user's tags.
Maybe you just need a current_user.viewable_users() method and then filter everything through that? I'm not sure what existing functionality you've got to preserve.
Blech, I don't feel like I'm helping you at all. Sorry.
Your approach is the problem. I know it seems expedient at the moment to hack something in where you don't have to refactor the existing call sites, but I believe given time this will come back to haunt you as the source of bugs and complexity.
Sleeping dogs that lie come back to bite you hard, in my experience. Typically in the form of a future developer who doesn't know your association is "magic" and uses it assuming it's just pail ole rails. He/she likely won't even have a reason to write a test case that would expose the behavior either, which raises the odds you'll only find out about the bug when it's live in production and the client is unhappy. Is it really worth the time you're saving now?
Austinfrombostin is pointing the way. Different semantics? Different names. Rule number one is always to write code that says what it does as clearly as possible. Anything else is the path of madness.