Is it possible to set an instance-level constraint on a has_many, :through relationship in rails 3.1?
http://guides.rubyonrails.org/association_basics.html#the-has_many-association
Something like:
Class A
has_many :c, :through => :b, :conditions => { "'c'.something_id" => #a.something_id }
The documentation gives me hope with this, but it doesn't work for me:
If you need to evaluate conditions dynamically at runtime, you could
use string interpolation in single quotes:
class Customer < ActiveRecord::Base
has_many :latest_orders, :class_name => "Order",
:conditions => 'orders.created_at > #{10.hours.ago.to_s(:db).inspect}'
end
That gives me "unrecognized token '#'" on rails 3.1. Wondering if this functionality doesn't work anymore?
EDIT
Want to clarify why I don't think scopes are the solution. I want to be able to get from an instance of A all of the Cs that have a condition (which is based on an attribute of that instance of A). These are the only Cs that should EVER be associated with that A. To do this with scopes, I would put a scope on C that takes an argument, and then have to call it from #a with some value? I don't get why that's better than incorporating it into my has_many query directly.
Use a scope on the orders model:
class Order < ActiveRecord::Base
belongs_to :customer
scope :latest, lambda { where('created_at > ?', 10.hours.ago) }
end
And then call it with:
#customer.orders.latest
And if you really want to use latest_orders, you can instead add this to the Customer model:
def latest_orders
orders.where('created_at > ?', 10.hours.ago)
end
Related
Setup
For this question, I'll use the following three classes:
class SolarSystem < ActiveRecord::Base
has_many :planets
scope :has_earthlike_planet, joins(:planets).merge(Planet.like_earth)
end
class Planet < ActiveRecord::Base
belongs_to :solar_system
belongs_to :planet_type
scope :like_earth, joins(:planet_type).where(:planet_types => {:life => true, :gravity => 9.8})
end
class PlanetType < ActiveRecord::Base
has_many :planets
attr_accessible :gravity, :life
end
Problem
The scope has_earthlike_planet does not work. It gives me the following error:
ActiveRecord::ConfigurationError: Association named 'planet_type' was
not found; perhaps you misspelled it?
Question
I have found out that this is because it is equivalent to the following:
joins(:planets, :planet_type)...
and SolarSystem does not have a planet_type association. I'd like to use the like_earth scope on Planet, the has_earthlike_planet on SolarSystem, and would like to avoid duplicating code and conditions. Is there a way to merge these scopes like I'm attempting to do but am missing a piece? If not, what other techniques can I use to accomplish these goals?
Apparently, at this time you can only merge simple constructs that don't involve joins. Here is a possible workaround if you modify your models to look like this:
class SolarSystem < ActiveRecord::Base
has_many :planets
has_many :planet_types, :through => :planets
scope :has_earthlike_planet, joins(:planet_types).merge(PlanetType.like_earth)
end
class Planet < ActiveRecord::Base
belongs_to :solar_system
belongs_to :planet_type
scope :like_earth, joins(:planet_type).merge(PlanetType.like_earth)
end
class PlanetType < ActiveRecord::Base
has_many :planets
attr_accessible :gravity, :life
scope :like_earth, where(:life => true, :gravity => 9.8)
end
** UPDATE **
For the record, a bug was filed about this behavior - hopefully will be fixed soon...
You are reusing the conditions from the scope Planet.like_earth, which joins planet_type. When these conditions are merged, the planet_type association is being called on SolarSystem, which doesn't exist.
A SolarSystem has many planet_types through planets, but this is still not the right association name, since it is pluralized. You can add the following to the SolarSystem class to setup the planet_type association, which is just an alias for planet_types. You can't use the Ruby alias however since AREL reflects on the association macros, and doesn't query on whether the model responds to a method by that name:
class SolarSystem < ActiveRecord::Base
has_many :planets
has_many :planet_types, :through => :planets
has_many :planet_type, :through => :planets, :class_name => 'PlanetType'
scope :has_earthlike_planet, joins(:planets).merge(Planet.like_earth)
end
SolarSystem.has_earthlike_planet.to_sql # => SELECT "solar_systems".* FROM "solar_systems" INNER JOIN "planets" ON "planets"."solar_system_id" = "solar_systems"."id" INNER JOIN "planets" "planet_types_solar_systems_join" ON "solar_systems"."id" = "planet_types_solar_systems_join"."solar_system_id" INNER JOIN "planet_types" ON "planet_types"."id" = "planet_types_solar_systems_join"."planet_type_id" WHERE "planet_types"."life" = 't' AND "planet_types"."gravity" = 9.8
An easy solution that I found is that you can change your joins in your Planet class to
joins(Planet.joins(:planet_type).join_sql)
This will create an SQL string for the joins which will always include the correct table names and therefore should always be working no matter if you call the scope directly or use it in a merge. It's not that nice looking and may be a bit of a hack, but it's only a little more code and there's no need to change your associations.
I am trying to add a condition to a has many through association without luck. This is the association in my video model:
has_many :voted_users, :through => :video_votes, :source => :user
I want to only get the voted_users whose video_votes have a value equal to 1 for that video. How would I do this?
I would suggest creating a model method within the video model class
Something like:
def users_with_one_vote
self.voted_users, :conditions => ['value = ?', 1]
end
Then in the controller use video.users_with_one_vote
Then testing is easier too.
Any chance you can change that column name from 'value'. Might give some issues (reserved?).
I'd do this in 2 stages:
First, I'd define the has_many :through relationship between the models without any conditions.
Second, I'd add a 'scope' that defines a where condition.
Specifically, I'd do something like:
class User < ActiveRecord::Base
has_many :video_votes
has_many :votes, :through=>:video_votes
def self.voted_users
self.video_votes.voted
end
end
class VideoVote
def self.voted
where("value = ?", 1)
end
end
class Video
has_many :video_votes
has_many :users, :through=>:video_votes
end
Then you could get the users that have voted using:
VideoVote.voted.collect(&:user).uniq
which I believe would return an array of all users who had voted. This isn't the exact code you'd use -- they're just snippets -- but the idea is the same.
Would
has_many :voted_users, :through => :video_votes, :source => :user, :conditions => ['users.votes = ?', 1]
Do the trick?
I found that defining this method in my model works:
def upvoted_users
self.voted_users.where("value = 1")
end
and then calling #video.upvoted_users does the trick.
The best way to do this without messing with the relations is by crafting a more complex query. Relations is not the best thing to use for this particular problem. Please understand that relations is more a "way of data definition" then a way of "bussiness rules definition".
Bussiness logic or bussiness rules must be defined on a more specifically layer.
My suggestion for your problem is to create a method to search for users who voted on your video only once. something like:
class Video < ActiveRecord::Base
def voted_once()
User.joins(:video_votes).where("video_votes.value == 1 AND video_votes.video_id == ?", this.id)
end
Rails is magical for many things, but complex queries still have to be done in a "SQL" way of thinking. Don't let the illusional object oriented metaphor blind you
As long as we are throwing around ideas, how about using association extensions.
class VideoVote
scope :upvotes, where(:value => 1)
end
class Video
has_many :voted_users, :through => :video_votes, :source => :user do
def upvoted
scoped & VideoVote.upvotes
end
end
end
Then you feel good about making a call with absolutely no arguments AND you technically didn't add another method to your Video model (it's on the association, right?)
#video.voted_users.upvoted
I am using Ruby on Rails 3.0.7 and I would like to set an has_many : through dynamic condition.
In my model file I have:
class Article < ActiveRecord::Base
has_many :article_category_relationships,
:class_name => 'Article::Categories::ArticleRelationship',
:foreign_key => 'article_id',
:autosave => true,
:dependent => :destroy
# Here should be the dynamic condition statement (read below for more
# information about this)
has_many :article_categories,
:through => :article_category_relationships,
:source => :article_category,
:dependent => :destroy
end
In the related Article::Categories::ArticleRelationship database table I have also a created_by_user_id column (other columns are article_id and category_id) which represents the id of the user who created the relationship.
So, in order to retrieve article categories related to a user, in the above Record Association code I would like to filter :article_category_relationships by passing a dynamic condition which depends by that user id value. Otherwise, if I pass no id value, a default value should permit to retrieve all article categories by using the #article.article_categories code.
Is it possible? If so, how can I code that in the Record Association statement?
I believe a scope in your category model might be what you're looking for — something like this:
scope :by_user, lambda {|user|
unless user.nil?
where('article_category_relationships.created_by_user_id IS ?', user.id).
join(:article_category_relationships)
end
}
This allows you to call #article.article_categories.by_user(#user).
I accidentally noticed that two of my models have some resemblance. Their names are GameItem and OwnedItem. A GameItem is a just an item of the game, while an OwnedItem represents if a player has that item, if it's on his/her inventory or warehouse and more. My models are now like ( i removed validations and some irrelevant code for simplicity) :
class OwnedItem < ActiveRecord::Base
belongs_to :user
belongs_to :game_item
belongs_to :ownable, :polymorphic => true # [warehouse|inventory]
scope :equipped, where(:is_equipped => 1).includes(:game_item)
scope :item, lambda { |item_type|
joins(:game_item).
where("game_items.item_type = ?", item_type ).
limit(1)
}
scope :inventory, where(:ownable_type => 'Inventory')
scope :warehouse, where(:ownable_type => 'Warehouse')
end
class GameItem < ActiveRecord::Base
scope :can_be_sold, where(:is_sold => 1)
scope :item_type, lambda { |item_type|
where("game_items.item_type = ?", item_type )
}
scope :item_types, lambda { |item_types|
where("game_items.item_type IN (?)", item_types )
}
scope :class_type, lambda { |class_type|
where("game_items.class_type = ?", class_type )
}
scope :grade, lambda { |grade|
where("game_items.grade = ?", grade )
}
end
Notice the issue with game_item.item_type. I reference it in owned_item model, thus breaking encapsulation and repeating myself. How can i actually be able to do something like :
user.inventory.owned_items.item_type('Weapon').equipped
that is, without actually adding repeated code in my OwnedItem model, but getting that information out of the GameItem model ?
I think you've defined the relationships here in a way that's going to cause you trouble. You may find it's better off to use a simple user to item join model, something like this:
class User < ActiveRecord::Base
has_many :owned_items
has_many :game_items, :through => :owned_items
end
class OwnedItem < ActiveRecord::Base
belongs_to :user
belongs_to :game_item
# Has 'location' field: 'warehouse' or 'inventory'
end
class GameItem < ActiveRecord::Base
has_many :owned_items
has_many :users, :through => :owned_items
end
This is a common pattern where you have users and some kind of thing which they will own an instance of. The relationship table in the middle, OwnedItem, is used to establish, among other things, any unique characteristics of this particular instance of GameItem as well as the location of it relative to the user.
Generally this sort of structure avoids using polymorphic associations which can be trouble if used too casually. Whenever possible, try and avoid polymorphic associations unless they are on the very edge of your relationships. Putting them in the middle massively complicates joins and makes indexes a lot harder to tune.
As a note about the original, you can roll up a lot of that into a simple scope that uses the hash method for where:
scope :with_item_type, lambda { |types|
where('game_items.item_type' => types)
}
This will take either an array or string argument and will use IN or = accordingly. It's actually quite handy to do it this way because you won't need to remember which one to use.
I have the following models: Users, Groups, Conversations, ConversationParticipants( has a read boolean)
What I want to do is get an unread Count for a particular user in a particular group.
Should I be using a named_scope for this? If so, which model would this belong in (not sure)...
Also, I can do: #user.conversation_participations which then has the read field, problem is it does not have the group field as conversation_participations links to conversations (which has the group_id) via a conversation_id key.
Thoughts?
Thanks
You didn't show the code for the models, so I made some assumptions. Here's one way:
class Conversation < ActiveRecord::Base
belongs_to :group
has_many :conversation_participants
has_many :participants, :through => :conversation_participants,\
:source => :user
scope :unread, lambda { |user,group| includes(:group,:conversation_participants).\
where(:group_id => group.id,\
:conversation_participants => {:read => false,:user_id => user.id})
}
end
You're asking for "unread conversations belonging to a specific user and group". Since the thing being asked for is a set of Conversations, that's a natural place to put the scope.
EDIT
I see you wanted the count, not the result set. Just add .count to the scope:
Conversation.unread(user,group).count
EDIT 2
is it possible to do something like
this instead to get the #,
current_user.unread(group).count ..?
Add an instance method on User:
def unread(group)
Conversation.unread(self,group)
end
Now you can call current_user.unread(group).count
If I understand the question correctly. I would use a named_scope in the ConversationParticipants called something like in_group:
scope :in_group, lambda do
|group| joins(:conversation).where('conversation.group_id = ?', group.id)
end
I'm assuming the ConversationParticipants has belongs_to :conversation.
Now you can do:
#user.conversation_participations.in_group( some_group )