I'm trying to create a Rails 3 model scope, which is, essentially:
scope :published, where(:published => true)
scope :viewable_by, lambda { |user| where(:user_id => user.id).or(published) }
The trouble is, I can't find a way to make this work.
I had hoped arel_table would work, but it complains it "Cannot visit ActiveRecord::Relation":
where(arel_table[:user_id].eq(user.id)).or(published)
This could be done by replicating the "published" scope as Arel but then I would be repeating code (and in my project the "published" scope is quite a bit more in depth).
The best I can come up with is this:
scope :viewable_by, lambda { |user| where("(user_id = ?) OR (#{published.where_values.collect(&:to_sql).join(" AND ")})", user.id) }
This is the shortest I could come up with after a bit of digging around:
scope :published, where(:published => true)
scope :viewable_by, lambda { |user| where("user_id = ? OR #{published.where_clauses.first}", user.id) }
(Of course it only works with a single where clause; use a join if there's more than one.)
I would recommend using a gem like MetaWhere that more clearly exposes some of the Arel underpinnings. Here's an example from the main page linked above that creates an OR query:
Article.where(:title.matches % 'Hello%' | :title.matches % 'Goodbye%').to_sql
=> SELECT "articles".* FROM "articles" WHERE (("articles"."title" LIKE 'Hello%'
OR "articles"."title" LIKE 'Goodbye%'))
Related
I have this rails logic that uses partial SQL query code. I was wondering if there was a way a better way or a cleaner way to do the same thing (i.e. use rails's methods to replace the SQL code)?
#servers = Server
.select("*", "(SELECT AVG('reviews'.'average') FROM 'reviews' WHERE 'reviews'.'server_id' = 'servers'.'id') AS s_avg")
.order("s_avg DESC")
.paginate(:page => params[:page], :per_page => 25)
First good thing is to move that code from view or controller to model and wrap it in scope. Moreover, scopes can be chained.
class Server < ActiveRecord::Base
scope :averaged, -> { where(SQL CODE HERE) }
scope :expensive, -> { where('price > ?', price) }
scope :latest, -> { where('created_at > ?', Date.today - 3.days.ago) }
scope :active, -> { where(active: true) }
end
Only then you can pass and chain it in controller:
#servers = Server.latest.averaged
So, simply try to brake your SQL on several parts, move these parts to model and wrap them with scopes.
You can find a lot of useful examples of query methods without pure SQL here:
http://guides.rubyonrails.org/active_record_querying.html
Working on a webpage I used the next line:
Model.select(:column).where("column IS NOT NULL")
I was wondering if there was a more Rails-ish way to do this, like using a hash for example
Model.select(:column).where(column: !nil)
The Squeel gem will allow you to use != nil type syntax, but natively rails will not.
Example: Model.where{column != nil}
I would prefer to use a scope as its more readable as well as its more manageable later (like merging with other scopes)
class Model < ActiveRecord::Base
scope :not_null, lambda { |column|
{:select => column,
:conditions => "#{column} NOT NULL"
}
}
end
then use
Model.not_null("column_name")
In the Rails 4 you can do this:
Model.select(:column).where.not(column: nil)
I've implemented "following" function. Showing "people user A is following" was simple, but showing "people who are following user A" is giving me troubles.
I have follow_links, which have id, user_id, and follow_you_id column. When user A begins following user B, the columns will be like (user_id = A, follow_you_id = B).
To show users that A(#user) is following, I can simply do
#follow_yous = #user.follow_yous
But I'm not sure how to show users who are following A(#user)
To do this, I first found all the related links.
#follow_links = FollowLink.where(:follow_you_id => #user.id)
Now I thought I could just do #follow_mes = #follow_links.users, but it says user is an undefined method. So I guess I can either call user.follow_yous or follow_you.users.
My next approach was
#follow_links = FollowLink.where(:follow_you_id => #user.id)
#follow_mes = User.where(:id => #user.id, :include => #follow_links)
I intended to find all the User objects that had the provided #follow_links objects, but I think the syntax was wrong. I couldn't find a decent solution after a bit of research. I'd appreciate any help.
Update:
FollowLink model
belongs_to :user
belongs_to :follow_you, :class_name => "User"
You can use joins like this:
#users = User.joins(:follow_links).where(:follow_links => { :follow_you_id => #user.id })
you can use following:
#follow_links = FollowLink.where(:follow_you_id => #user.id)
#follow_links.collect(&:user) # :user should be the name of your relation to user in your followlink model
=> [User1, User2,...]
I am using Ruby on Rails 3.2.2 and I am experimenting the Squeel gem. I would like to know if (in some way, by using the Squeel gem or not) it is possible to "add" SQL clauses related to a scope method "directly" in a where clause. That is, I have:
class Article < ActiveRecord::Base
# Note: This is a scope method.
def self.created_by(user)
where(:user_id => user.id)
end
# I would like to use a scope method like the following.
#
# Note: Code in the following method doesn't work, but it should help
# understanding what I mean.
def self.scope_method_name(user)
where{ created_by(user) | ... & ... }
end
end
So, when I run Article.scope_method_name(#current_user).to_sql then it should return something like the following:
SELECT articles.* FROM articles WHERE articles.user_id = 1 OR ... AND ...
I tryed sifters but those (at least for me) are intended to be used exclusively in other Squeel statements. That is, if I state a sifter then I cannot use that to scope ActiveRecords because that sifter returns a Squeel::Nodes::Predicate object instead of an ActiveRecord::Relation.
You have to drop down into more raw AREL for OR operations
def self.scope_method_name(user)
t = arel_table
where(
(t[:user_id].eq(user.id).or(
t[:blah].eq('otherthing')
).and([:bleh].eq('thirdthing'))
)
end
Or something along those lines.
You can chain scopes like Article.by_author(user).by_editor() but this joins all the conditions with ANDs. So, to get around this, you can write individual scopes (not chaining them) using Squeel like:
class Article < ActiveRecord::Base
scope :by_author, ->(user) { where{author_id == user.id} }
scope :by_editor, ->(user) { where{editor_id == user.id} }
scope :by_title, ->(token) { where{title =~ "%#{token}%"} }
scope :by_author_or_editor, ->(user) { where{(author_id == user.id)|(editor_id == user.id)} }
scope :by_author_or_editor_and_title, ->(user, token) { where{((author_id == user.id)|(editor_id == user.id))&(title =~ "%#{token}%")} }
end
Or you can use sifters:
class Article < ActiveRecord::Base
sifter :sift_author do |user|
author_id == user.id
end
sifter :sift_editor do |user|
editor_id == user.id
end
sift :sift_title do |token|
title =~ "%#{token}%"
end
scope :by_author, ->(user) { where{sift :sift_author, user} }
scope :by_editor, ->(user) { where{sift :sift_editor, user} }
scope :by_title, ->(token) { where{sift :sift_title, token} }
scope :by_author_or_editor, -> (user) { where{(sift :sift_author, user)|(sift :sift_editor, user)} }
scope :by_author_or_editor_and_title, ->(user, token) { where{((sift :sift_author, user)|(sift :sift_editor, user))&(sift :sift_title, token)} }
end
This gives you your scopes that return an ActiveRecord::Relation, so you can in theory further chain them.
I'm calling a pretty simple function, and can't seem to figure out whats going on. (I'm using rails 3.0.3 and the master branch of 'will_paginate' gem). I have the following code:
results = Article.search(params) # returns an array of articles
#search_results = results.paginate :page => params[:page], :per_page=>8, :order => order_clause
No matter what I make the order_clause (for example 'article_title desc' and 'article_title asc'), the results are always the same in the same order. So when I check using something like #search_results[0], the element is always the same. In my view, they are obviously always the same as well. Am I totally missing something?
I'm sure its something silly, but I've been banging my head against the wall all night. Any help would be much appreciated!
Edited to Add: The search clause does the following:
def self.search(params)
full_text_search(params[:query].to_s).
category_search(params[:article_category].blank? ? '' : params[:article_category][:name]).
payout_search(params[:payout_direction], params[:payout_value]).
length_search(params[:length_direction], params[:length_value]).
pending.
distinct.
all
end
where each of these guys is a searchlogic based function like this:
#scopes
scope :text_search, lambda {|query|
{
:joins => "INNER JOIN users ON users.id IN (articles.writer_id, articles.buyer_id)",
:conditions => ["(articles.article_title LIKE :query) OR
(articles.description LIKE :query) OR
(users.first_name LIKE :query) OR
(users.last_name LIKE :query)", { :query => "%#{query}%" }]
}
}
scope :distinct, :select => "distinct articles.*"
#methods
def self.payout_search(dir, val)
return no_op if val.blank?
send("payment_amount_#{dir.gsub(/\s+/,'').underscore}", val)
end
def self.length_search(dir, val)
return no_op if val.blank?
send("min_words_#{dir.gsub(/\s+/,'').underscore}", val)
end
Thanks.
If you look at the example from the will_paginate github page you can spot one important difference between their use of the :order clause and yours:
#posts = Post.paginate :page => params[:page], :order => 'created_at DESC'
This calls paginate on the Post object (with no objects being selected yet - no SQL has been executed before paginate comes along). This is different in your example: as you state in the first line of code "returns an array of articles". The simplest I can come up with showing the problem is
results = Model.limit(5).all
#results = results.paginate :order => :doesnt_matter_anymore
won't sort, but this will:
results = Model.limit(5)
#results = results.paginate :order => :matters
It should suffice to take the all out of the search method. It makes ActiveRecord actually perform the SQL query when calling this method. Will_paginate will do that for you when you call paginate (if you let it...). Check out the section on Lazy Loading in this post about Active Record Query Interface 3.0