ActiveRecord: chaining scopes and queries on associated records - mysql

I came across a piece of ActiveRecord behavior that I find totally intuitive, but it took me by surprise. Given,
class User < ActiveRecord::Base
has_one :profile
scope :active, -> { where(status: 1) }
def self.find_by_code(code)
Profile.find_by_code(code).person
end
end
class Profile < ActiveRecord::Base
belongs_to :user
end
I can combine the active scope and the find_by_code class method like,
User.active.find_by_code("ABC123")
and it will find the profile with the given code, then use the user_id on that profile to find the associated person. AND it remembers the active scope. How does it do that?
You can see in the generated queries that it first retrieves a profile, then queries for the relevant person:
SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`code` = 'ABC123' LIMIT 1
SELECT `user`.* FROM `user` WHERE `user`.`active` = 1 AND `user`.`id` = 8 LIMIT 1
Unrelated, but I'm on Rails 3. I know the finder methods have changed a bit since then.

Related

Ruby on rails data fetching issue

Hello i'm new to rails
I have a table named 'messages' which has columns current_user__id , to_user_id and created time
I am trying to build a chat application where different users can chat individually and those message will be stored at messages table with their respective ids.
Now in order to print the messages on screen.
I'm facing issues
I need a query such that both the current_user__id and to_user_id conversations and to_user_id and current_user__id conversation will be listed by the latest created time.
i will assume that you have two ActiveModel's: User and Message. Make sure that you have classes like:
class User < ApplicationRecord
has_many :messages
end
class Message < ApplicationRecord
belongs_to :current_user, class_name: 'User', foreign_key: 'current_user_id'
belongs_to :to_user, class_name: 'User', foreign_key: 'to_user_id'
end
A small trivia when you add t.timestamps to your migrations it creates created_at and updated_at fields for you.
Now I will just hard code the raw sql query for you:
def get_messages(current_user_id, to_user_id)
#messages = Message.where(' current_user_id=? OR receiver_user_id=? OR current_user_id=? OR receiver_user_id=? ',
current_user_id, current_user_id, to_user_id, to_user_id).order('created_at DESC')
end
You can play with order('created_at DESC') in order if you just want in ascending order you can replace DESC with ASC or order(:created_at)
You may put any other query conditions also like not showing deleted messages etc. You can learn more from official Ruby on Rails document for Active Record Query Interface.

Best practice: Which information should I store in my database?

Currently I am developing a small book rating app, where users can rate and comment on books.
Of course I have a book model:
class Book < ActiveRecord::Base
has_many :ratings
end
and a rating model:
class Rating < ActiveRecord::Base
belongs_to :book
end
The "overall rating value" of a rating object is calculated by different rating categories (e.g. readability, ... ). Furthermore the overall rating of one book should be calculated by all given ratings.
Now the question I am asking myself: Should I calculate/query the overall rating for every book EVERYTIME someone visits my page or should I add a field to my book model where the overall rating is (periodically) calculated and saved?
EDIT: The "calculation" I would use in this case is a simple average determination.
Example: A Book has about 200 ratings. Every rating is a composition of 10 category ratings. So I want to determine the average of one rating and in the end of all 200 ratings.
If the averaging of those ratings is not computationally expensive (i.e. doesn't take a long time), then just calculate it on-the-fly. This is in keeping with the idea of not prematurely optimsing (see http://c2.com/cgi/wiki?PrematureOptimization).
However, if you do want to optimise this calculation then storing it on the book model and updating the calculation on rating writes is the way to go. This is known as "caching" the result. Here is some code that will cache the average rating in the database. (There are other ways of caching).
class Book < ActiveRecord::Base
has_many :ratings, after_add :update_average_rating
def update_average_rating
update_attribute(:average_rating, average_rating)
end
def average_rating
rating_sum / ratings.count
end
def rating_sum
ratings.reduce(0) {|sum, rating|
sum + rating.value # assuming rating model has a value attribute
}
end
end
class Rating < ActiveRecord::Base
belongs_to :book
end
Note: the above code assumes the presence of an average_rating column on your book table in your database. Remember to add this column with a migration.
DB
The most efficient (although not conventional) way is to use db-level ALIAS columns, allowing you to calculate the AVG or SUM of the rating with each book call:
#app/models/book.rb
class Book < ActiveRecord::Base
def reviews_avg category
cat = category ? "AND `category` = \"#{category}\"" : ""
sql = "SELECT AVG(`rating`) FROM `reviews` WHERE `book_id` = #{self.id} #{cat})
results = ActiveRecord::Base.connection.execute(sql)
results.first.first.to_f
end
end
This would allow:
#book = Book.find x
#book.reviews_avg # -> 3.5
#book.reviews_avg "readability" # -> 5
This is the most efficient because it's handled entirely by the DB:
Rails
You should use the average functionality of Rails:
#app/models/book.rb
class Book < ActiveRecord::Base
has_many :ratings do
def average category
if category
where(category: category).average(:rating)
else
average(:rating)
end
end
end
end
The above will give you the ability to call an instance of a #book, and evaluate the average or total for its ratings:
#book = Book.find x
#book.reviews.average #-> 3.5
#book.reviews.average "readability" #-> 5
--
You could also use a class method / scope on Review:
#app/models.review.rb
class Review < ActiveRecord::Base
scope :avg, (category) -> { where(category: category).average(:rating) }
end
This would allow you to call:
#book = Book.find x
#book.reviews.avg #-> 3.5
#book.reviews.avg "readability" #-> 5
Association Extensions
A different way (not tested) would be to use the proxy_association.target object in an ActiveRecord Association Extension.
Whilst not as efficient as a DB-level query, it will give you the ability to perform the activity in memory:
#app/models/book.rb
class Book < ActiveRecord::Base
has_many :reviews do
def avg category
associative_array = proxy_association.target
associative_array = associative_array.select{|key, hash| hash["category"] == category } if category
ratings = associative_array.map { |a| a["rating"] }
ratings.inject(:+) / associative_array.size #-> 35/5 = 7
end
end
end
This would allow you to call:
#book = Book.find x
#book.reviews.avg # -> 3.5
#book.reviews.avg "readability" # -> 5
There is no need at all to recalculate the average overall rating for every page visit since it only will change when somebody actually rates the book. So just use a field AVG_RATING or something like this and update the value on every given rating.
Have you consider to use a cached version of the rating.
rating = Rails.cache.fetch("book_#{id}_rating", expires_in: 5.minutes) do
do the actual rating calculation here
end
In most cases you can get averages simply by querying the database:
average = book.reviews.average(:rating)
And in most cases its not going to be expensive enough that querying per request is going to be a real problem - and pre-mature optimization might be a waste of time and resources as Neil Atkinson points out.
However when the cost of calculation becomes an issue there are several approaches to consider which depend on the nature of the calculated data.
If the calculated data is something with merits being a resource on its own you would save it in the database. For example reports that are produced on a regular bases (daily, monthly, annual) and which need to be query-able.
Otherwise if the calculated data has a high "churn rate" (many reviews are created daily) you would use caching to avoid the expensive query where possible but stuffing the data into your database may lead to an excessive amount of slow UPDATE queries and tie up your web or worker processes.
There are many caching approaches that compliment each other:
etags to leverage client side caching - don't re-render if the response has not changed anyways.
fragment caching avoids db queries and re-rendering view chunks for data that has not changed.
model caching in Memcached or Redis can be used to avoid slow queries.
low level caching can be used to store stuff like averages.
See Caching with Rails: An overview for more details.

Rails 4 - order by related models created_at - activity

In my rails app I have few related models for example:
Event
has_many :comments
has_many :attendents
has_many :requests
What I need is to order by 'created_at' but not only main model (Event) but also related models, so I will display on top of the list event with most recent activity i.e.: comments, attendents, requests
So if Event date is newer than any comment, request or attendent date this event will be on top.
But if there is an event with newer comment this one will be on top etc.
How should I implement such ordering?
EDIT
db is mysql
Thanks
I would place a column on the event for last_activity, and maintain it by touching from the associated models.
The alternative is to order by using a join or subquery against the other tables, which is going to require a database query that will be much less efficient than simply ordering by last_activity descending.
Event
.select("events.*")
.joins("LEFT OUTER JOIN comments ON comments.event_id = events.id")
.joins("LEFT OUTER JOIN attendents ON attendents.event_id = events.id" )
.joins("LEFT OUTER JOIN requests ON requests.event_id = events.id")
.group("events.id")
.order("GREATEST(events.created_at,
MAX(comments.created_at),
MAX(attendents.created_at),
MAX(requests.created_at)) DESC")
If I understand your question correctly, something like this
Firstly add this to your Models in question:
default_scope order('created_at DESC')
Then I would do some grouping:
#comments = Comment.all
#comments_group = #comments.group_by { |c| c.event}
And in your view you'll be able to loop through each block:
#comments_group.each do |event, comment|
- event.name
comment.each do|c|
- c.body
I did not test this but it should give you an idea.

Sorting associated objects based on the association's creation date

Right now, I'm working on a simple app. It requires to get the associated objects ordered by the date that they we're added to the object. For that, I want to order them based on the pivot-table's id.
My app looks a bit like this:
class Product < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :products
end
However, when a user wants to buy a product, I would add a new relation into the pivot table courses_users. When I then run #product.users, I will get them back in the order the users where created, not added as the relation.
I've tried creating a query scope, but it didn't work. I also tried to create a order on the has_and_belongs_to_many, as such:
has_and_belongs_to_many :users, order: 'course_users.id ASC'
But none of that seemed to work, no ORDER statement could be found in the logs.
Add the created_at field to your table.
rails g migration AddTimestampsToCourseUsers created_at:datetime
then you can
#product.users.order "course_users.created_at ASC"

rails active record associations sql behavior

I have 2 models:
# models/car.rb
class Car < ActiveRecord::Base
belongs_to :model
end
and
# models/manufacturer.rb
class Manufacturer < ActiveRecord::Base
has_many :cars
end
When I'm executing command in rails console Car.find(1).manufacturer it shows me that one more sql query was executed SELECT manufacturers.* FROM manufacturers WHERE manufacturers.id = 54 LIMIT 1,
so I am interested is it usual (for production, first of all) behavior, when a lot of sql queries being executed just to get some object property? what about performance?
UPDATE, ANSWER:
I got an answer from another source: I was told it's "necessary evil" as a payment for abstraction
This is not a "necessary evil" and your intuition that the second query is needless is correct. What you need to do is use :include/includes to tell Rails to do a JOIN to get the associated objects in the same SELECT. So you could do this:
Car.find 1, :include => :manufacturer
# or, in Rails 3 parlance:
Car.includes(:manufacturer).find 1
Rails calls this "eager loading" and you can read more about it in the documentation (scroll down to or Ctrl+F for "Eager loading of associations").
If you always want to eager-load the associated objects you can declare default_scope in your model:
class Car
belongs_to :manufacturer
default_scope :include => :manufacturer
# or Rails 3:
default_scope includes(:manufacturer)
end
However you shouldn't do this unless you really need the associated Manufacturer every time you show a Car record.