Count and Select Object in ActiveRecord with 1 query - sql

We have objects that we want to represent in stacks (think of stacking items in an MMO). There will be duplicate rows.
Let's say our owned_objects table looks like this.
user_id | object_id
1 | 27
1 | 27
3 | 46
3 | 46
5 | 59
I want the query to do
SELECT
user_id,
object_id,
count(*) AS count
FROM owned_objects
GROUP BY
user_id,
object_id;
And return either the 3 distinct OwnedObjects (or even just getting the distinct Objects would work too) and a count associated with it.
I know this is possible with SQLAlchemy, but can you do it with ActiveRecord?

How about …
#objects = user.objects.all(:select => "count(*) as count, objects.*", :group => :object_id)
… or similar?
You can then retrieve the counts by a dynamically created attribute on each object:
#object.first.count # the "stack depth" of the first object.
This assumes either a has_and_belongs_to_many :objects or a has_many :objects, :through => :owned_objects on user.

Found a solution, but not sure if it's the cleanest (hope it isn't).
Basically I created a SQL view that does that query and created a model for it. There's a plugin for rails that recognizes views on migrations.

Related

Rails ActiveRecord only return distinct records based on a column

Suppose I have an ActiveRecord model called Checkin and I only want to return Checkin records distinct by the user_id. Is there way to do this that returns an AR relation? I need to apply multiple scopes to it so I would prefer to avoid find_by_sql.
Example:
Let's say we have the following records
id: 1
user_id : 5
location_id: 12
id: 2
user_id: 25
location_id: 12
id: 3
user_id: 5
location_id: 12
I want to be able to say something like:
Checkin.distinct_by(:user_id) and have that return only rows 1 and 2 (because user_id=5 should be distinct). I would like this to be an ActiveRecord:Relation ideally so that I can apply other conditions onto this.
I think you should make your question more clear, can you provide some example? I don't know this is what you are looking for: you can try to use "select" or "collect" function after your where statement.

Querying a has_many association's count in Rails 3

I've been going over a tonne of StackOverflow articles trying to work out a particularly tricky Rails 3 join query to no avail - so I'm asking a new question!
I have a model called "User" which has_many "Checks" via a polymorphic association (the actual columns on Check are "target_type" and "target_id"). The Check has a string column called "type", which denotes the reason for the check. Something like this:
-----------------------------------------------------
id | target_type | target_id | type |
-----------------------------------------------------
1 User 1 type_1
2 User 2 type_2
I want to find all users who don't have a check associated with them with a set type - so it's a join and a count, I think.
So for instance, I want to be able to make a query to find all users who have no "type_1" checks, and it should return the user with id #2.
How would I go about doing this?
(I've been looking at all the stuff around counts and grouping (e.g. Rails has_many association count child rows) but nothing seems to quite match.)
Thanks!
You can try sth like:
class Merchant < ActiveRecord::Base
...
def self.non_fraudulent
includes(:fraud_checks).group("#{self.table_name}.id").having("SUM(CASE WHEN fraud_checks.type = 'fraudulent_ip' THEN 1 ELSE 0 END) = 0")
end
end

how to do a search across many to many tables with rails 3

I have three tables containing details of moulds (molds) and their patterns linked together via a genres table :
Mould table
id : integer
name : string
Pattern table
id : integer
name : string
genres table
id : integer
mould_id :integer
pattern_id : integer
I have data in the tables as follows
Mould table
ID Name
1 A1
2 A2
3 A3
53 A4
54 A5
197 A6
198 A7
1204 A8
1205 A9
Pattern Table
ID Name
1 Running
2 Scroll
Genres Table
ID mould_id pattern_id
1 1 1
2 2 1
3 3 1
4 53 1
5 53 2
6 54 1
7 197 2
8 198 1
9 1204 2
10 1205 1
From my calcs, Moulds 53, 197 and 1204 all have a pattern of Scroll so I should be able to write the following SQL to give that
SELECT m.id FROM moulds m INNER JOIN genres g
ON m.id = g.mould_id INNER JOIN patterns p
ON g.pattern_id = p.id
WHERE p.id = 2
GROUP BY m.id
HAVING COUNT(*) >= 1
But it only returns 197 So something is wrong there (or with my data)
Then converting it to Rails I wrap it up in a find_by_sql which gives the same result (which makes sense)
Can someone help with the SQL or suggest a better, rails way of doing this sort of query?
I've tested the models and data that you have above and I get the result you expect (i.e. the three moulds with names "A4", "A6" and "A8"). So I suspect something is going on with your data.
That aside, I'd suggest using Rails query methods to get the desired data rather than resorting to SQL. For example, this:
Mould.select('moulds.id')\
.joins(:patterns)\
.where('patterns.id' => 2)\
.group('moulds.id')\
.having('COUNT(*) >= ?', 1)
will generate this SQL:
SELECT moulds.id FROM "moulds"
INNER JOIN "genres" ON "genres"."mould_id" = "moulds"."id"
INNER JOIN "patterns" ON "patterns"."id" = "genres"."pattern_id"
WHERE "patterns"."id" = 2
GROUP BY moulds.id
HAVING COUNT(*) >= 1
This is pretty much the same as the SQL you have, and produces the same result (again, the correct one with three results).
Note that the INNER JOIN genres g part of your SQL comes out here without explicitly mentioning it. This works as long as you have through associations defined on Mould and Pattern:
class Mould < ActiveRecord::Base
has_many :genres
has_many :patterns, :through => :genres
#...
end
class Pattern < ActiveRecord::Base
has_many :genres
has_many :moulds, :through => :genres
#...
end
Since you tell Rails that Mould and Pattern are associated through the genres table, it knows how to build the correct SQL query to join them together.
Hope that helps.

Grouped aggregations with Yii STAT?

I have a Yii STAT Relation that's defined to provide a grouped SUM result, however when I access the relation in my View, the only value is the latest single value rather than each value.
For example, here's my relation:
'total_salaries_by_job' => array(
self::STAT,
'Employee',
'department_id',
'select' => 'job_type_id, SUM(salary)',
'group'=>"job_type_id"
)
This generates the following SQL:
SELECT
department_id AS c
, job_type_id
, SUM(salary) AS s
FROM Employee AS t
WHERE t.department_id = 1
GROUP BY
department_id
, job_type_id
Running that manually, the result set is:
c | job_type_id | s
------+----------------+---------
1 | 1 | 233000
------+----------------+---------
1 | 2 | 25000
------+----------------+---------
1 | 3 | 179000
However, in my view, if I do the following:
<pre>
<?php print_r($department->total_salaries_by_job); ?>
</pre>
The result is simply: 179000, whereas I was expecting it to be an array with 3 elements.
Is returning just 1 value the way STAT relations work or is there something else I need to be doing?
Is it possible to do what I'm attempting?
You can do what you are after, but you can't use a STAT relationship to do it. Rather, use a Normal HAS_MANY relationship and use your same select statement.

PostgreSQL GROUP BY different from MySQL?

I've been migrating some of my MySQL queries to PostgreSQL to use Heroku. Most of my queries work fine, but I keep having a similar recurring error when I use group by:
ERROR: column "XYZ" must appear in the GROUP BY clause or be used in
an aggregate function
Could someone tell me what I'm doing wrong?
MySQL which works 100%:
SELECT `availables`.*
FROM `availables`
INNER JOIN `rooms` ON `rooms`.id = `availables`.room_id
WHERE (rooms.hotel_id = 5056 AND availables.bookdate BETWEEN '2009-11-22' AND '2009-11-24')
GROUP BY availables.bookdate
ORDER BY availables.updated_at
PostgreSQL error:
ActiveRecord::StatementInvalid: PGError: ERROR: column
"availables.id" must appear in the GROUP BY clause or be used in an
aggregate function:
SELECT "availables".* FROM "availables" INNER
JOIN "rooms" ON "rooms".id = "availables".room_id WHERE
(rooms.hotel_id = 5056 AND availables.bookdate BETWEEN E'2009-10-21'
AND E'2009-10-23') GROUP BY availables.bookdate ORDER BY
availables.updated_at
Ruby code generating the SQL:
expiration = Available.find(:all,
:joins => [ :room ],
:conditions => [ "rooms.hotel_id = ? AND availables.bookdate BETWEEN ? AND ?", hostel_id, date.to_s, (date+days-1).to_s ],
:group => 'availables.bookdate',
:order => 'availables.updated_at')
Expected Output (from working MySQL query):
+-----+-------+-------+------------+---------+---------------+---------------+
| id | price | spots | bookdate | room_id | created_at | updated_at |
+-----+-------+-------+------------+---------+---------------+---------------+
| 414 | 38.0 | 1 | 2009-11-22 | 1762 | 2009-11-20... | 2009-11-20... |
| 415 | 38.0 | 1 | 2009-11-23 | 1762 | 2009-11-20... | 2009-11-20... |
| 416 | 38.0 | 2 | 2009-11-24 | 1762 | 2009-11-20... | 2009-11-20... |
+-----+-------+-------+------------+---------+---------------+---------------+
3 rows in set
MySQL's totally non standards compliant GROUP BY can be emulated by Postgres' DISTINCT ON. Consider this:
MySQL:
SELECT a,b,c,d,e FROM table GROUP BY a
This delivers 1 row per value of a (which one, you don't really know). Well actually you can guess, because MySQL doesn't know about hash aggregates, so it will probably use a sort... but it will only sort on a, so the order of the rows could be random. Unless it uses a multicolumn index instead of sorting. Well, anyway, it's not specified by the query.
Postgres:
SELECT DISTINCT ON (a) a,b,c,d,e FROM table ORDER BY a,b,c
This delivers 1 row per value of a, this row will be the first one in the sort according to the ORDER BY specified by the query. Simple.
Note that here, it's not an aggregate I'm computing. So GROUP BY actually makes no sense. DISTINCT ON makes a lot more sense.
Rails is married to MySQL, so I'm not surprised that it generates SQL that doesn't work in Postgres.
PostgreSQL is more SQL compliant than MySQL. All fields - except computed field with aggregation function - in the output must be present in the GROUP BY clause.
MySQL's GROUP BY can be used without an aggregate function (which is contrary to the SQL standard), and returns the first row in the group (I don't know based on what criteria), while PostgreSQL must have an aggregate function (MAX, SUM, etc) on the column, on which the GROUP BY clause is issued.
Correct, the solution to fixing this is to use :select and to select each field that you wish to decorate the resulting object with and group by them.
Nasty - but it is how group by should work as opposed to how MySQL works with it by guessing what you mean if you don't stick fields in your group by.
If I remember correctly, in PostgreSQL you have to add every column you fetch from the table where the GROUP BY clause applies to the GROUP BY clause.
Not the prettiest solution, but changing the group parameter to output every column in model works in PostgreSQL:
expiration = Available.find(:all,
:joins => [ :room ],
:conditions => [ "rooms.hotel_id = ? AND availables.bookdate BETWEEN ? AND ?", hostel_id, date.to_s, (date+days-1).to_s ],
:group => Available.column_names.collect{|col| "availables.#{col}"},
:order => 'availables.updated_at')
According to MySQL's "Debuking GROUP BY Myths" http://dev.mysql.com/tech-resources/articles/debunking-group-by-myths.html. SQL (2003 version of the standard) doesn't requires columns referenced in the SELECT list of a query to also appear in the GROUP BY clause.
For others looking for a way to order by any field, including joined field, in postgresql, use a subquery:
SELECT * FROM(
SELECT DISTINCT ON(availables.bookdate) `availables`.*
FROM `availables` INNER JOIN `rooms` ON `rooms`.id = `availables`.room_id
WHERE (rooms.hotel_id = 5056
AND availables.bookdate BETWEEN '2009-11-22' AND '2009-11-24')
) AS distinct_selected
ORDER BY availables.updated_at
or arel:
subquery = SomeRecord.select("distinct on(xx.id) xx.*, jointable.order_field")
.where("").joins(")
result = SomeRecord.select("*").from("(#{subquery.to_sql}) AS distinct_selected").order(" xx.order_field ASC, jointable.order_field ASC")
I think that .uniq [1] will solve your problem.
[1] Available.select('...').uniq
Take a look at http://guides.rubyonrails.org/active_record_querying.html#selecting-specific-fields