I'm having the following issue. I want to set a certain amount of has_many relations in a model, with names taken from a passed array (or in this case, the keys of a hash). Like this:
object_class_names = {:foo => FooClass, :bar => BarClass}
for key_name in object_class_names.keys
has_many "#{key_name}_objects".to_sym,
-> {where(var: key_name)},
:class_name => object_class_names[key_name]
end
This results in two has_many relations: some_object.foo_objects & some_object.bar_objects. Both have a specific class_name and a specific scope, set in the where clause in the lambda. However, because the scope is a lambda it gets the key_name in the where clause dynamically. This is the last known key_name variable, which is the last one in the loop, in this case 'bar'. So both foo_objects and bar_objects return a set of objects scoped with where(var: "bar").
Usually the lambda makes a great way to pass in dynamic scopes in has_many relations, but in this case I don't really need it. Is it possible to set a static scope inside a has_many relation?
You might use Hash#each_pair here:
object_class_names.each_pair do |key_name, klass|
has_many :"#{key_name}_objects", -> { where(var: key_name) }, class_name: klass.to_s
end
Does this work? I haven't tested it, but the theory is that you're accessing the specific key name you want, rather than the last-known key name.
object_class_names.keys.each_with_index do |key_name, index|
has_many "#{key_name}_objects",
-> { where(:var => object_class_names.keys[index]) },
:class_name => object_class_names[key_name]
end
Related
I am working on a plugin for Discourse, which means that I can modify classes with class_eval, but I cannot change the DB schema. To store extra data about the Topic model, I can perform joins with TopicCustomField, which is provided for this purpose.
I am able to store and retrieve all the data I need, but when many Topics are loaded at once, the DB performance is inefficient because my indirect data is loaded once for each Topic by itself. It would be much better if this data were loaded all at once for each Topic, like can happen when using preload or includes.
For example, each Topic has a topic_guid, and a set of parent_guids (stored in a single string with dashes because order is important). These parent_guids point to both other Topic's topic_guids as well as the name of other Groups.
I would love to be able write something like:
has_many :topic_custom_fields
has_many :parent_guids, -> { where(name: 'parent_guids').pluck(:value).first }, :through => :topic_custom_fields
has_many :parent_groups, class_name: 'Group', primary_key: :parent_guids, foreign_key: :name
But this :through complains about not being able to find an association ":parent_guids" in TopicCustomField, and primary_key won't actually take an association instead of a DB column.
I've also tried the following, but the :through clauses are not able to use the functions as associations.
has_many :topic_custom_fields do
def parent_guids
parent_guids_str = where(name: PARENT_GUIDS_FIELD_NAME).pluck(:value).first
return [] unless parent_guids_str
parent_guids_str.split('-').delete_if { |s| s.length == 0 }
end
def parent_groups
Group.where(name: parent_guids)
end
end
has_many :parent_guids, :through => :topic_custom_fields
has_many :parent_groups, :through => :topic_custom_fields
Using Rails 4.2.7.1
Actually, through parameter of rails associations is to set a many-to-many association with a model, passing "through" other model:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
So you can't do
has_many :parent_guids, :through => :topic_custom_fields
since ParentGuid is not a model related to TopicCustomFields. Also, pass a block to has_many is only to extend the association with new methods as the ones rails already provide you, like topic_custom_fields.create, topic_custom_fields.build, etc.
Why don't you define the methods inside the block at your second example in the Topic class to retrieve the groups? Is there something you want that would not be possible only with the methods?
Update
Well, I don't think it's possible achieve the same improved performance in this case, since the group ids still have to be handled from topic_custom_fields, and the improved performance is reached through JOINs. Maybe a complex combination of preload, where and references could do the trick, but I don't know if it's possible.
You could try to minimize the db calls instead, maybe gathering all the parent_guids before querying the groups.
I hope there is a more elegant solution, but this is what I have done in order to preload my data efficiently. This should be fairly easy to extend to other applications.
I modify Relation's exec_queries, which calls other preloading functions.
ActiveRecord::Relation.class_eval do
attr_accessor :preload_funcs
old_exec_queries = self.instance_method(:exec_queries)
define_method(:exec_queries) do |&block|
records = old_exec_queries.bind(self).call(&block)
if preload_funcs
preload_funcs.each do |func|
func.call(self, records)
end
end
records
end
end
To Topic, I added:
has_many :topic_custom_fields
attr_accessor :parent_groups
def parent_guids
parent_guids_str = topic_custom_fields.select { |a| a.name == PARENT_GUIDS_FIELD_NAME }.first
return [] unless parent_guids_str
parent_guids_str.value.split('-').delete_if { |s| s.length == 0 }
end
And then in order to preload the parent_groups, I do:
def preload_parent_groups(topics)
topics.preload_funcs ||= []
topics.preload_funcs <<= Proc.new do |association, records|
parent_guidss = association.map {|t| t.parent_guids}.flatten
parent_groupss = Group.where(name: parent_guidss).to_a
records.each do |t|
t.parent_groups = t.parent_guids.map {|guid| parent_groupss.select {|group| group.name == guid }.first}
end
end
topics
end
And finally, I add the preloaders to my Relation query:
result = result.preload(:topic_custom_fields)
result = preload_parent_groups(result)
I have a product model setup like the following:
class Product < ActiveRecord::Base
has_many :product_atts, :dependent => :destroy
has_many :atts, :through => :product_atts
has_many :variants, :class_name => "Product", :foreign_key => "parent_id", :dependent => :destroy
end
And I want to search for products that have associations with multiple attributes.
I thought maybe this would work:
Product.joins(:product_atts).where(parent_id: params[:product_id]).where(product_atts: {att_id: [5,7]})
But this does not seem to do what I am looking for. This does where ID or ID.
So I tried the following:
Product.joins(:product_atts).where(parent_id: 3).where(product_atts: {att_id: 5}).where(product_atts: {att_id: 7})
But this doesn't work either, it returns 0 results.
So my question is how do I look for a model by passing in attributes of multiple join models of the same model type?
SOLUTION:
att_ids = params[:att_ids] #This is an array of attribute ids
product = Product.find(params[:product_id]) #This is the parent product
scope = att_ids.reduce(product.variants) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE product_id=products.id AND att_id=?)', att_id)
end
product_variant = scope.first
This is a seemingly-simple request made actually pretty tricky by how SQL works. Joins are always just joining rows together, and your WHERE clauses are only going to be looking at one row at a time (hence why your expectations are not working like you expect -- it's not possible for one row to have two values for the same column.
There are a bunch of ways to solve this when dealing with raw SQL, but in Rails, I've found the simplest (not most efficient) way is to embed subqueries using the EXISTS keyword. Wrapping that up in a solution which handles arbitrary number of desired att_ids, you get:
scope = att_ids_to_find.reduce(Product) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE parent_id=products.id AND att_id=?)', att_id)
end
products = scope.all
If you're not familiar with reduce, what's going on is it's taking Product, then adding one additional where clause for each att_id. The end result is something like Product.where(...).where(...).where(...), but you don't need to worry about that too much. This solution also works well when mixed with scopes and other joins.
I have a Gift model:
class Gift
include Mongoid::Document
include Mongoid::Timestamps
has_many :gift_units, :inverse_of => :gift
end
And I have a GiftUnit model:
class GiftUnit
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :gift, :inverse_of => :gift_units
end
Some of my gifts have gift_units, but others have not. How do I query for all the gifts where gift.gift_units.size > 0?
Fyi: Gift.where(:gift_units.exists => true) does not return anything.
That has_many is an assertion about the structure of GiftUnit, not the structure of Gift. When you say something like this:
class A
has_many :bs
end
you are saying that instance of B have an a_id field whose values are ids for A instances, i.e. for any b which is an instance of B, you can say A.find(b.a_id) and get an instance of A back.
MongoDB doesn't support JOINs so anything in a Gift.where has to be a Gift field. But your Gifts have no gift_units field so Gift.where(:gift_units.exists => true) will never give you anything.
You could probably use aggregation through GiftUnit to find what you're looking for but a counter cache on your belongs_to relation should work better. If you had this:
belongs_to :gift, :inverse_of => :gift_units, :counter_cache => true
then you would get a gift_units_count field in your Gifts and you could:
Gift.where(:gift_units_count.gt => 0)
to find what you're looking for. You might have to add the gift_units_count field to Gift yourself, I'm finding conflicting information about this but I'm told (by a reliable source) in the comments that Mongoid4 creates the field itself.
If you're adding the counter cache to existing documents then you'll have to use update_counters to initialize them before you can query on them.
I tried to find a solution for this problem several times already and always gave up. I just got an idea how this can be easily mimicked. It might not be a very scalable way, but it works for limited object counts. The key to this is a sentence from this documentation where it says:
Class methods on models that return criteria objects are also treated like scopes, and can be chained as well.
So, get this done, you can define a class function like so:
def self.with_units
ids = Gift.all.select{|g| g.gift_units.count > 0}.map(&:id)
Gift.where(:id.in => ids)
end
The advantage is, that you can do all kinds of queries on the associated (GiftUnits) model and return those Gift instances, where those queries are satisfied (which was the case for me) and most importantly you can chain further queries like so:
Gift.with_units.where(:some_field => some_value)
This seems like a bug in Rails to me, but there's probably not much I can do about that. So how can I accomplish my expected behavior?
Suppose we have:
class User < ActiveRecord::Base
has_many :awesome_friends, :class_name => "Friend", :conditions => {:awesome => true}
end
And execute the code:
>> my_user.awesome_friends << Friend.new(:name=>'jim')
Afterwards, when I inspect this friend object, I see that the user_id field is populated. But I would also expect to see the "awesome" column set to 'true', which it is not.
Furthermore, if I execute the following from the console:
>> my_user.awesome_friends << Friend.new(:name=>'jim')
>> my_user.awesome_friends
= [#<Friend id:1, name:"jim", awesome:nil>]
# Quit and restart the console
>> my_user.awesome_friends
= []
Any thoughts on this? I suppose the conditions hash could be arbitrarily complex, making integration into the setter impossible. But in a way it feels like by default we are passing the condition ":user_id => self.id", and that gets set, so shouldn't others?
Thanks,
Mike
EDIT:
I found that there are callbacks for has_many, so I think I might define the relationship like this:
has_many :awesome_friends,
:class_name => "Friend",
:conditions => {:awesome => true},
:before_add => Proc.new{|p,c| c.awesome = true},
:before_remove => Proc.new{|p,c| c.awesome = false}
Although, it's starting to feel like maybe I'm just implementing some other, existing design pattern. Maybe I should subclass AwesomeFriend < Friend? Ultimately I need a couple of these has_many relationships, and subclassing get's messy with all the extra files..
EDIT 2:
Okay, thanks to everyone who commented! I ultimately wrapped up the method above into a nice little ActiveRecord extension, 'has_many_of_type'. Which works like follows:
has_many_of_type :awesome_friends, :class_name => "Friend", :type=>:awesome
Which just translates to has_many with the appropriate conditions, before_add, and before_remove params (and it assumes the existence of a column named friend_type).
You need use:
my_user.awesome_friends.create(:name=>'jim') or my_user.awesome_friends.build(:name=>'jim')
In documentation:
has_many (:conditions)
Record creations from the association are scoped if a hash is used. has_many :posts, :conditions => {:published => true} will create published posts with #blog.posts.create or #blog.posts.build.
It's :class_name rather than :class, for one thing.
This isn't a bug I don't think. The :conditions hash only deterimines how you query for the objects. But I don't think it's rational to just assume that any object you stuff in the collection could be made to conform to the conditions.
In your simple example it makes sense, but you could also put more complex logic in there.
The documentation seems pretty clear on this as well:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
:conditions
Specify the conditions that the associated object must meet in order to be included as a WHERE SQL fragment, such as authorized = 1.
I need to pass self as object not class to :conditions string, is there any way to do this?
has_many :topic,
:class => 'FileTopic',
:conditions => "id in (select * from file_topics where program_id = #{self.id})"
My problem is self is always giving me the id of the class but not the instance of the class. I guess has_many is evaluated on the class level?
Thanks
It is evalued upon loading the class, yeah. But only if you use double quotes - variables in single-quoted strings are filled upon calling. More info here.
However, maybe you should look into named scopes?
Has many is a class method. So any reference to self in its arguments are references to the class.
It looks like you want to specify the foreign key on the belongs_to side of things.
Have you tried this yet:
has_many :topic, :class => 'FileTopic', :foreign_key => "program_id"
You should really have a read through the ActiveRecord::Associations documentation if you haven't yet. There are very few association problems that can't be solved using the right set of options to belongs_to/has_one/has_many