Get count from another table - ruby-on-rails

I have a many-to-many, HMT model setup and I want to add a count value to return as part of my tab_community#index method. The models for the table of interest and the join table are as follows:
class TabCommunity < ApplicationRecord
belongs_to :ref_community_type
has_many :tab_client_project_communities
has_many :tab_projects, through: :tab_client_project_communities
has_many :tab_community_accounts
has_many :tab_accounts, through: :tab_community_accounts
has_many :tab_client_project_communities
has_many :ref_platforms, through: :tab_client_project_communities
end
class TabCommunityAccount < ApplicationRecord
belongs_to :tab_community
belongs_to :tab_account
end
The #index method currently looks like this:
_tab_community_ids = params[:tab_community_ids].split(',')
#tab_communities = TabCommunity.where(id: _tab_community_ids).includes(:ref_platforms).all.order(:updated_at).reverse_order
This query is what I want to replicate in ActiveRecord:
select (select count(*) from tab_community_accounts where tab_community_id = c.id) as cnt, c.* from tab_communities c
The results I want are below:
7318 149 sports_writers 7 2017-12-17 15:45:36.946965 2017-12-17 15:45:36.946965
0 172 random_admin 8 2018-04-16 19:21:21.844041 2018-04-16 19:21:21.844041
2731 173 random_aacc 7 2018-04-16 19:22:35.074461 2018-04-16 19:22:35.074461
(The 1st column is count(*) from tab_community_accounts, the rest is from tab_communities.)
From what I've seen so far I should use either .select() or .pluck() but neither one works for me. I tried this out:
TabCommunity.pluck("distinct tab_community_accounts.tab_account_id as cnt").where(id: _tab_community_ids).includes(:ref_platforms).all.order(:updated_at).reverse_order
Is this close to what I need or am I completely off?

What you want is something like:
#tab_communities = TabCommunity
.where(id: _tab_community_ids)
.select('tab_communities.*, count(tab_community_accounts.id) AS cnt')
.left_outer_joins(:tab_community_accounts)
.includes(:ref_platforms) # consider if you actually need this
.group(:id)
.order(updated_at: :desc) # use an explicit order instead!
TabCommunity Load (1.1ms) SELECT tab_communities.*, count(tab_community_accounts.id) AS cnt FROM "tab_communities" LEFT OUTER JOIN "tab_community_accounts" ON "tab_community_accounts"."tab_community_id" = "tab_communities"."id" WHERE "tab_communities"."id" = 1 GROUP BY "tab_communities"."id" ORDER BY "tab_communities"."updated_at" DESC
=> #<ActiveRecord::Relation [#<TabCommunity id: 1, created_at: "2018-05-07 21:13:24", updated_at: "2018-05-07 21:13:24">]>
.select just alters the SELECT portion of the query. The result returned is still an ActiveRecord::Relation containing model instances.
ActiveRecord will automatically create an attribute for cnt:
irb(main):047:0> #tab_communities.map(&:cnt)
=> [1]
.pluck on the other hand just pulls the column values and returns an array or array of arrays if the query contains multiple columns.
#tab_communities = TabCommunity
.where(id: _tab_community_ids)
.left_outer_joins(:tab_community_accounts)
.includes(:ref_platforms) # consider if you actually need this
.group(:id)
.order(updated_at: :desc)
.pluck('tab_communities.id, count(tab_community_accounts.id) AS cnt')
(1.0ms) SELECT tab_communities.id, count(tab_community_accounts.id) AS cnt FROM "tab_communities" LEFT OUTER JOIN "tab_community_accounts" ON "tab_community_accounts"."tab_community_id" = "tab_communities"."id" WHERE "tab_communities"."id" = 1 GROUP BY "tab_communities"."id" ORDER BY "tab_communities"."updated_at" DESC
=> [[1, 1]]
Using .* with pluck is not a good idea since you don't know what order the attributes have in the resulting array.

Related

Rails join two tables based on relation in third

I want to join 2 tables, but I need to use 3th to set a relation between them.
Each Order is prepared by an Employee. I want to find OrderDetails only for specific employees. To check that, I need to find who was assigned to shop orders.
I'm almost sure that I know how to do it in SQL. Just concept query:
SELECT OrderDetails.payment_id FROM OrderDetais
INNER JOIN ShopOrders ON ShopOrders.id = OrderDetails.shop_order_id
INNER JOIN Employees ON Employees.id = ShopOrders.employee_id
WHERE Employees.id IN (5, 6, 8)
I'm just trying to write the same in Rails:
query = OrderDetail
.where('order_details.created_at > ? AND order_details.created_at < ?', 1.year.ago, 2.days.ago)
query = query.joins(:shop_order)
I use to_sql to check if my query looks good. Everything goes ok, until I join query.joins(: employee)
query = OrderDetail
.where('order_details.created_at > ? AND order_details.created_at < ?', 1.year.ago, 2.days.ago)
query = query.joins(:shop_order)
query = query.joins(:employee)
then I receive an error:
ActiveRecord::ConfigurationError: Can't join 'OrderDetail' to association named 'employee'; perhaps you misspelled it?>
I tried to follow Joining Nested Associations (Multiple Level). But I don't know how to use it properly.
In OrderDetail model there is:
delegate :employee, to: :shop_order, allow_nil: true
In ShopOrder model:
belongs_to :employee, optional: false
has_many :order_details, dependent: :restrict_with_error
And Employee model:
has_many :shop_orders
has_many :order_details, through: :shop_orders
I fixed it like that:
query.joins(:shop_order)
.joins('JOIN employees ON employees.id = shop_orders.employee_id')
.where(employees: { id: [5, 6, 8] })
Basing on the 'Joining Nested Associations (Multiple Level)' reference you provided the following should do what you want:
query
.joins(shop_orders: :employee)
.where(employees: { id: [5, 6, 8] })

Write and Activerecord join query

I'm writing an activerecord join query but It doesn't work.
I have these two classes
class User
belongs_to :store, required: true
end
class Store < ActiveRecord::Base
has_many :users, dependent: :nullify
has_one :manager, -> { where role: User.roles[:manager] }, class_name: 'User'
end
I need to get all the stores with a manager and all the stores without a manager.
I write these two queries
Store.includes(:users).where('users.role <> ?', User.roles[:manager]).references(:users).count
Store.includes(:users).where('users.role = ?', User.roles[:manager]).references(:users).count
and the result is
2.2.1 :294 > Store.includes(:users).where('users.role <> ?', User.roles[:manager]).references(:users).count
(6.6ms) SELECT COUNT(DISTINCT "stores"."id") FROM "stores" LEFT OUTER JOIN "users" ON "users"."store_id" = "stores"."id" WHERE (users.role <> 1)
=> 201
2.2.1 :295 > Store.includes(:users).where('users.role = ?', User.roles[:manager]).references(:users).count
(4.0ms) SELECT COUNT(DISTINCT "stores"."id") FROM "stores" LEFT OUTER JOIN "users" ON "users"."store_id" = "stores"."id" WHERE (users.role = 1)
=> 217
Now I know that I have 219 stores, and using
with_manager = 0
without_manager = 0
Store.all.each do |s|
if s.manager.present?
with_manager = with_manager +1
else
without_manager = without_manager +1
end
end
I know also that I have 217 stores with manager and 2 store without manager. One query is working, the second (stores without manager) fails.
So I must fix the query, but I cannot understand how can I fix it...
Usually I use the following thing for, getting al stores with a manager:
Store.joins(:manager)
Joins will use a inner join and not a left join that is includes case.
In the opposite case, that's tricky, but I do in this way:
Store.includes(:manager).where(users: { id: nil })
It's a left join with manager and getting all stores without a user included.

Rails/ActiveRecord: Eager loading join table entries via a composite primary key

I'm having issues eager loading a join table with ActiveRecord in Rails (4.2.4) (w/MySQL). The issue is that the join table makes use of a composite primary key, which is causing some weird? behaviour with the eager load. Here's an example to clarify:
class Student < ActiveRecord::Base
has_many :course_student_joins
has_many :courses, through: :course_student_joins
end
class Course < ActiveRecord::Base
has_many :course_student_joins
has_many :students, through: :course_student_joins
end
class CourseStudentJoin < ActiveRecord::Base
belongs_to :course
belongs_to :student
end
Table: courses
id name
1 Course 1
2 Course 2
Table: students
id name
1 Joey
2 Johnny
3 Dee Dee
Table: course_student_joins
course_id student_id extra_id
1 1 5
2 2 6
So course_student_joins is a classic many to many join table. Here's the interesting bit, if the PRIMARY KEY on the course_student_joins table consists of just course_id (single field), this works:
> Course.eager_load(:course_student_joins).first.course_student_joins
Result:
SQL (0.2ms) SELECT `courses`.`id` AS t0_r0, `courses`.`name` AS t0_r1, `course_student_joins`.`course_id` AS t1_r0, `course_student_joins`.`student_id` AS t1_r1, `course_student_joins`.`extra_id` AS t1_r2 FROM `courses` LEFT OUTER JOIN `course_student_joins` ON `course_student_joins`.`course_id` = `courses`.`id` WHERE `courses`.`id` = 1 AND `courses`.`id` IN (1)
=> #<ActiveRecord::Associations::CollectionProxy [#<CourseStudentJoin course_id: 1, student_id: 1, extra_id: 5>]>
If the PRIMARY KEY is changed (database side) to a composite key of (course_id, student_id), which is what we want (as a course can't have the same student twice and the primary key needs to be unique), this is what we get:
> Course.eager_load(:course_student_joins).find_by(id:1).course_student_joins
Result:
SQL (0.2ms) SELECT `courses`.`id` AS t0_r0, `courses`.`name` AS t0_r1, `course_student_joins`.`course_id` AS t1_r0, `course_student_joins`.`student_id` AS t1_r1, `course_student_joins`.`extra_id` AS t1_r2 FROM `courses` LEFT OUTER JOIN `course_student_joins` ON `course_student_joins`.`course_id` = `courses`.`id` WHERE `courses`.`id` = 1 AND `courses`.`id` IN (1)
=> #<ActiveRecord::Associations::CollectionProxy []>
Note the empty collection in the second result, and the SQL that remains exactly the same in both (expected).
We've played with foreign_key, primary_key settings on the has_many declarations, and even a 'solution' that changes the loading to reflect how it was performed in rails 2.0 and below. Also played with includes/references as well. No luck, any/all help appreciated.
The end goal is to come up with an ActiveRecord collection of courses with eagerly joined students and the entries in the join table, as we need to operate off of some values in the join table within an ActiveModel::Serializer which we will be passing the collection through.
The most Rails-friendly method would be an auto-incrementing id integer primary key + a unique index on (course_id, student_id).
Is there any reason you would want to be loading and the CourseStudentJoin records? It seems that you wouldn't want to manipulate them directly, anyways, and would be better off doing things like
Course.includes(:students).where(active: true, category: "Math").foo.bar
Student.includes(:courses).where(state: "CA").foo.bar
Feels like you don't need a primary key for a join table. You can just specify id: false on the migration file.
Also be mindful of changing database parameters () without telling rails schema about these changes, you can update the schema by dry running rake db:migrate
The last thing you are looking for is HABTM relationship. has_and_belongs_to_many: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_and_belongs_to_many
All you need to is:
student = Student.find(1)
student.courses
Follow up. Having a single, auto-incremeneting column as a primary key which would never be used, didn't sit well. The composite_primary_keys gem does affect the eager_load mechanism within ActiveRecord and resolves the issue. We hadn't considered it as we don't require the primary functionality of the gem, but it does solve this issue in the most acceptable manner.

How to order records by their latest child records attribute

I'm having troubles to order my records by their has_one association. I'm quite sure the solution is obvious, but I just can't get it.
class Migration
has_many :checks
has_one :latest_origin_check, -> { where(origin: true).order(at: :desc) }, class_name: 'Check'
end
class Check
belongs_to :migration
end
If I order by checks.status I always get different check ids. Shouldn't they be the same but with different order?
Or is the -> { } way to get the has_one association the problem?
Migration.all.includes(:latest_origin_check).order("checks.status DESC").each do |m| puts m.latest_origin_check.id end
So in one sentence: How do I order records through a custom has_one association?
I'm using Ruby 2.0.0, Rails 4.2 and PostgreSQL.
Update:
I wasn't specific enough. I've got two has_one relations on the checks relation.
Also very Important. One Migration has a way to big number of checks to include all the checks at once. So Migration.first.includes(:checks) would be very slow. We are talking about serveral thousand and I only need the latest.
class Migration
has_many :checks
has_one :latest_origin_check, -> { where(origin: true).order(at: :desc) }, class_name: 'Check'
has_one :latest_target_check, -> { where(origin: false).order(at: :desc) }, class_name: 'Check'
end
class Check
belongs_to :migration
end
Now if I get the latest_origin_check, I get the correct Record. The query is the following.
pry(main)> Migration.last.latest_origin_check
Migration Load (1.1ms) SELECT "migrations".* FROM "migrations" ORDER BY "migrations"."id" DESC LIMIT 1
Check Load (0.9ms) SELECT "checks".* FROM "checks" WHERE "checks"."migration_id" = $1 AND "checks"."origin" = 't' ORDER BY "checks"."at" DESC LIMIT 1 [["migration_id", 59]]
How do I get the latest check of each migration and then sort the migrations by a attribute of the latest check?
I'm using ransack. Ransack seems to get it right when I order the records by "checks.at"
SELECT "migrations".* FROM "migrations" LEFT OUTER JOIN "checks" ON "checks"."migration_id" = "migrations"."id" AND "checks"."origin" = 't' WHERE (beginning between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000' or ending between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000') ORDER BY "checks"."at" ASC
But the same query returns wrong results when I order by status
SELECT "migrations".* FROM "migrations" LEFT OUTER JOIN "checks" ON "checks"."migration_id" = "migrations"."id" AND "checks"."origin" = 't' WHERE (beginning between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000' or ending between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000') ORDER BY "checks"."status" ASC
Check.status is a boolean, check.at is a DateTime. A colleague suggested that the boolean is the problem. Do I need to convert the booleans to an integer to make them sortable? How do I do that only for the :latest_origin_check? Something like that?
.order("(case when \"checks\".\"status\" then 2 when \"checks\".\"status\" is null then 0 else 1 end) DESC")
You already have a has_many relationship with Check on Migration. I think you are looking for a scope instead:
scope :latest_origin_check, -> { includes(:checks).where(origin:true).order("checks.status DESC").limit(1)}
Drop the has_one :latest_origin_check line on Migration.
Migration.latest_origin_check
I think the line about should return your desired result set.

Rails ActiveRecord query using multiple joins involving polymorphic association

I'm trying to figure out how I can replicate the following SQL query using AR given the model definitions below. The cast is necessary to perform the average. The result set should group foo by bar (which comes from the polymorphic association). Any help is appreciated.
SQL:
SELECT AVG(CAST(r.foo AS decimal)) "Average", s.bar
FROM rotation r INNER JOIN cogs c ON r.cog_id = c.id
INNER JOIN sprockets s ON s.id = c.crankable_id
INNER JOIN machinists m ON r.machinist_id = m.id
WHERE c.crankable_type = 'Sprocket' AND
r.machine_id = 123 AND
m.shop_id = 1
GROUP BY s.bar
ActiveRecord Models:
class Rotation < ActiveRecord::Base
belongs_to :cog
belongs_to :machinist
belongs_to :machine
end
class Cog < ActiveRecord::Base
belongs_to :crankable, :polymorphic => true
has_many :rotation
end
class Sprocket < ActiveRecord::Base
has_many :cogs, :as => :crankable
end
class Machinist < ActiveRecord::Base
belongs_to :shop
end
UPDATE
I've figured out a way to make it work, but it feels like cheating. Is there are a better way than this?
Sprocket.joins('INNER JOIN cogs c ON c.crankable_id = sprockets.id',
'INNER JOIN rotations r ON r.cog_id = c.id',
'INNER JOIN machinists m ON r.machinist_id = m.id')
.select('sprockets.bar', 'r.foo')
.where(:r => {:machine_id => 123}, :m => {:shop_id => 1})
.group('sprockets.bar')
.average('CAST(r.foo AS decimal)')
SOLUTION
Albin's answer didn't work as-is, but did lead me to a working solution. First, I had a typo in Cog and had to change the relation from:
has_many :rotation
to the plural form:
has_many :rotations
With that in place, I am able to use the following query
Sprocket.joins(cogs: {rotations: :machinist})
.where({ machinists: { shop_id: 1 }, rotations: { machine_id: 123}})
.group(:bar)
.average('CAST(rotations.foo AS decimal)')
The only real difference is that I had to separate the where clause since a machine does not belong to a machinist. Thanks Albin!
I think this code is a little simpler and taking more help from AR
Sprocket
.joins(cogs: {rotations: :machinist})
.where({ machinists: { machine_id: 123, shop_id: 1 } } )
.group(:bar)
.average('CAST(rotations.foo AS decimal)')
The select clause was unnecessary, you don't have to select values since you only need them internally in the query, AR helps you decide what you need afterwards.
I tested this out using a similar structure in one of my own projects but it is not the exact same models so there might be a typo or something in there if it does not run straight up. I ran:
Activity
.joins(locations: {participants: :stuff})
.where({ stuffs: { my_field: 1 } })
.group(:title)
.average('CAST(participants.date_of_birth as decimal)')
producing this query
SELECT AVG(CAST(participants.date_of_birth as decimal)) AS average_cast_participants_date_of_birth_as_decimal, title AS title
FROM `activities`
INNER JOIN `locations` ON `locations`.`activity_id` = `activities`.`id`
INNER JOIN `participants` ON `participants`.`location_id` = `locations`.`id`
INNER JOIN `stuffs` ON `stuffs`.`id` = `participants`.`stuff_id`
WHERE `stuffs`.`my_field` = 1
GROUP BY title
which AR makes in to a hash looking like this:
{"dummy title"=>#<BigDecimal:7fe9fe44d3c0,'0.19652273E4',18(18)>, "stats test"=>nil}

Resources