I'm trying to group a table (oauth_access_tokens) by application_id and select the highest record by ID from the corresponding group (see the complete model below). I've seen this post, which explains how to get the highest ID from the group, but sadly it's not working for my case.
I have a table called oauth_access_tokens with the following attributes: id, resource_owner_id, application_id, token, refresh_token, expires_in, scopes and revoked_at.
The method I have in my model:
def last_token
Doorkeeper::AccessToken.group(:application_id).having('id = MAX(id)')
end
After calling it like so: User.first.last_token (I made sure that there are a few records in the database present).
sql output:
Doorkeeper::AccessToken Load (0.5ms) SELECT `oauth_access_tokens`.* FROM `oauth_access_tokens` GROUP BY `oauth_access_tokens`.`application_id` HAVING id = MAX(id)
Output: #<ActiveRecord::Relation []>
Why don't I get the record with the highest ID? When I run User.first.last_token I expect to see the access_token with the id of 28.
Happy holidays!
Given that id is the primary key of :users table and oauth_access_tokens.resource_owner_id points to users.id, something like this should work:
class User < ApplicationRecord
...
def last_token
# 'id' (or, self.id) in 'resource_owner_id: id' points to the user id, i.e. 'User.first.id' in 'User.first.last_token'
Doorkeeper::AccessToken.group(:application_id).where(resource_owner_id: id).pluck('max(id)').first
end
...
end
Though the above solution should work, but you can improve the query writing to a more readable way by defining the associations inside corresponding models like below:
class User < ApplicationRecord
...
has_many :access_tokens,
class_name: 'Doorkeeper::AccessToken',
foreign_key: :resource_owner_id
...
def last_token
access_tokens.group(:application_id).pluck('max(id)').first
end
...
end
class Doorkeeper::AccessToken < ApplicationRecord
...
self.table_name = 'oauth_access_tokens'
belongs_to :resource_owner, class_name: 'User'
...
end
In both cases, User.first.last_token will return 28 as you would expect.
Update
You are only 1 query away to get the access_token instances instead of mere ids. The method will now return an ActiveRecord::Relation of Doorkeeper::AccessToken instances which meet the defined criteria.
def last_tokens # pluralized method name
token_ids = access_tokens.group(:application_id).pluck('max(id)')
Doorkeeper::AccessToken.where(id: token_ids)
end
Related
I have an UserProfile model:
class UserProfile < ApplicationRecord
after_update :check_changes
def check_changes
AuditRecord.create(account_id: self.id, fields: self.saved_changes , account_type: 'UserProfile', admin_id: 3) if self.saved_changes?
end
id
user_id
name
last_name
1
1
john
doe
2
2
foo
bar
and AuditRecord model:
id
account_type
account_id
field
admin_id
1
UserProfile
1
{}
3
2
UserProfile
2
{}
3
This AuditRecord saves all of the updates of the profiles, but could be updated by different admins.
How can I send to check_changes function the admin_id? Because right now always is going to be 3.
If I understand correctly, you just want to pass an admin_id to AuditRecord, which means you need another model Admin, which is related to AuditRecord in some way.
Start by generating Admin model, set up the appropriate active record association and references.
Rails docs for this
You should be able to access associations now, with something like admin.auditrecords depending upon how you set up the associations.
after_update is called automatically. You can't pass some variables there dynamically
But you can evenly distribute tasks between admins
class AuditRecord < ApplicationRecord
class << self
def most_free_admin_id
select(:admin_id).
group(:admin_id).
order('COUNT (admin_id)').
first.
admin_id
end
end
end
AuditRecord.most_free_admin_id will generate SQL like this
SELECT admin_id
FROM audit_records
GROUP BY admin_id
ORDER BY COUNT (admin_id)
LIMIT 1;
It will return id of the admin who has the least audit records
Also it's better to create such records not after update, but after commit when record is 100% saved in DB. For this purpose after_update_commit is better
Also in Ruby we use self. only in situations where it is impossible without it
Finally you can apply it in the model
class UserProfile < ApplicationRecord
after_update_commit :check_changes
private
def check_changes
AuditRecord.create(
account_id: id,
fields: saved_changes,
account_type: 'UserProfile',
admin_id: AuditRecord.most_free_admin_id
)
end
end
I have three models. User, Question and Attempt.
Each User has_many :attempts,
similarly, Question has_many :attempts
and Attempt belongs_to User and Question
Required:
What I want is a method to return a user's last attempt for every question that user has an associated attempt.
I mean if a user has attempted q1, q2 and has 3 attempts for each question, then my method should return last attempt of q1 and last attempt of q2.
Right now:
I have created a method in User model.
def last_attempts
return self.attempts.joins(:question).order("questions.id ASC ,attempts.updated_at DESC").select("DISTINCT ON (question_id) *")
end
Error:
This method returns duplicate attempts. (all attempts for every question of that user). all 3 attempts of q1 and all three attempts of q2
It looks like you have an unnecessary join in your query.
You should be able to do the following:
def questions_last_attempts
attempts
.select("DISTINCT ON (question_id) *")
.order(:question_id, created_at: :desc)
end
Which should generate the following SQL:
SELECT DISTINCT ON (question_id) * FROM "attempts" WHERE "attempts"."user_id" = 1 ORDER BY "attempts"."question_id" ASC, "attempts"."created_at" DESC
One way is to add a relationship between User and Question through Attempt.
Then map the user.questions ids to select each time the first ordered attempt:
class User < ApplicationRecord
has_many :attempts
has_many :questions, through: :attempts
def last_attempts
questions.order(:id).distinct.map do |question|
attempts.where(question_id: question.id).order(updated_at: :desc).first
end
end
end
I have 2 models(Clients and Projects) that are connected like this:
class Project < ActiveRecord::Base
belongs_to :cliente
end
class Cliente < ActiveRecord::Base
has_many :projects
end
Projects have a :cliente_id column in its schema, so if I do:
Project.cliente_id I will get the cliente_id correctly.
My doubt is, I want to get the client name from it's id, so I need something like:
Project.cliente_id.name
Which is the correct way to retrieve this info?
You find associated objects through the association:
project = Project.find(1) # Returns the full `project` object
project.cliente # Returns the full `cliente` object
project.cliente.name # Returns just the `name` attribute
project.cliente_id == project.cliente.id # Returns true
You can get the complete Cliente object with project.cliente (note that the _id is not used). So you can use it like a regular Cliente; for example, to get name just do:
project = Project.find(1)
project.cliente.name
I'm trying to eager load some associations, but I want to do filtering on some of the sub-relations here is what I mean.
class MyModel < ActiveRecord::Base
has_many :my_model_details, dependent: :destroy
end
class MyModelDetails < ActiveRecord::Base
belongs_to :property
belongs_to :my_model
belongs_to :track
belongs_to :person
end
So if I want to get all MyModel objects which have details that belong to certain property with property name I would do this.
MyModel.includes(my_model_details: [:property, :person]).where('property.property_name = ?', 'Property name')
The reason why I want use includes instead of joins, is that I want to have my model details in grouped by the last property and person name. So I don't want to fire the N+1 query in that case.
Ex
If my query returns single MyModel object with two MyModelDetail records.
Where mymodeldetail id 1 has property id 1 and mymodeldetail id2 has property id 2. And they both belong to the same person.
So on my UI I would display this:
MyModel 1
MyModelDetail 2 - Property name: 'some name' - Person: 'Bob'
Now what happens when I use my includes query is this :
MyModel.includes(my_model_details: [:property, :person]).where('property.property_name = ?', 'Property name')
SELECT "MY_MODEL".* FROM "MY_MODEL" WHERE (PROPERTY.PROPERTY_NAME = 'Prop')
If I use includes with where, join sql is not generated at all and so this query fails of course:
"PROPERTY"."PROPERTY_NAME": invalid identifier
Why is that, I'm using rails 4? How can I make it work with both joins and includes
I think it should be properties, because table name is pluralized according to CoC, try using properties.property_name instead
MyModel.includes(my_model_details: [:property, :person]).where('properties.property_name = ?', 'Property name')
As suggested by BroiSatse, add .references(:properties) to your query
Hope that helps!
My rating system is quite simple, either the user like or he doesn't like the article.
Basically, I have something like this and it works perfectly :
class Article
has_many :ratings
def self.by_ratings
all.sort_by{|article| (article.ratings.where(like: true).size).to_f / (article.ratings.size) }
end
end
As you can guess, our app becomes huge in database, traffic increase and this part of the code becomes a bottleneck.
I am trying to rewrite it in pure sql for performance increase. I am also trying to make it as an ActiveRelation to chain up with other conditions. Trying also to write the sql but no success. Any ideas ?
Thanks !
Using :
Rails 3.0.10
ActiveRecord
Sqlite 3 (We can switch to Postgresql)
What you need is a cache column.
1) Create a migration in order to ass the cache column:
class AddRatingCacheToArticles < ActiveRecord::Migration
def self.up
add_column :articles, :rating_cache, :decimal, :default => 0.0
end
def self.down
remove_column :articles, :rating_cache
end
end
2) Define an update method in Article which will do the count:
class Article < ActiveRecord::Base
has_many :ratings
def update_rating_cache
current_rating = ratings.where(:like => true).count.to_f/ratings.count.to_f
update_attribute(:rating_cache, current_rating)
end
end
3) Setup a callback in Rating to trigger the update_rating_cache method when they are saved:
class Rating < ActiveRecord::Base
belongs_to :article
after_save :update_article_rating_cache
after_destroy :update_article_rating_cache
def update_article_rating_cache
article.update_rating_cache if article
end
end
4) It is now super easy to sort your articles by rating:
class Article < ActiveRecord::Base
has_many :ratings
def self.by_ratings
order('rating_cache DESC')
end
def update_rating_cache
current_rating = ratings.where(:like => true).count.to_f/ratings.count.to_f
update_attribute(:rating_cache, current_rating)
end
end
And that can be used as an ActiveRelation!
Good luck :)
I haven't been able to test this SQL in your model yet, but can you try:
select articles_ratings.*, likes/total rating from (select articles.*, SUM(if(articles.like, 1, 0)) likes, count(*) total from articles JOIN ratings on article_id = articles.id GROUP BY articles.id) articles_ratings ORDER BY rating desc
That should hopefully give you a list of articles, sorted by their rating (highest to lowest). I can try and follow up with some rails if that works.
EDIT As suggesteed by #socjopa, if you're not trying to get this to production immediately, my next recommendation would be to move this query to a view. Treat it like any other ActiveRecord, and associate it to your Articles accordingly.
With the appropriate indexes, the view should be fairly quick but may not really be necessary to calculate the rating value at runtime each time. You may also want to consider storing a rating column on your Articles table, if performance isn't where you need it to be. You could simply update this rating whenever an article rating is modified or created.
That said, this performance should be night and day over your current iteration.
I am making some assumptions about your implementation, like I do assume that you have value field in your Rating model that can be 1 for a "like" and -1 for "not like", etc
Start with:
class Article
has_one :rating_value, :class_name => 'RatingValue'
end
class RatingValue < ActiveRecord::Base
belongs_to :article
set_table_name "rating_values"
end
So, in a migration you generate view (postgres example):
execute %q{CREATE OR REPLACE VIEW rating_values AS
SELECT ratings.article_id, sum(ratings.value) AS value
FROM ratings
GROUP BY ratings.article_id;}
given you have a database view like this, you can make a scope you need for sorting:
scope :ascend_by_rating, {:joins => %Q{
LEFT JOIN "rating_values"
ON rating_values.article_id = article.id },
:order => "rating_values.value ASC"}
Should be much more efficent than your sorting attempt in Ruby.
From your by_ratings method, I understand that you want the articles sorted by most liked reviews/ratings.
Can we rewrite the method into a scope like this:
scope :by_ratings, select('articles.*, ((select count(id) from ratings where article_id = articles.id) - count(article_id) ) as article_diff_count')
.joins(:ratings).group('article_id').where('like = ?',true).order('article_diff_count asc')
I chose to compare the difference instead of ratio between total ratings and liked ratings since this should be lighter on the SQL engine. Hope this helps.