Rails where on unusual subquery - ruby-on-rails

I already know how to use Rails to create subquery within a where condition, like so:
Order.where(item_id: Item.select(:id).where(user_id: 10))
However, my case is a little bit more tricky as you'll see. I'm trying to convert this query:
Post.find_by_sql(
<<-SQL
SELECT posts.*
FROM posts
WHERE (
SELECT name
FROM moderation_events
WHERE moderable_id = posts.id
AND moderable_type = 'Post'
ORDER BY created_at DESC
LIMIT 1
) = 'reported'
SQL
)
into an ActiveRecord/Arel-like(ish) call but couldn't find a way so far, therefore the raw SQL code and the use of find_by_sql.
I'm wondering if anyone out there already faced the same issue and if there's a better way to write this query ?
EDIT
The raw query above is working and returns exactly the result I want. I'm using PostgreSQL.
Post model
class Post < ApplicationRecord
has_many :moderation_events, as: :moderable, dependent: :destroy, inverse_of: :moderable
end
ModerationEvent model
class ModerationEvent < ApplicationRecord
belongs_to :moderable, polymorphic: true
belongs_to :post, foreign_key: :moderable_id, inverse_of: :moderation_events
end
EDIT 2
I had tried to used Rails associations to query it, using includes, joins and the like. However, the query above is very specific and work well with that form. Altering it with a JOIN query does not return the expected results.
The ORDER and LIMIT statement are very important here and cannot be moved outside of it.
A post can have multiple moderation_events. A moderation event can have multiple name (a.k.a type): reported, validated, moved and deleted.
Here is what the query is doing:
Getting all posts having their last moderation event to be a 'reported' event
I'm not trying to alter the query above because it does works well and fast in our case. I'm just trying to convert it in a more active record fashion without changing it, if possible

Related

has_one association not working with includes

I've been trying to figure out some odd behavior when combining a has_one association and includes.
class Post < ApplicationRecord
has_many :comments
has_one :latest_comment, -> { order('comments.id DESC').limit(1) }, class_name: 'Comment'
end
class Comment < ApplicationRecord
belongs_to :post
end
To test this I created two posts with two comments each. Here are some rails console commands that show the odd behavior. When we use includes then it ignores the order of the latest_comment association.
posts = Post.includes(:latest_comment).references(:latest_comment)
posts.map {|p| p.latest_comment.id}
=> [1, 3]
posts.map {|p| p.comments.last.id}
=> [2, 4]
I would expect these commands to have the same output. posts.map {|p| p.latest_comment.id} should return [2, 4]. I can't use the second command because of n+1 query problems.
If you call the latest comment individually (similar to comments.last above) then things work as expected.
[Post.first.latest_comment.id, Post.last.latest_comment.id]
=> [2, 4]
If you have another way of achieving this behavior I'd welcome the input. This one is baffling me.
I think the cleanest way to make this work with PostgreSQL is to use a database view to back your has_one :latest_comment association. A database view is, more or less, a named query that acts like a read-only table.
There are three broad choices here:
Use lots of queries: one to get the posts and then one for each post to get its latest comment.
Denormalize the latest comment into the post or its own table.
Use a window function to peel off the latest comments from the comments table.
(1) is what we're trying to avoid. (2) tends to lead to a cascade of over-complications and bugs. (3) is nice because it lets the database do what it does well (manage and query data) but ActiveRecord has a limited understanding of SQL so a little extra machinery is needed to make it behave.
We can use the row_number window function to find the latest comment per-post:
select *
from (
select comments.*,
row_number() over (partition by post_id order by created_at desc) as rn
from comments
) dt
where dt.rn = 1
Play with the inner query in psql and you should see what row_number() is doing.
If we wrap that query in a latest_comments view and stick a LatestComment model in front of it, you can has_one :latest_comment and things will work. Of course, it isn't quite that easy:
ActiveRecord doesn't understand views in migrations so you can try to use something like scenic or switch from schema.rb to structure.sql.
Create the view:
class CreateLatestComments < ActiveRecord::Migration[5.2]
def up
connection.execute(%q(
create view latest_comments (id, post_id, created_at, ...) as
select id, post_id, created_at, ...
from (
select id, post_id, created_at, ...,
row_number() over (partition by post_id order by created_at desc) as rn
from comments
) dt
where dt. rn = 1
))
end
def down
connection.execute('drop view latest_comments')
end
end
That will look more like a normal Rails migration if you're using scenic. I don't know the structure of your comments table, hence all the ...s in there; you can use select * if you prefer and don't mind the stray rn column in your LatestComment. You might want to review your indexes on comments to make this query more efficient but you'd be doing that sooner or later anyway.
Create the model and don't forget to manually set the primary key or includes and references won't preload anything (but preload will):
class LatestComment < ApplicationRecord
self.primary_key = :id
belongs_to :post
end
Simplify your existing has_one to just:
has_one :latest_comment
Maybe add a quick test to your test suite to make sure that Comment and LatestComment have the same columns. The view won't automatically update itself as the comments table changes but a simple test will serve as a reminder.
When someone complains about "logic in the database", tell them to take their dogma elsewhere as you have work to do.
Just so it doesn't get lost in the comments, your main problem is that you're abusing the scope argument in the has_one association. When you say something like this:
Post.includes(:latest_comment).references(:latest_comment)
the scope argument to has_one ends up in the join condition of the LEFT JOIN that includes and references add to the query. ORDER BY doesn't make sense in a join condition so ActiveRecord doesn't include it and your association falls apart. You can't make the scope instance-dependent (i.e. ->(post) { some_query_with_post_in_a_where... }) to get a WHERE clause into the join condition, then ActiveRecord will give you an ArgumentError because ActiveRecord doesn't know how to use an instance-dependent scope with includes and references.

ORDER BY and DISTINCT ON (...) in Rails

I am trying to ORDER by created_at and then get a DISTINCT set based on a foreign key.
The other part is to somehow use this is ActiveModelSerializer. Specifically I want to be able to declare:
has_many :somethings
In the serializer. Let me explain further...
I am able to get the results I need with this custom sql:
def latest_product_levels
sql = "SELECT DISTINCT ON (product_id) client_product_levels.product_id,
client_product_levels.* FROM client_product_levels WHERE client_product_levels.client_id = #{id} ORDER BY product_id,
client_product_levels.created_at DESC";
results = ActiveRecord::Base.connection.execute(sql)
end
Is there any possible way to get this result but as a condition on a has_many relationship so that I can use it in AMS?
In pseudo code: #client.products_levels
Would do something like: #client.order(created_at: :desc).select(:product_id).distinct
That of course fails for reasons that are beyond me.
Any help would be great.
Thank you.
A good way to structure this is to split your query into two parts: the first part manages the filtering of rows so that you get only your latest client product levels. The second part uses a standard has_many association to connect Client with ClientProductLevel.
Starting with your ClientProductLevel model, you can create a scope to do the latest filtering:
class ClientProductLevel < ActiveRecord::Base
scope :latest, -> {
select("distinct on(product_id) client_product_levels.product_id,
client_product_levels.*").
order("product_id, created_at desc")
}
end
You can use this scope anywhere that you have a query that returns a list of ClientProductLevel objects, e.g., ClientProductLevel.latest or ClientProductLevel.where("created_at < ?", 1.week.ago).latest, etc.
If you haven't already done so, set up your Client class with a has_many relationship:
class Client < ActiveRecord::Base
has_many :client_product_levels
end
Then in your ActiveModelSerializer try this:
class ClientSerializer < ActiveModel::Serializer
has_many :client_product_levels
def client_product_levels
object.client_product_levels.latest
end
end
When you invoke the ClientSerializer to serialize a Client object, the serializer sees the has_many declaration, which it would ordinarily forward to your Client object, but since we've got a locally defined method by that name, it invokes that method instead. (Note that this has_many declaration is not the same as an ActiveRecord has_many, which specifies a relationship between tables: in this case, it's just saying that the serializer should present an array of serialized objects under the key `client_product_levels'.)
The ClientSerializer#client_product_levels method in turn invokes the has_many association from the client object, and then applies the latest scope to it. The most powerful thing about ActiveRecord is the way it allows you to chain together disparate components into a single query. Here, the has_many generates the `where client_id = $X' portion, and the scope generates the rest of the query. Et voila!
In terms of simplification: ActiveRecord doesn't have native support for distinct on, so you're stuck with that part of the custom sql. I don't know whether you need to include client_product_levels.product_id explicitly in your select clause, as it's already being included by the *. You might try dumping it.

Includes not allowing outer join and count on child table

class Course < ActiveRecord::Base
has_many :registrations
delegate :count, to: :registrations, prefix: true
end
class Registration < ActiveRecord::Base
belongs_to :course
end
In my Courses index view I show a count of Registrations for each record. What is best practice for this? Using includes(:registrations) with the delegated registrations_count method looks slower (in the console) than doing a database count in the main query.
I want to show all records, so I can't use an INNER join(). Using includes() as below gives the error PG::GroupingError: ERROR: column "registrations.id" must appear in the GROUP BY clause or be used in an aggregate function I tried adding a where clause on :registrations to this but it still errored:
Course.includes(:registrations).group("courses.id").select("courses.*, COUNT(registrations.id) as registrations_count")
Is it right to have to specify the outer join as follows?
Course.joins('LEFT OUTER JOIN registrations ON registrations.course_id = courses.id').select('courses.*, COUNT(registrations.id) as registrations_count').group('courses.id')
This last query does work but it feels what I'm doing should be fairly standard so I'd like to be sure I'm taking the right approach in general.
In my Courses index view I show a count of Registrations for each record. What is best practice for this?
counter_caching is the best option for counting association objects. A counter cache in Rails is just an additional column that tracks the number of associated models.
You just have to do this
belongs_to :course , counter_cache: true
And then you can simply do this
#course.registrations_count
Learn more in RailsCast
Looks like you are missing the difference between includes vs joins, Learn here
For me this is good.
Course.joins('LEFT OUTER JOIN registrations ON registrations.course_id = courses.id').select('courses.*, COUNT(registrations.id) as registrations_count').group('courses.id')

Rails belongs_to with conditions on originating table

I have a Transaction model where from_owner is polymorphic, because the transaction could come from several other models.
class Transaction < ActiveRecord::Base
belongs_to :from_owner, polymorphic: true
end
I am trying to set up a specific belongs_to for when from_owner_type is a particular value:
belongs_to :from_person,
conditions: ['from_owner_type = ?', Person.name],
class_name: Person,
foreign_key: 'from_owner_id'
The problem I'm encountering is that the conditions seem to be for Person and not Transaction. So I get the following SQL error trying to call from_person on a Transaction:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: from_owner_type: SELECT "people".* FROM "people" WHERE "people"."id" = 1 AND (from_owner_type = 'Person') LIMIT 1
What I want is for from_person on a Transaction to return nil if the Transaction from_owner_type is not Person, and otherwise return the related Person. I could set up a custom from_person method that does this, but I thought it might be possible as a belongs_to. I'm wanting to use this with CanCan conditions. I'm using Rails 3.
From your comments, it seems that the aim of this is to be able to set up a CanCan rule that allows a user to :read any Transactions that they are the owner of, correct? You should be able to do that with the following rule:
can :read, Transaction, from_owner_id: profile.id, from_owner_type: Person.name
and which should mean you don't need to bother messing anything with your Transaction model at all. (I haven't tested this, but the theory should be right, even if the syntax isn't quite there. For example, I'm not exactly sure where you profile.id comes from.)

Rails way to COUNT the nr of records in my case

I am using Rails v2.3.2.
I have a model called UsersCar:
class UsersCar < ActiveRecord::Base
belongs_to :car
belongs_to :user
end
This model mapped to a database table users_cars, which only contains two columns : user_id, car_id.
I would like to use Rails way to count the number of car_id where user_id=3. I konw in plain SQL query I can achieve this by:
SELECT COUNT(*) FROM users_cars WHERE user_id=3;
Now, I would like to get it by Rails way, I know I can do:
UsersCar.count()
but how can I put the ...where user_id=3 clause in Rails way?
According to the Ruby on Rails Guides, you can pass conditions to the count() method. For example:
UsersCar.count(:conditions => ["user_id = ?", 3])
will generates:
SELECT count(*) AS count_all FROM users_cars WHERE (user_id = 3)
If you have the User object, you could do
user.cars.size
or
user.cars.count
Another way would be to do:
UserCar.find(:user_id => 3).size
And the last way that I can think of is the one mentioned above, i.e. 'UserCar.count(conditions)'.
With the belogngs to association, you get several "magic" methods on the parent item to reference its children.
In your case:
users_car = UsersCar.find(1) #=>one record of users_car with id = 1.
users_car.users #=>a list of associated users.
users_car.users.count #=>the amount of associated users.
However, I think you are understanding the associations wrong, based on the fact that your UsersCar is named awkwardly.
It seems you want
User has_and_belongs_to_many :cars
Car has_and_belongs_to_manu :users
Please read abovementioned guide on associations if you want to know more about many-to-many associations in Rails.
I managed to find the way to count with condition:
UsersCar.count(:condition=>"user_id=3")

Resources