How to pass a Relation as a subquery in Rails 4? - ruby-on-rails

The example below works in Rails 3 but fails in Rails 4.
The reason is that the Relation returned by accessing the association now uses actual database parameter rather than interpolating the "owning" id.
class Blog < ActiveRecord::Base
has_many :posts, :dependent => :destroy
end
class Post < ActiveRecord::Base
belongs_to :blog
end
b = Blog.first # provided something exists of course
query = Post.where(id: b.posts.where("'complicated query' = ''")); 1 # just to avoid printing in console
puts query.to_sql
# SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (SELECT "posts"."id" FROM "posts" WHERE "posts"."blog_id" = $1 AND ('complicated query' = ''))
query.to_a
# raises the error:
# PG::UndefinedParameter: ERROR: there is no parameter $1
# LINE 1: ...M "posts" WHERE "posts"."blog_id" = $1 AND
# ^
Note the $1 parameter in the subquery which is obviously not provided from the main query (thus the error).
So the question is how can we do the same thing in Rails 4 now (preferably with minimal changes)?

Answering my own question.
This actually isn't a breaking change in Rails 4. It should still works as expected.
It is squeel gem that breaks it https://github.com/ernie/squeel/issues/272

Related

Rails: weird behavior when getting last record while ordering

I'm using Rails 6 and I've noticed a strange behavior in Active Record when trying to get the latest record from a collection. Here is what I have:
session.rb
class Session < ApplicationRecord
has_many :participations
end
participation.rb
class Participation < ApplicationRecord
belongs_to :session
end
When I'm trying to get the latest participation with:
Participation.order(created_at: :desc).last
The SQL query generated looks like:
SELECT "participations".*
FROM "participations"
ORDER BY "participations"."created_at" ASC
LIMIT $1
Note that I did order(created_at: :desc) but the SQL is using ASC.
However, if I change my code to:
Participation.order(created_at: :asc).last
The SQL query is doing the opposite (a DESC):
SELECT "participations".*
FROM "participations"
ORDER BY "participations"."created_at" DESC
LIMIT $1
Does anyone have an explanation as to why it behave this way ? Is it a Rails bug ?
Seems like using last with order is causing this issue. If I remove last, ActiveRecord is generating the correct SQL (using the correct order)
ActiveRecord is optimizing the SQL statement for you. This
Participation.order(created_at: :desc).last
returns the same result as
Participation.order(created_at: :asc).first
But the latter statement is more efficient because it has to traverse fewer rows, so Rails generates SQL as if you had written it that way.

Unable to order by translated column in Rails using Mobility

This question is based on an issue posted to the Mobility GitHub project.
Context
Rails: 5.0.6
Mobility: 0.4.2 (with table backend)
I'm working with an articles table that supports multiple article types (e.g., blog post, case study, knowledge base article). This table includes a column to track the number of times an article is viewed—an integer column that increments every time a show action is called for an article.
In implementing translations for these articles, I want to track the number of views for each translation individually, not for the main article object. In an effort to achieve this, I included the views property as one of the translated properties on my object:
class Article < ApplicationRecord
include Taggable
include PgSearch
translates :title, type: :string
translates :subtitle, type: :text
translates :body, type: :text
translates :views, type: :integer
multisearchable :against => [:title, :subtitle, :body]
has_and_belongs_to_many :products
attachment :hero_image, content_type: %w(image/jpeg image/png image/gif)
validates :title, :body, :posted_on, presence: true
scope :current, -> { where 'posted_on < ?', Date.tomorrow }
scope :news_articles, -> { where type: ['BlogPost', 'CaseStudy'] }
def log_view(by = 1)
self.views ||= 0
self.views += by
self.save(touch: false)
end
def to_param
"#{id} #{title}".parameterize
end
def published?
posted_on < Date.tomorrow
end
end
Expected Behavior
In my controller, I want to list the top ten most viewed articles, which I get with this query:
#top_articles = Article.current.news_articles.order(views: :desc, posted_on: :desc).limit(10)
I expect to receive an array of articles, as I did before implementing Mobility.
Actual Behavior
What I get instead is #<Article::ActiveRecord_Relation:0x233686c>. If I then try to convert that to an array with #top_articles.to_a, I get this error:
Article Load (0.7ms) SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-11') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"."views" DESC, "articles"."posted_on" DESC LIMIT $1 [["LIMIT", 10]]
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column articles.views does not exist
LINE 1: ...les"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"...
^
: SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-11') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"."views" DESC, "articles"."posted_on" DESC LIMIT $1
from /usr/local/bundle/gems/activerecord-5.0.6/lib/active_record/connection_adapters/postgresql_adapter.rb:598:in `async_exec'
Changing the query to include i18n:
#top_articles = Article.i18n.current.news_articles.order(views: :desc, posted_on: :desc).limit(10)
… returns #<#<Mobility::Backends::ActiveRecord::Table::QueryMethods:0x00000000050a86d8>:0x286c3e0>, and when I try to convert that to an array, I get the same thing:
Article Load (0.7ms) SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-11') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"."views" DESC, "articles"."posted_on" DESC LIMIT $1 [["LIMIT", 10]]
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column articles.views does not exist
LINE 1: ...les"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"...
^
: SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-11') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY "articles"."views" DESC, "articles"."posted_on" DESC LIMIT $1
from /usr/local/bundle/gems/activerecord-5.0.6/lib/active_record/connection_adapters/postgresql_adapter.rb:598:in `async_exec'
It turns out that while Mobility supports the use of translated fields in the where clause, it does not currently support them in the order clause of an Active Record query.
Workaround attempts
1. Reference the translation table in the order clause
Based on feedback from the gem author, I tried the query:
Article.i18n.current.news_articles.order('article_translations.views desc', 'articles.posted_on desc')
… which returns a #<#<Mobility::Backends::ActiveRecord::Table::QueryMethods>> object, and to_a returns this error:
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "article_translations"
LINE 1: ...les"."type" IN ('BlogPost', 'CaseStudy') ORDER BY article_tr...
^
: SELECT "articles".* FROM "articles" WHERE (posted_on < '2018-02-12') AND "articles"."type" IN ('BlogPost', 'CaseStudy') ORDER BY article_translations.views desc, articles.posted_on desc LIMIT $1
from /usr/local/bundle/gems/activerecord-5.0.6/lib/active_record/connection_adapters/postgresql_adapter.rb:598:in `async_exec'
2. Add a joins or includes clause for the translation table
Article.i18n.joins(:article_translations).order('article_translations.views desc', 'articles.posted_on desc').limit(10)
This query, again, returns a #<#<Mobility::Backends::ActiveRecord::Table::QueryMethods>> object, and to_a results in:
ActiveRecord::ConfigurationError: Can't join 'Article' to association named 'article_translations'; perhaps you misspelled it?
from /usr/local/bundle/gems/activerecord-5.0.6/lib/active_record/associations/join_dependency.rb:231:in `find_reflection'
3. Add has_many :article_translations to model
Adding a relation to the model throws back this error:
uninitialized constant Article::ArticleTranslation
So…
What should I try next?
UPDATE
Ordering by translated attributes is now supported as of version 0.8.0/
Just do this:
Article.i18n.current.news_articles.
order(:views => :desc, :'articles.posted_on' => :desc)
and Mobility will handle everything (you don't need to join the translation table, etc.)
ORIGINAL ANSWER
You were right that you need to join the translations table, but the association is named translations, not article_translations.
In any case, there is a method join_translations that joins the translation table, so this should work:
Article.i18n.
current.
news_articles.
join_translations.
order('article_translations.views desc', 'articles.posted_on desc')

Rails include a conditional polymorphic association

I have a polymorphic association:
Following the rails association model picture from rails guide and Supposing employees is my Content and pictures is my translations i need perform an filtered association.
class Content < ApplicationRecord
has_many :translations,:as => :transl
end
class Translation < ApplicationRecord
enum idiom: [ :EN, :NO]
belongs_to :transl, :polymorphic => true
end
As a result I would like to have a json with my content but only with specific idiom for that i have something like this:
Content.all.to_json(:include => {:translations => {:only => [:text, :idiom],:where =>{idiom: "EN"}}})
I tried to create a method to be included on to_json, but methods inside it is meant to be only getters.
i tried also left outer join but it don't work as well:
SELECT "contents".* FROM "contents" LEFT OUTER JOIN "translations" ON "translations"."transl_id" = "contents"."id" WHERE (translations.idiom=0)
To work i must to perform a raw query enforcing to get some results from the left side i assume that could have some contents without translation such as:
SELECT "contents".* FROM "contents" LEFT OUTER JOIN "translations" ON "contents"."id" = "translations"."transl_id" **OR "contents"."id" >0** WHERE (translations.idiom="EN")
Is there a better way to do this?
SELECT "contents". FROM "contents" LEFT OUTER JOIN "translations" ON "contents"."id" = "translations"."transl_id" OR "contents"."id" >0 WHERE (translations.idiom="EN")*
can be ===>
#lang="EN"
Content.where(:id > 0).includes(:translations).where({:translations =>{:idiom=>#lang}})

Rails Scoping for Existence

I am running Rails 4.0. Ruby 2.0
For signing up, I only ask users to provide me with there email in the new page.
In the update action, I then ask users for their name.
When listing users in the Index action, I only want to show users have updated their name.
I know I need to scope based on if users have updated their name.
User Model
scope :name, where(name: true)
User Controller
def index
#users = User.name
end
Error
undefined method `gsub' for #
I Think the issue is the way, I am calling the scope. I might need to use exists?
Any help is greatly, appreciated. Thank you.
Personally, just to be a little more conventional, I would use
class User < ActiveRecord::Base
scope :has_name, where("users.name != ''")
end
This way, when your model gets joined with another, you won't introduce a column ambiguity in the event multiple tables have a name column
Consider this example
$ rails new app
$ cd app
$ rails g resource User name:string company_id:integer
$ rails g resource Company name:string
$ rake db:migrate
Our models
class User < ActiveRecord::Base
belongs_to :company
scope :has_name, where("name != ''")
end
class Company < ActiveRecord::Base
has_many :users
end
A problem
$ rails c
irb> User.has_name.join(:company)
Oh noes!
User Load (0.4ms) SELECT "users".* FROM "users" INNER JOIN
"companies" ON "companies"."id" = "users"."company_id" WHERE
(name != '')
SQLite3::SQLException: ambiguous column name: name: SELECT
"users".* FROM "users" INNER JOIN "companies" ON
"companies"."id" = "users"."company_id" WHERE (name != '')
ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous
column name: name: SELECT "users".* FROM "users" INNER JOIN
"companies" ON "companies"."id" = "users"."company_id" WHERE
(name != '')
Let's fix the scope
class User < ActiveRecord::Base
belongs_to :company
scope :has_name, where("users.name != ''")
end
Re-run our query
irb> reload!
irb> User.has_name.join(:company)
Proper output
User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "companies"
ON "companies"."id" = "users"."company_id" WHERE (users.name != '')
=> []
You could use:
scope :with_name, where("name <> ''")
Though the above doesn't tell you if they've actually modified their name, just that it isn't blank. If you wanted to track the name column for changes, you could use something like the PaperTrail gem for this.
Based on additional feedback, I'd recommend:
scope :with_name, where("users.name != ''")

How do I write a UNION chain with ActiveRelation?

I need to be able to chain an arbitrary number of sub-selects with UNION using ActiveRelation.
I'm a little confused by the ARel implementation of this, since it seems to assume UNION is a binary operation.
However:
( select_statement_a ) UNION ( select_statement_b ) UNION ( select_statement_c )
is valid SQL. Is this possible without doing nasty string-substitution?
You can do a bit better than what Adam Lassek has proposed though he is on the right track. I've just solved a similar problem trying to get a friends list from a social network model. Friends can be aquired automatically in various ways but I would like to have an ActiveRelation friendly query method that can handle further chaining. So I have
class User
has_many :events_as_owner, :class_name => "Event", :inverse_of => :owner, :foreign_key => :owner_id, :dependent => :destroy
has_many :events_as_guest, :through => :invitations, :source => :event
def friends
friends_as_guests = User.joins{events_as_guest}.where{events_as_guest.owner_id==my{id}}
friends_as_hosts = User.joins{events_as_owner}.joins{invitations}.where{invitations.user_id==my{id}}
User.where do
(id.in friends_as_guests.select{id}
) |
(id.in friends_as_hosts.select{id}
)
end
end
end
which takes advantage of Squeels subquery support. Generated SQL is
SELECT "users".*
FROM "users"
WHERE (( "users"."id" IN (SELECT "users"."id"
FROM "users"
INNER JOIN "invitations"
ON "invitations"."user_id" = "users"."id"
INNER JOIN "events"
ON "events"."id" = "invitations"."event_id"
WHERE "events"."owner_id" = 87)
OR "users"."id" IN (SELECT "users"."id"
FROM "users"
INNER JOIN "events"
ON "events"."owner_id" = "users"."id"
INNER JOIN "invitations"
ON "invitations"."user_id" =
"users"."id"
WHERE "invitations"."user_id" = 87) ))
An alternative pattern where you need a variable number of components is demonstrated with a slight modification to the above code
def friends
friends_as_guests = User.joins{events_as_guest}.where{events_as_guest.owner_id==my{id}}
friends_as_hosts = User.joins{events_as_owner}.joins{invitations}.where{invitations.user_id==my{id}}
components = [friends_as_guests, friends_as_hosts]
User.where do
components = components.map { |c| id.in c.select{id} }
components.inject do |s, i|
s | i
end
end
end
And here is a rough guess as to the solution for the OP's exact question
class Shift < ActiveRecord::Base
def self.limit_per_day(options = {})
options[:start] ||= Date.today
options[:stop] ||= Date.today.next_month
options[:per_day] ||= 5
queries = (options[:start]..options[:stop]).map do |day|
where{|s| s.scheduled_start >= day}.
where{|s| s.scheduled_start < day.tomorrow}.
limit(options[:per_day])
end
where do
queries.map { |c| id.in c.select{id} }.inject do |s, i|
s | i
end
end
end
end
Because of the way the ARel visitor was generating the unions, I kept getting SQL errors while using Arel::Nodes::Union. Looks like old-fashioned string interpolation was the only way to get this working.
I have a Shift model, and I want to get a collection of shifts for a given date range, limited to five shifts per day. This is a class method on the Shift model:
def limit_per_day(options = {})
options[:start] ||= Date.today
options[:stop] ||= Date.today.next_month
options[:per_day] ||= 5
queries = (options[:start]..options[:stop]).map do |day|
select{id}.
where{|s| s.scheduled_start >= day}.
where{|s| s.scheduled_start < day.tomorrow}.
limit(options[:per_day])
end.map{|q| "( #{ q.to_sql } )" }
where %{"shifts"."id" in ( #{queries.join(' UNION ')} )}
end
(I am using Squeel in addition to ActiveRecord)
Having to resort to string-interpolation is annoying, but at least the user-provided parameters are being sanitized correctly. I would of course appreciate suggestions to make this cleaner.
I like Squeel. But don't use it. So I came to this solution (Arel 4.0.2)
def build_union(left, right)
if right.length > 1
Arel::Nodes::UnionAll.new(left, build_union(right[0], right[1..-1]))
else
Arel::Nodes::UnionAll.new(left, right[0])
end
end
managers = [select_manager_1, select_manager_2, select_manager_3]
build_union(managers[0], managers[1..-1]).to_sql
# => ( (SELECT table1.* from table1)
# UNION ALL
# ( (SELECT table2.* from table2)
# UNION ALL
# (SELECT table3.* from table3) ) )
There's a way to make this work using arel:
tc=TestColumn.arel_table
return TestColumn.where(tc[:id]
.in(TestColumn.select(:id)
.where(:attr1=>true)
.union(TestColumn.select(:id)
.select(:id)
.where(:attr2=>true))))

Resources