Rails: ActiveRecord preload alludes me - ruby-on-rails

I am not able to grasp how the ActiveRecord preload method is of use.
When I do, for example, User.preload(:posts), it does runs two queries but what is returned is just the same as User.all. The second query does not seem to affect the result.
User Load (3.2ms) SELECT "users".* FROM "users"
Post Load (1.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
Can someone explain?
Thanks!

Output is the same, but when you'll call user.posts, Rails will not load your posts from the database next time:
users = User.preload(:posts).limit(5) # users collection, 2 queries to the database
# User Load ...
# Post Load ...
users.flat_map(&:posts) # users posts array, no loads
users.flat_map(&:posts) # users posts array, no loads
You can do it as mutch times as you want, Rails just 'remember' your posts in RAM. The idea is that you go to the database only once.

Related

Unique Random Record in Ruby

I have a Question table in which there is id# and title as columns.
Now, I need to randomly select 5 questions from the table. I am seeing people are using:
Question.order("RANDOM()").limit(5) //using postgre
Till now I have:
def selectr
#randquestion=[]
while #randquestion.length<3 do
Question.uncached do
ques=Question.order("RANDOM()").first
#randquestion << ques
end
end
end
I found uncaching from Ruby on Rails Active Record RANDOM() always the same within a loop.
But I am not sure if this will give me unique questions. I want 3 unique questions only.
You can do this by fetching each question via a separate SQL request, with subsequent requests excluding IDs for records already seen. Here's an example, tested with Rails 5.2 and MySQL:
def random_questions(number)
already_seen = []
number.times.map do
question = Question.order('RAND()').where.not(id: already_seen).first
already_seen << question.id
question
end
end
If you try this out with random_questions(3), you'll see:
Question Load (1.5ms) SELECT `questions`.* FROM `questions` WHERE 1=1 ORDER BY RAND() LIMIT 1
Question Load (1.4ms) SELECT `questions`.* FROM `questions` WHERE `questions`.`id` != 2 ORDER BY RAND() LIMIT 1
Question Load (1.3ms) SELECT `questions`.* FROM `questions` WHERE `questions`.`id` NOT IN (2, 1) ORDER BY RAND() LIMIT 1
As an aside, please note that order('RAND()') triggers a deprecation warning in newer versions of Rails:
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "RAND()". Non-attribute arguments will be disallowed in Rails 6.0. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql().
To avoid this warning, use .order(Arel.sql('RAND()')) instead.

Rails: Why does where(id: objects) work?

I have the following statement:
Customer.where(city_id: cities)
which results in the following SQL statement:
SELECT customers.* FROM customers WHERE customers.city_id IN (SELECT cities.id FROM cities...
Is this intended behavior? Is it documented somewhere? I will not use the Rails code above and use one of the followings instead:
Customer.where(city_id: cities.pluck(:id))
or
Customer.where(city: cities)
which results in the exact same SQL statement.
The AREL querying library allows you to pass in ActiveRecord objects as a short-cut. It'll then pass their primary key attributes into the SQL it uses to contact the database.
When looking for multiple objects, the AREL library will attempt to find the information in as few database round-trips as possible. It does this by holding the query you're making as a set of conditions, until it's time to retrieve the objects.
This way would be inefficient:
users = User.where(age: 30).all
# ^^^ get all these users from the database
memberships = Membership.where(user_id: users)
# ^^^^^ This will pass in each of the ids as a condition
Basically, this way would issue two SQL statements:
select * from users where age = 30;
select * from memberships where user_id in (1, 2, 3);
Each of these involves a call on a network port between applications and the data to then be passsed back across that same port.
This would be more efficient:
users = User.where(age: 30)
# This is still a query object, it hasn't asked the database for the users yet.
memberships = Membership.where(user_id: users)
# Note: this line is the same, but users is an AREL query, not an array of users
It will instead build a single, nested query so it only has to make a round-trip to the database once.
select * from memberships
where user_id in (
select id from users where age = 30
);
So, yes, it's expected behaviour. It's a bit of Rails magic, it's designed to improve your application's performance without you having to know about how it works.
There's also some cool optimisations, like if you call first or last instead of all, it will only retrieve one record.
User.where(name: 'bob').all
# SELECT "USERS".* FROM "USERS" WHERE "USERS"."NAME" = 'bob'
User.where(name: 'bob').first
# SELECT "USERS".* FROM "USERS" WHERE "USERS"."NAME" = 'bob' AND ROWNUM <= 1
Or if you set an order, and call last, it will reverse the order then only grab the last one in the list (instead of grabbing all the records and only giving you the last one).
User.where(name: 'bob').order(:login).first
# SELECT * FROM (SELECT "USERS".* FROM "USERS" WHERE "USERS"."NAME" = 'bob' ORDER BY login) WHERE ROWNUM <= 1
User.where(name: 'bob').order(:login).first
# SELECT * FROM (SELECT "USERS".* FROM "USERS" WHERE "USERS"."NAME" = 'bob' ORDER BY login DESC) WHERE ROWNUM <= 1
# Notice, login DESC
Why does it work?
Something deep in the ActiveRecord query builder is smart enough to see that if you pass an array or a query/criteria, it needs to build an IN clause.
Is this documented anywhere?
Yes, http://guides.rubyonrails.org/active_record_querying.html#hash-conditions
2.3.3 Subset conditions
If you want to find records using the IN expression you can pass an array to the conditions hash:
Client.where(orders_count: [1,3,5])
This code will generate SQL like this:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))

Sql output content is produced twice in rails

I am using rails to develop a social website for posting, tagging etc. I am also using a gem public_activity and this is used to render the page. While loading the page, sometimes the post are shown twice when there is only some posts. But when I see the log of server I found queries are repeated twice ie, taken data from both database and cache and resulting in loading those posts twice.
Server log
SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 836 LIMIT 1
CACHE (0.0ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 836 LIMIT 1 [["id", 836]]
Please help to solve the issue . Thanks in advance.

How to enable caching of model data?

How do I enable caching across requests/actions?
My stack:
Rails 4.2.7
Postgres 9.5
I notice the following in my Rails logs
Country Load (0.4ms) SELECT "countries".* FROM "countries" WHERE "countries"."iso" = $1 LIMIT 1 [["iso", "US"]]
State Load (1.4ms) SELECT "states".* FROM "states" WHERE "states"."iso" = $1 AND "states"."country_id" = $2 LIMIT 1 [["iso", "OH"], ["country_id", 233]]
Country Load (0.3ms) SELECT "countries".* FROM "countries" WHERE "countries"."iso" = $1 LIMIT 1 [["iso", "US"]]
State Load (1.1ms) SELECT "states".* FROM "states" WHERE "states"."iso" = $1 AND "states"."country_id" = $2 LIMIT 1 [["iso", "OH"], ["country_id", 233]]
Country Load (3.6ms) SELECT "countries".* FROM "countries" WHERE "countries"."iso" = $1 LIMIT 1 [["iso", "US"]]
State Load (1.2ms) SELECT "states".* FROM "states" WHERE "states"."iso" = $1 AND "states"."country_id" = $2 LIMIT 1 [["iso", "OH"], ["country_id", 233]]
Note that the same queries are being run multiple times against my database in rapid succession. Is there some way I can indicate to Rails that certain tables/models are very unlikely to change, and so certain lookups can be cached on the app server?
Is there some way I can indicate to Rails that certain tables/models
are very unlikely to change, and so certain lookups can be cached on
the app server?
Absolutely, model caching seems to be a perfect fit here.
Here's the article which will give you a good overview of setting up different types of caching.
Also, check out official guides on caching.
Basically, you want to look into model caching.
You can write an extention and use it in models, which are to be cached:
module ModelCachingExtention
extend ActiveSupport::Concern
included do
class << self
# The first time you call Model.all_cached it will cache the collection,
# each consequent call will not fire the DB query
def all_cached
Rails.cache.fetch(['cached_', name.underscore.to_s, 's']) { all.load }
end
end
after_commit :clear_cache
private
# Making sure, that data is in consistent state by removing the cache
# everytime, the table is touched (eg some record is edited/created/destroyed etc).
def clear_cache
Rails.cache.delete(['cached_', self.class.name.underscore.to_s, 's'])
end
end
end
Then use it in model:
class Country < ActiveRecord::Base
include ModelCachingExtention
end
Now, when using Country.all_cached you will have the cached collection returned with zero db queries (once it is cached).
Within a single request queries are already cached by Rails. You will see it within your logfile with the prefix CACHE. Some operations, like inserting a new record within your request leads to a clear_query_cache and all cache entries are gone.
If you want that your query cache has a longer life time, you have to do it on your own. You can use Rails caching features for this:
Rails.cache.fetch("countries iso: #{iso}", expires_in: 1.hour) do
Country.where(iso: iso).all
end
You can also use memcache. With memcache you can share cached data between multiple servers.
You have to set it up within your specific environment config.
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"

General Active Record joins or includes

I have two models/tables A and B. I'd like to perform an Active Record query where the results include columns from both tables. I tried inner joins as they sounded like they combine columns from both tables. However trying the Active Record joins finder method returns results from only the first table.
What Active Record queries include columns from two tables in the results? Perhaps the includes finder method could help.
Edit: think of the two tables as ForumThreads and Posts. Each forum thread has multiple posts. I'd like the rows in the query results to contain information for each post and information for the forum thread (for example the thread title).
This question might have answered my question: Rails Joins and include columns from joins table
Joins performs an inner join, but will not return the data until you ask for it.
User.where(:id => 1).joins(:client_applications)
User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "client_applications" ON "client_applications"."user_id" = "users"."id" WHERE "users"."id" = 1
Includes will execute two queries (using where in) and cache the associated data (Eager Loading)
User.where(:id => 1).includes(:client_applications)
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1
ClientApplication Load (13.6ms) SELECT "client_applications".* FROM "client_applications" WHERE "client_applications"."user_id" IN (1)

Resources