I have a project on Ruby on Rails 4.1.4, using activeadmin 1.0.0.pre from git://github.com/activeadmin/activeadmin
, pg 0.17.1
, PostgreSQL 9.3
In the project I have these models:
class User
has_one :account
class Account
belongs_to :user
has_many :project_accounts
has_many :projects, :through => :project_accounts
class Project
# the project has a boolean attribute 'archive'
has_many :project_accounts
class ProjectAccount
belongs_to :account
belongs_to :project
I've got a task to implement an ActiveAdmin filter on the index page, called "by_active_projects", so it has to show the users that have defined number of active projects, that means such projects that have archive == false
.
E.g. if I type '2' in the filter, it has to find such accounts that have exactly 2 active projects.
For now I have registered "Account" resource in ActiveAdmin, and within admin/account.rb
i've added filter :by_active_projects_eq
After that I've defined a scope having_active_projects
for Account model (models/account.rb):
scope :having_active_projects, ->(number) { joins(:projects).where("projects.archive = ?", false).having("count(projects) = ?", number).group("accounts.id") }
Next step, I've defined a ransacker for Account model like this:
ransacker :by_active_projects, formatter: proc{ |v|
data = Account.having_active_projects(v).map(&:id)
data ||= nil
} do |parent|
parent.table[:id]
end
In the development DB there is one account, that has exactly 8 active projects, and filtering works great for it. But when I've tried to filter accounts by 2 active projects, I faced an error. In the DB there are three such accounts, and the error page reported me that the query syntax is wrong:
SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "accounts" WHERE "accounts"."deleted_at" IS NULL AND "accounts"."id" = 'e4d247ec-e64d-4e8a-996a-4d73ccb11257', 'bcb8fa61-4a53-4b45-8954-8fb6ae328365', '93d670b6-7b8f-4c27-91cc-e0f44c137114' LIMIT 30 OFFSET 0) subquery_for_count
As you can see, instead of
"accounts"."id" IN ('e4d247ec-e64d-4e8a-996a-4d73ccb11257', 'bcb8fa61-4a53-4b45-8954-8fb6ae328365', '93d670b6-7b8f-4c27-91cc-e0f44c137114')
this thing is being generated:
"accounts"."id" = 'e4d247ec-e64d-4e8a-996a-4d73ccb11257', 'bcb8fa61-4a53-4b45-8954-8fb6ae328365', '93d670b6-7b8f-4c27-91cc-e0f44c137114'
I tried to dig down into the source code, moving with breakpoints on the pieces of code within the ActiveRecord, ActiveAdmin and Ransack lib's and figured out that the relation is being constructed with the help of Arel::Nodes::Equality. I'm not sure this is the reason but one I can say for sure:
lib/active_record/relation/query_methods.rb `
560 def where!(opts = :chain, *rest)
561 if opts == :chain
562 WhereChain.new(self)
563 else
564 references!(PredicateBuilder.references(opts)) if Hash === opts
565 self.where_values += build_where(opts, rest)
566 self
567 end
568 end`
self
here is an Active Record relation for Account;
before calling build_where
on the row #565, self.to_sql
equals to
SELECT "accounts".* FROM "accounts" WHERE "accounts"."deleted_at" IS NULL ORDER BY "accounts"."created_at" desc
after calling it and assigning the result to self.where_values
,
self.to_sql
equals to
SELECT "accounts".* FROM "accounts" WHERE "accounts"."deleted_at" IS NULL AND "accounts"."id" = 'e4d247ec-e64d-4e8a-996a-4d73ccb11257', 'bcb8fa61-4a53-4b45-8954-8fb6ae328365', '93d670b6-7b8f-4c27-91cc-e0f44c137114' ORDER BY "accounts"."created_at" desc
Any help or information on the matter is appreciated! Thanks!
So I found the solution:
First, I've changed my filter in admin/account.rb
from
filter :by_active_projects_eq
to
filter :by_active_projects_in,
:as => :string
This approach resulted in correct SQL generation,
"accounts"."id" IN ('e4d247ec-e64d-4e8a-996a-4d73ccb11257', 'bcb8fa61-4a53-4b45-8954-8fb6ae328365', '93d670b6-7b8f-4c27-91cc-e0f44c137114')
After that I also had to change my ransacker from
ransacker :by_active_projects, formatter: proc{ |v|
data = Account.having_active_projects(v).map(&:id)
data ||= nil
} do |parent|
parent.table[:id]
end
to
ransacker :by_active_projects, formatter: proc{ |v|
data = Account.having_active_projects(v).pluck(:id)
data.present? ? data : nil
} do |parent|
parent.table[:id]
end
because the way it was implemented also caused incorrect query: for example there are no such accounts that have exactly 5 active projects. In that case
data = Account.having_active_projects(v).pluck(:id)
returned "Empty Array", and dealing with this Array with data ||= nil
never actually returned nil
, and that resulted in SQL like this:
SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "accounts" WHERE "accounts"."deleted_at" IS NULL AND "accounts"."id" IN () LIMIT 30 OFFSET 0) subquery_for_count
notice the "accounts"."id" IN ()
part, that was causing trouble.
After replacing data ||= nil
with data.present? ? data : nil
, if data
was not present it was assigned a nil
, and that part in SQL was generated corectly: "accounts"."id" IN (NULL)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With