Rails self-referential nested association not working with scope - ruby-on-rails-3

I am building a synonym dictionary app in Rails 3.2.13 where every word has many meanings, and every meaning is associated to many words.
Hence, in order to get all the synonyms for a searched term (e.g.'house') I would want to be able to do something like:
Word.syno.find_by_entry('house')
I tried implementing with a scope, like this:
class Meaning < ActiveRecord::Base
has_and_belongs_to_many :words
has_and_belongs_to_many :synonyms, :class_name => "Word"
end
class Word < ActiveRecord::Base
attr_accessible :entry
has_and_belongs_to_many :meaningsā€¢
scope :syno, includes(:meanings => :synonyms)
end
Currently, I can get synonyms for 'house', with code like: Word.find_by_entry('house').meanings.first.synonims
However, when I ask for Word.syno.find_by_entry('house'), I get the word 'house' itself and none of its synonyms.
These are the generated SQL queries as reported by rails console, for that last expression:
1.9.3p286 :011 > Word.sino.find_by_entry("house")
Word Load (0.5ms) SELECT `words`.* FROM `words` WHERE `words`.`entry` = 'house' LIMIT 1
SQL (0.3ms) SELECT `meanings`.*, `t0`.`word_id` AS ar_association_key_name FROM `meanings` INNER JOIN `meanings_words` `t0` ON `meanings`.`id` = `t0`.`meaning_id` WHERE `t0`.`word_id` IN (10112)
SQL (12.9ms) SELECT `words`.*, `t0`.`meaning_id` AS ar_association_key_name FROM `words` INNER JOIN `meanings_words` `t0` ON `words`.`id` = `t0`.`word_id` WHERE `t0`.`meaning_id` IN (1174, 3941, 4926, 7360)
=> #<Word id: 10112, entry: "house", changed_at: nil, updated_at: nil>
How should I write my scope to avoid this behaviour?

I think you got the word 'house' because that is what you asked for, a word that has the entry 'house'. I would take a two staged approach, kind of like you did with your "what works" example.
class Word < ActiveRecord::Base
has_and_belongs_to_many :meanings
has_many :synonyms, :through => :meanings
end
Word.where(entry: 'house').first.synonyms
This assumes that the word exists... In practice you would check for the existence of the word before calling for its synonyms.

I was wrong and Rails answer to:
Word.syno.find_by_entry('house')
is a nested hash with all house's meanings and synonyms.
I tested this code in the Rails console and I was misled by the console's output, which only showed the response to the first query (i.e. the word 'house'). However, when I tested in my Rails application everything worked fine.
Nevertheless, I still must find out why the console's output didn't include the results of the last of all queries.

Related

Add Arbitrary Attribute to SQL Query from Joins Record without WHERE clause (Active record)

I'm trying to create an attribute in my select statement that depends on whether or not an association exists. I'm not sure if it's possible with a single query, and the goal is to not have to iterate a list afterward.
Here is the structure.
class Project < ApplicationRecord
has_many :subscriptions
has_many :users, through: :subscriptions
end
class User < ApplicationRecord
has_many :subscriptions
has_many :projects, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :project
belongs_to :user
end
Knowing a project, the goal of the query is to return ALL users and include on them a new attribute call subscribed - denoting whether or not they are subscribed.
non-working code (pseudo code):
project = Project.find_by(name: 'has_subscribers')
query = 'users.*, (subscriptions.project_id = ?) AS subscribed'
users = User.includes(:subscriptions).select(query, project.id)
user.first.subscribed
# => true or false
I'm open to whether or not there is a better way of going about this. However, the information is:
You know the project record.
You query a list of ALL users
Each user record has a subscribed attribute, denoting whether its
subscribed to the given project
Solution:
I was able to figure out a straight forward solution using the bool_or aggregate method. Coalesce ensures that the value returned is false instead of nil, should no subscriptions exists.
query = "users.*, COALESCE(bool_or(subscriptions.project_id = '#{project_id}'::uuid), false) as subscribed"
User.left_outer_joins(:subscriptions)
.select(query)
.group('users.id')
Yep, you can do this:
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id])
Which will result in a SQL query like this:
SELECT *, "subscriptions"."project_id" FROM "users" INNER JOIN "subscriptions" ON "subscriptions"."user_ud" = "users"."id";
If you want to specify a specific project (i.e. use an expression), you can do it with Arel like this:
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id].eq(42))
Unfortunately, you won't have a column name alias, and you can't call as on an Arel::Nodes::Equality instance. I don't know enough about the internals of Arel to have a way out of that box. But you can do this if you want the composability of Arel (e.g. if this is going to be something that needs to work with multiple models or columns):
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id].eq(42).to_sql + " as has_project")
This is a bit clunky, but it works and provides a user.has_project method that returns a boolean. You can pretty it up like so:
class User
scope :with_project_status, lambda do |project_id|
has_project =
Subscription.arel_table[:project_id].
eq(project_id).to_sql + " as has_project"
joins(:projects).select(Arel.star, has_project)
end
end
User.with_project_status(42).where(active: true)

putting a condition on an includes

I have the following relationships:
Category has_many :posts
Post has_many :comments
Post has_many :commenters, :through => :comments
I have the following eager load, giving me posts, comments and commenters (note that I need all 3, and hence the includes as opposed to joins)
category.posts.includes(:comments, :commenters)
However, I'd like to limit comments (and if possible commenters) to only those created in the past two weeks while still returning the same set of posts. Initially I thought I could specify a condition on the includes:
category.posts.includes(:comments, :commenters).where("comments.created_at > ?", 2.weeks.ago)
But found that this returns only the posts that meet the condition. I'm thinking that I may need to do something like performing a subquery on comments and then performing a join. Is there an easy way to do this with AR of would I be better off doing this with sql?
Finally managed to figure this out from reading this page:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
I simply needed to create an association in my Post model like:
Post has_many :recent_comments, :class_name = 'Comment', :conditions => ["created_at > ?", 2.weeks.ago]
Then I could do the following to get the desired ActiveRecord::Association object:
category.posts.includes(:recent_comments => :commenters)
There was also a suggestion of doing this by using a scope on a model. However, I read somewhere (I think it was on SO) that scopes are on their way out and that ARel has taken their place so I decided to do this without scopes.
Try :
category.posts.all(:includes => {:comments =>:commenters}, :conditions => ["comments.created_at = ? AND commenters.created_at = ?", 2.weeks.ago, 2.weeks.ago]

How to retrieve both "associated" records and "associated through" records in a performant way?

I am using Ruby on Rails 3.1 and I am trying to improve an SQL query in order to retrieve both "associated" records and "associated through" records (ActiveRecord::Associations) in a performant way so to avoid the "N + 1 query problem". That is, I have:
class Article < ActiveRecord::Base
has_many :category_relationships
has_many :categories,
:through => :category_relationships
end
class Category < ActiveRecord::Base
has_many :article_relationships
has_many :articles,
:through => :article_relationships
end
In a couple of SQL queries (that is, in a "performant way", maybe by using the Ruby on Rails includes() method) I would like to retrieve both categories and category_relationships, or both articles and article_relationships.
How can I make that?
P.S.: I am improve queries like the followings:
#category = Category.first
articles = #category.articles.where(:user_id => #current_user.id)
articles.each do |article|
# Note: In this example the 'inspect' method is just a method to "trigger" the
# "eager loading" functionalities
article.category_relationships.inspect
end
You can do
Article.includes(:category_relationships => :categories).find(1)
Which will reduce this to 3 queries (1 for each table). For performance, also make sure your foreign keys have an index.
But in general, I'm curious why the "category_relationships" entity exists at all, and why this isn't a has_and_belongs_to sort of situation?
Updated
As per your changed question, you can still do
Category.includes(:article_relationships => :articles).first
If you watch the console (or tail log/development) you'll see that when you call the associations, it'll hit the cached values and you're golden.
But I am still curious why you're not using a Has and Belongs To Many association.

Ruby-on-Rails: How to pull out most recent entries from a limited subset of a database table

Imagine something like a model User who has many Friends, each of who has many Comments, where I'm trying to display to the user the latest 100 comments by his friends.
Is it possible to draw out the latest 100 in a single SQL query, or am I going to have to use Ruby application logic to parse a bigger list or make multiple queries?
I see two ways of going about this:
starting at User.find and use some complex combination of :join and :limit. This method seems promising, but unfortunately, would return me users and not comments, and once I get those back, I'd have lots of models taking up memory (for each Friend and the User), lots of unnecessary fields being transferred (everything for the User, and everything about the name row for the Friends), and I'd still have to step through somehow to collect and sort all the comments in application logic.
starting at the Comments and using some sort of find_by_sql, but I just can't seem to figure out what I'd need to put in. I don't know how you could have the necessary information to pass in with this to limit it to only looking at comments made by friends.
Edit: I'm having some difficult getting EmFi's solution to work, and would appreciate any insight anyone can provide.
Friends are a cyclic association through a join table.
has_many :friendships
has_many :friends,
:through => :friendships,
:conditions => "status = #{Friendship::FULL}"
This is the error I'm getting in relevant part:
ERROR: column users.user_id does not exist
: SELECT "comments".* FROM "comments" INNER JOIN "users" ON "comments".user_id = "users".id WHERE (("users".user_id = 1) AND ((status = 2)))
When I just enter user.friends, and it works, this is the query it executes:
: SELECT "users".* FROM "users" INNER JOIN "friendships" ON "users".id = "friendships".friend_id WHERE (("friendships".user_id = 1) AND ((status = 2)))
So it seems like it's mangling the :through to have two :through's in one query.
Given the following relationships:
class User < ActiveRecord::Base
has_many :friends
has_many :comments
has_many :friends_comments, :through => :friends, :source => :comments
end
This statement will execute a single SQL statement. Associations essentially create named scopes for you that aren't evaluated until the end of the chain.
#user.friends_comments.find(:limit => 100, :order => 'created_at DESC')
If this is a common query, the find can be simplified into its own scope.
class Comments < ActiveRecord::Base
belongs_to :user
#named_scope was renamed to scope in Rails 3.2. If you're working
#if you're working in a previous version uncomment the following line.
#named_scope :recent, :limit => 100, : order => 'created at DESC'
scope :recent, :limit => 100, :order => 'created_at DESC'
end
So now you can do:
#user.friends_comments.recent
N.B.: The friends association on user may be a cyclical one through a join table, but that's not important to this solution. As long as friends is a working association on User, the preceding will work.

Constructing a has-and-belongs-to-many query

I have a rails app (running on version 2.2.2) that has a model called Product. Product is in a has-and-belongs-to-many relationship with Feature. The problem is that I need have search functionality for the products. So I need to be able to search for products that have a similar name, and some other attributes. The tricky part is that the search must also return products that have the exact set of features indicated in the search form (this is represented by a bunch of checkboxes). The following code works, but it strikes me as rather inefficient:
#products = Product.find(:all, :conditions=>["home=? AND name LIKE ? AND made_by LIKE ? AND supplier LIKE ? AND ins LIKE ?",hme,'%'+opts[0]+'%','%'+opts[1]+'%','%'+opts[3]+'%','%'+opts[4]+'%'])
#see if any of these products have the correct features
if !params[:feature_ids].nil?
f = params[:feature_ids].collect{|i| i.to_i}
#products.delete_if {|x| x.feature_ids!=f}
end
I'm sorry that my grasp of rails/sql is so weak, but does anyone have any suggestions about how to improve the above code? Thanks so much!
First, i would recommend you to manually write a FeatureProduct model (and not use the default 'has_and_belongs_to_many')
EG
class FeatureProduct
belongs_to :feature
belongs_to :product
end
class Product
has_many :feature_products
has_many :features, :through => :feature_products
end
class Feature
has_many :feature_products
has_many :products, :through => :feature_products
end
For the search: You may find the gem SearchLogic to be exactly what you need. It has support for 'LIKE' conditions (it means that you can write in a more 'Rails way' your query). It also has support for performing a search with conditions on a related model (on your Feature model, to be more precise).
The solution would be something like:
search = Product.search
search.name_like = opt[0]
search.made_by_like = opt[1]
...
search.feature_products_id_equals = your_feature_ids
..
#product_list = search.all
There is also an excellent screencast explaining the use of this gem.
Good luck :)