ActiveRecord query, joins (Rails 3.1) - ruby-on-rails

How would I go about getting a JSON object with
[
{
Property.field_1,
Property.field_n,
PropAssignmConsumer.field_1,
PropAssignmConsumer.field_n
},
{
Property.field_1,
Property.field_n,
PropAssignmConsumer.field_1,
PropAssignmConsumer.field_n
},
...,
{
Property.field_1,
Property.field_n,
PropAssignmConsumer.field_1,
PropAssignmConsumer.field_n
}
]
sorted by some key (can be a field in either Property or PropAssignmConsumer) for a given user_entity object? i.e. get all properties linked to a given consumer/user_entity, extracting fields from both properties and prop_assignm_consumers, sorting by a field in the properties or prop_assignm_consumer table.
These are my models:
class Property < ActiveRecord::Base
has_many :prop_assignm_consumers, :dependent => :restrict
end
class PropAssignmConsumer < ActiveRecord::Base
belongs_to :consumer
belongs_to :property
end
class Consumer < UserEntity
has_many :prop_assignm_consumers, :dependent => :destroy
has_many :properties, :through => :prop_assignm_consumers
end
I am currently doing
properties = user_entity.properties.find(:all, :order => "#{sort_key} #{sort_ord}")
properties.each do |p|
a = p.prop_assignm_consumers.find_by_consumer_id(current_user.user_entity.id)
... do something with a and p....
end
but this seems inefficient....
Any help would be appreciated.

Maybe I'm missing something. Why doesn't your property also reference consumers? You have many to many, you just didn't complete it. Just adding has_many :consumers, :through => :prop_assignm_consumer would then let you do
properties = user_entity_properties.all(:include => :consumers)
properties.each do |p|
p.consumers.where(:id => current_user.user_entity.id)
end
Though now that we write that, and given that you're doing find_by and not find_all_by, it's pretty clear there's going to be only 1. So you can go the other way.
consumer = Consumer.where(:id => current_user.user_entity.id).includes(:properties).first
consumer.properties.each do |p|
... do something with p and consumer
end
ref
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Related

How to query a collection through multiple associations?

I need to collect objects that are all connected through multiple layers of associations, and I don't know who to do so.
I need to get a collection of CustomText based on a string query param.
Basically I need to do a query that will pull a collection of CustomText by name:
#searched_content = params[:search].downcase
#query = CustomText.where("lower(name) like ?", "%#{#searched_content}%")
but then also filters the #query to only search LineItems that have been a part of an approved order. I am using Spree, where Spree::Order has_many Spree::LineItem. Basically, doing something like this (doesn't work at all, but hopefully you'll be able to see what I'm trying to do):
#query = Spree::LineItem.joins(:order).where(spree_orders: {state: "complete"}).joins(:custom_texts).where("lower(name) like ?", "%#{#searched_content}%"))
Models:
class CustomText < ActiveRecord::Base
belongs_to :custom_set, :inverse_of => :custom_texts
end
class CustomSet < ActiveRecord::Base
belongs_to :spree_line_item, :class_name => Spree::LineItem, :foreign_key => :spree_line_item_id
has_may :custom_texts, :dependent => :destroy, :inverse_of => :custom_set
end
class LineItem < ActiveRecord::Base
has_many :custom_texts, :through => :custom_sets
has_many :custom_sets, :dependent => :destroy, :foreign_key => :spree_line_item_id
end
Any help would be very appreciated.
Well, this was just a simple syntax error with one too many end parenthesis at the end...stupid spelling error.
#query = Spree::LineItem.joins(:order).where(spree_orders: {state: "complete"}).joins(:custom_texts).where("lower(name) like ?", "%#{#searched_content}%")

(Rails 3) Combine two queries into one

I have these models simplified:
class Game::Champ < ActiveRecord::Base
has_one :contract, :class_name => "Game::ChampTeamContract", :dependent => :destroy
has_one :team, :through => :contract
# Attributes: :avg => integer
end
#
class Game::Team < ActiveRecord::Base
has_many :contracts, :class_name => "Game::ChampTeamContract", :dependent => :destroy
has_many :champs, :through => :contracts
end
#
class Game::ChampTeamContract < ActiveRecord::Base
belongs_to :champ
belongs_to :team
# Attributes: :expired => bool, :under_negotiation => bool
end
#
So what I want to do here is to find all Game::Champs that have no Game::ChampTeamContract whatsoever OR has, but (is not :under_negociation OR is :expired ), sorted by Champ.avg ASC
I am kinda stuck at using two queries, concating the result and sorting it. I wish there were a better way to to it more "Railish"
UPDATE: Just added a constraint about :expired
Try something like:
Game::Champs.
joins("left outer join game_champ_team_contracts on game_champ_team_contracts.champ_id = game_champs.id").
where("game_champ_team_contracts.id is null or (game_champ_team_contracts.state != ? or game_champ_team_contracts.state = ?)", :under_negotiation, :expired).
order("game_champs.avg ASC")
This is a fairly nasty line if left as-is, so if you use this, it needs tidying up. Use scopes or methods to split it up as much as possible!
I just tested with a super simple query:
#bars1 = Bar.where(:something => 1)
#bars2 = Bar.where(:something => 2)
#bars = #bars1 + #bars2
Not sure if it's right, but it works...

Rails has_many :through and Setting Property on Join model

Similar to this question, how do I set a property on the join model just before save in this context?
class Post < ActiveRecord::Base
has_many :post_assets
has_many :assets, :through => :post_assets
has_many :featured_images, :through => :post_assets, :class_name => "Asset", :source => :asset, :conditions => ['post_assets.context = ?', "featured"]
end
class PostAssets < ActiveRecord::Base
belongs_to :post
belongs_to :asset
# context is so we know the scope or role
# the join plays
validates_presences_of :context
end
class Asset < ActiveRecord::Base
has_many :post_assets
has_many :posts, :through => :post_assets
end
I just want to be able to do this:
#post = Post.create!(:title => "A Post")
#post.featured_images << Asset.create!(:title => "An Asset")
# ...
#post = Post.first
#featured = #post.featured_images.first
#=> #<Asset id: 1, title: "An Asset">
#featured.current_post_asset #=> #<PostAsset id: 1, context: "featured">
How would that work? I've been banging my head over it all day :).
What currently happens is when I do this:
#post.featured_images << Asset.create!(:title => "An Asset")
Then the join model PostAsset that gets created never gets a chance to set context. How do I set that context property? It looks like this:
PostAsset.first #=> #<PostAsset id: 1, context: nil>
Update:
I have created a test gem to try to isolate the problem. Is there an easier way to do this?!
This ActsAsJoinable::Core class makes it so you can have many to many relationships with a context between them in the join model. And it adds helper methods. The basic tests show basically what I'm trying to do. Any better ideas on how to do this properly?
Look at the has_many options in the ActiveRecord::Associations::ClassMethods API located here: http://rails.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#M001316
This is the most interesting quote:
:conditions
Specify the conditions that the associated object must meet in order to be included as a WHERE SQL fragment, such as authorized = 1. 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.
So I believe your conditions must be specified as a hash, like so:
class Post < ActiveRecord::Base
has_many :post_assets
has_many :featured_post_assets, :conditions => { :context => 'featured' }
has_many :assets, :through => :post_assets
has_many :featured_images, :through => :featured_post_assets,
:class_name => "Asset", :source => :asset,
end
And you should also do the following:
#post.featured_images.build(:title => "An asset")
instead of:
#post.featured_images << Asset.create!(:title => "An Asset")
This should call the scoped asset build, as suggested in the quote above to add the context field to asset. It will also save both the join model object (post_asset) and the asset object to the database at the same time in one atomic transaction.

Rails model with foreign_key and link table

I am trying to create a model for a ruby on rails project that builds relationships between different words. Think of it as a dictionary where the "Links" between two words shows that they can be used synonymously. My DB looks something like this:
Words
----
id
Links
-----
id
word1_id
word2_id
How do I create a relationship between two words, using the link-table. I've tried to create the model but was not sure how to get the link-table into play:
class Word < ActiveRecord::Base
has_many :synonyms, :class_name => 'Word', :foreign_key => 'word1_id'
end
In general, if your association has suffixes such as 1 and 2, it's not set up properly. Try this for the Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
end
Link model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => 'Word'
# Creates the complementary link automatically - this means all synonymous
# relationships are represented in #word.synonyms
def after_save_on_create
if find_complement.nil?
Link.new(:word => synonym, :synonym => word).save
end
end
# Deletes the complementary link automatically.
def after_destroy
if complement = find_complement
complement.destroy
end
end
protected
def find_complement
Link.find(:first, :conditions =>
["word_id = ? and synonym_id = ?", synonym.id, word.id])
end
end
Tables:
Words
----
id
Links
-----
id
word_id
synonym_id
Hmm, this is a tricky one. That is because synonyms can be from either the word1 id or the word2 id or both.
Anyway, when using a Model for the link table, you must use the :through option on the Models that use the Link Table
class Word < ActiveRecord::Base
has_many :links1, :class_name => 'Link', :foreign_key => 'word1_id'
has_many :synonyms1, :through => :links1, :source => :word
has_many :links2, :class_name => 'Link', :foreign_key => 'word2_id'
has_many :synonyms2, :through => :links2, :source => :word
end
That should do it, but now you must check two places to get all the synonyms. I would add a method that joined these, inside class Word.
def synonyms
return synonyms1 || synonyms2
end
||ing the results together will join the arrays and eliminate duplicates between them.
*This code is untested.
Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
def link_to(word)
synonyms << word
word.synonyms << self
end
end
Setting :dependent => :destroy on the has_many :links will remove all the links associated with that word before destroying the word record.
Link Model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => "Word"
end
Assuming you're using the latest Rails, you won't have to specify the foreign key for the belongs_to :synonym. If I recall correctly, this was introduced as a standard in Rails 2.
Word table:
name
Link table:
word_id
synonym_id
To link an existing word as a synonym to another word:
word = Word.find_by_name("feline")
word.link_to(Word.find_by_name("cat"))
To create a new word as a synonym to another word:
word = Word.find_by_name("canine")
word.link_to(Word.create(:name => "dog"))
I'd view it from a different angle; since all the words are synonymous, you shouldn't promote any one of them to be the "best". Try something like this:
class Concept < ActiveRecord::Base
has_many :words
end
class Word < ActiveRecord::Base
belongs_to :concept
validates_presence_of :text
validates_uniqueness_of :text, :scope => :concept_id
# A sophisticated association would be better than this.
def synonyms
concept.words - [self]
end
end
Now you can do
word = Word.find_by_text("epiphany")
word.synonyms
Trying to implement Sarah's solution I came across 2 issues:
Firstly, the solution doesn't work when wanting to assign synonyms by doing
word.synonyms << s1 or word.synonyms = [s1,s2]
Also deleting synonyms indirectly doesn't work properly. This is because Rails doesn't trigger the after_save_on_create and after_destroy callbacks when it automatically creates or deletes the Link records. At least not in Rails 2.3.5 where I tried it on.
This can be fixed by using :after_add and :after_remove callbacks in the Word model:
has_many :synonyms, :through => :links,
:after_add => :after_add_synonym,
:after_remove => :after_remove_synonym
Where the callbacks are Sarah's methods, slightly adjusted:
def after_add_synonym synonym
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end
def after_remove_synonym synonym
if complement = find_synonym_complement(synonym)
complement.destroy
end
end
protected
def find_synonym_complement synonym
Link.find(:first, :conditions => ["word_id = ? and synonym_id = ?", synonym.id, self.id])
end
The second issue of Sarah's solution is that synonyms that other words already have when linked together with a new word are not added to the new word and vice versa.
Here is a small modification that fixes this problem and ensures that all synonyms of a group are always linked to all other synonyms in that group:
def after_add_synonym synonym
for other_synonym in self.synonyms
synonym.synonyms << other_synonym if other_synonym != synonym and !synonym.synonyms.include?(other_synonym)
end
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end

how to access rails join model attributes when using has_many :through

I have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end

Resources