Rails ActiveRecord - get belongs_to association with lock - ruby-on-rails

I would like to retrieve a belongs_to association and acquire a database lock on its object.
> character.team.lock!
ActiveRecord::Base -- Team Load -- { :sql => "SELECT `teams`.* FROM `teams` WHERE `teams`.`id` = 1 LIMIT 1" }
ActiveRecord::Base -- Team Load -- { :sql => "SELECT `teams`.* FROM `teams` WHERE `teams`.`id` = 1 LIMIT 1 FOR UPDATE" }
Above runs two queries, which technically makes sense - character.team loads the team, then team.lock! selects once more with FOR UPDATE.
The question is - how can I make it issue only one query?

You can carry on a scope to the association using ActiveRecord::Relation#scoping:
Team.lock.scoping { character.team }

Apparently you can't, because the .lock method will always reload the instance (issuing a second SQL load). From the docs:
.lock: Obtain a row lock on this record. Reloads the record to obtain the requested lock. Pass an SQL locking clause to append the end of the SELECT statement or pass true for “FOR UPDATE” (the default, an exclusive row lock). Returns the locked record.

Thankfully there is a solution:
character.association(:team).scope.lock.to_a[0]
# SELECT "teams".* FROM "teams" WHERE "teams"."id" = '...' LIMIT 1 FOR UPDATE
The only difference to the SQL query is the added FOR UPDATE.
May be abstracted in ApplicationRecord for all belongs_to associations:
class ApplicationRecord < ActiveRecord::Base
# ...
def self.belongs_to(name, *)
class_eval <<~RUBY
def locked_#{name}
association(:#{name}).scope.lock.to_a[0]
end
RUBY
super
end
end
Then in app code:
character.locked_team

Related

Rails: weird behavior when getting last record while ordering

I'm using Rails 6 and I've noticed a strange behavior in Active Record when trying to get the latest record from a collection. Here is what I have:
session.rb
class Session < ApplicationRecord
has_many :participations
end
participation.rb
class Participation < ApplicationRecord
belongs_to :session
end
When I'm trying to get the latest participation with:
Participation.order(created_at: :desc).last
The SQL query generated looks like:
SELECT "participations".*
FROM "participations"
ORDER BY "participations"."created_at" ASC
LIMIT $1
Note that I did order(created_at: :desc) but the SQL is using ASC.
However, if I change my code to:
Participation.order(created_at: :asc).last
The SQL query is doing the opposite (a DESC):
SELECT "participations".*
FROM "participations"
ORDER BY "participations"."created_at" DESC
LIMIT $1
Does anyone have an explanation as to why it behave this way ? Is it a Rails bug ?
Seems like using last with order is causing this issue. If I remove last, ActiveRecord is generating the correct SQL (using the correct order)
ActiveRecord is optimizing the SQL statement for you. This
Participation.order(created_at: :desc).last
returns the same result as
Participation.order(created_at: :asc).first
But the latter statement is more efficient because it has to traverse fewer rows, so Rails generates SQL as if you had written it that way.

Rails 5 select from two different tables and get one result

I have 3 models, Shop, Client, Product.
A shop has many clients, and a shop has many products.
Then I have 2 extra models, one is ShopClient, that groups the shop_id and client_id. The second is ShopProduct, that groups the shop_id and product_id.
Now I have a controller that receives two params, the client_id and product_id. So I want to select all the shops (in one instance variable #shops) filtered by client_id and product_id without shop repetition. How can I do this??
I hope I was clear, thanks.
ps: I'm using Postgresql as database.
Below query will work for you.
class Shop
has_many :shop_clients
has_many :clients, through: :shop_clients
has_many :shop_products
has_many :products, through: :shop_products
end
class Client
end
class Product
end
class ShopClient
belongs_to :shop
belongs_to :client
end
class ShopProduct
belongs_to :shop
belongs_to :product
end
#shops = Shop.joins(:clients).where(clients: {id: params[:client_id]}).merge(Shop.joins(:products).where(products: {id: params[:product_id]}))
Just to riff on the answer provided by Prince Bansal. How about creating some class methods for those joins? Something like:
class Shop
has_many :shop_clients
has_many :clients, through: :shop_clients
has_many :shop_products
has_many :products, through: :shop_products
class << self
def with_clients(clients)
joins(:clients).where(clients: {id: clients})
end
def with_products(products)
joins(:products).where(products: {id: products})
end
end
end
Then you could do something like:
#shops = Shop.with_clients(params[:client_id]).with_products(params[:product_id])
By the way, I'm sure someone is going to say you should make those class methods into scopes. And you certainly can do that. I did it as class methods because that's what the Guide recommends:
Using a class method is the preferred way to accept arguments for scopes.
But, I realize some people strongly prefer the aesthetics of using scopes instead. So, whichever pleases you most.
I feel like the best way to solve this issue is to use sub-queries. I'll first collect all valid shop ids from ShopClient, followed by all valid shop ids from ShopProduct. Than feed them into the where query on Shop. This will result in one SQL query.
shop_client_ids = ShopClient.where(client_id: params[:client_id]).select(:shop_id)
shop_product_ids = ShopProduct.where(product_id: params[:product_id]).select(:shop_id)
#shops = Shop.where(id: shop_client_ids).where(id: shop_product_ids)
#=> #<ActiveRecord::Relation [#<Shop id: 1, created_at: "2018-02-14 20:22:18", updated_at: "2018-02-14 20:22:18">]>
The above query results in the SQL query below. I didn't specify a limit, but this might be added by the fact that my dummy project uses SQLite.
SELECT "shops".*
FROM "shops"
WHERE
"shops"."id" IN (
SELECT "shop_clients"."shop_id"
FROM "shop_clients"
WHERE "shop_clients"."client_id" = ?) AND
"shops"."id" IN (
SELECT "shop_products"."shop_id"
FROM "shop_products"
WHERE "shop_products"."product_id" = ?)
LIMIT ?
[["client_id", 1], ["product_id", 1], ["LIMIT", 11]]
Combining the two sub-queries in one where doesn't result in a correct response:
#shops = Shop.where(id: [shop_client_ids, shop_product_ids])
#=> #<ActiveRecord::Relation []>
Produces the query:
SELECT "shops".* FROM "shops" WHERE "shops"."id" IN (NULL, NULL) LIMIT ? [["LIMIT", 11]]
note
Keep in mind that when you run the statements one by one in the console this will normally result in 3 queries. This is due to the fact that the return value uses the #inspect method to let you see the result. This method is overridden by Rails to execute the query and display the result.
You can simulate the behavior of the normal application by suffixing the statements with ;nil. This makes sure nil is returned and the #inspect method is not called on the where chain, thus not executing the query and keeping the chain in memory.
edit
If you want to clean up the controller you might want to move these sub-queries into model methods (inspired by jvillians answer).
class Shop
# ...
def self.with_clients(*client_ids)
client_ids.flatten! # allows passing of multiple arguments or an array of arguments
where(id: ShopClient.where(client_id: client_ids).select(:shop_id))
end
# ...
end
Rails sub-query vs join
The advantage of a sub-query over a join is that using joins might end up returning the same record multiple times if you query on a attribute that is not unique. For example, say a product has an attribute product_type that is either 'physical' or 'digital'. If you want to select all shops selling a digital product you must not forget to call distinct on the chain when you're using a join, otherwise the same shop may return multiple times.
However if you'll have to query on multiple attributes in product, and you'll use multiple helpers in the model (where each helper joins(:products)). Multiple sub-queries are likely slower. (Assuming you set has_many :products, through: :shop_products.) Since Rails reduces all joins to the same association to a single one. Example: Shop.joins(:products).joins(:products) (from multiple class methods) will still end up joining the products table a single time, whereas sub-queries will not be reduced.
Below sql query possibly gonna work for you.
--
-- assuming
-- tables: shops, products, clients, shop_products, shop_clients
--
SELECT DISTINCT * FROM shops
JOIN shop_products
ON shop_products.shop_id = shops.id
JOIN shop_clients
ON shop_clients.shop_id = shops.id
WHERE shop_clients.client_id = ? AND shop_products.product_id = ?
If you'll face difficulties while creating an adequate AR expression for this sql query, let me know.
Btw, here is a mock

How to order records by their latest child records attribute

I'm having troubles to order my records by their has_one association. I'm quite sure the solution is obvious, but I just can't get it.
class Migration
has_many :checks
has_one :latest_origin_check, -> { where(origin: true).order(at: :desc) }, class_name: 'Check'
end
class Check
belongs_to :migration
end
If I order by checks.status I always get different check ids. Shouldn't they be the same but with different order?
Or is the -> { } way to get the has_one association the problem?
Migration.all.includes(:latest_origin_check).order("checks.status DESC").each do |m| puts m.latest_origin_check.id end
So in one sentence: How do I order records through a custom has_one association?
I'm using Ruby 2.0.0, Rails 4.2 and PostgreSQL.
Update:
I wasn't specific enough. I've got two has_one relations on the checks relation.
Also very Important. One Migration has a way to big number of checks to include all the checks at once. So Migration.first.includes(:checks) would be very slow. We are talking about serveral thousand and I only need the latest.
class Migration
has_many :checks
has_one :latest_origin_check, -> { where(origin: true).order(at: :desc) }, class_name: 'Check'
has_one :latest_target_check, -> { where(origin: false).order(at: :desc) }, class_name: 'Check'
end
class Check
belongs_to :migration
end
Now if I get the latest_origin_check, I get the correct Record. The query is the following.
pry(main)> Migration.last.latest_origin_check
Migration Load (1.1ms) SELECT "migrations".* FROM "migrations" ORDER BY "migrations"."id" DESC LIMIT 1
Check Load (0.9ms) SELECT "checks".* FROM "checks" WHERE "checks"."migration_id" = $1 AND "checks"."origin" = 't' ORDER BY "checks"."at" DESC LIMIT 1 [["migration_id", 59]]
How do I get the latest check of each migration and then sort the migrations by a attribute of the latest check?
I'm using ransack. Ransack seems to get it right when I order the records by "checks.at"
SELECT "migrations".* FROM "migrations" LEFT OUTER JOIN "checks" ON "checks"."migration_id" = "migrations"."id" AND "checks"."origin" = 't' WHERE (beginning between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000' or ending between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000') ORDER BY "checks"."at" ASC
But the same query returns wrong results when I order by status
SELECT "migrations".* FROM "migrations" LEFT OUTER JOIN "checks" ON "checks"."migration_id" = "migrations"."id" AND "checks"."origin" = 't' WHERE (beginning between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000' or ending between '2015-02-22 23:00:00.000000' and '2015-02-23 22:59:59.000000') ORDER BY "checks"."status" ASC
Check.status is a boolean, check.at is a DateTime. A colleague suggested that the boolean is the problem. Do I need to convert the booleans to an integer to make them sortable? How do I do that only for the :latest_origin_check? Something like that?
.order("(case when \"checks\".\"status\" then 2 when \"checks\".\"status\" is null then 0 else 1 end) DESC")
You already have a has_many relationship with Check on Migration. I think you are looking for a scope instead:
scope :latest_origin_check, -> { includes(:checks).where(origin:true).order("checks.status DESC").limit(1)}
Drop the has_one :latest_origin_check line on Migration.
Migration.latest_origin_check
I think the line about should return your desired result set.

Rails scope with polymorphic model

I am getting some strange results from a very basic scope on a model with a polymorphic relationship. Here's the brief summary and detail of the relationships.
models/financials.rb
class Financial < ActiveRecord::Base
belongs_to :financiable, :polymorphic => true
#ltm is a boolean field in the model
scope :ltm, -> { where(ltm: true).last }
And then there's a basic Firm model that has many financials
models/firm.rb
class Firm < ActiveRecord::Base
has_many :financials, :as => :financiable, dependent: :destroy
So I get a bizarre result when a firm has no ltm financials (i.e. no financials with ltm: true). But when I call firm.financials.ltm I get an activerecord relation of financials that belong to the firm but do NOT have ltm: true. However, when I just do firm.financials.where(ltm: true).last I get nil
Summary of results for when there are no ltm financials for the firm:
firm.financials.ltm #AR relation of financials that belong to the firm but are not ltm
firm.financials.where(ltm: true).last #nil
And what makes it even stranger is that when a firm does have ltm financials, the scope works as expected.
Has anyone ever had this problem before or have any ideas? I mean the easy answer is to not use the scope but I wanted to understand what could be causing this.
---UPDATES BASED ON COMMENTS---
Thank you guys for putting a lot of thought into this.
D-side You were correct. The code was firm.financials.ltm and not firm.financials.ltm.last. That was a typo when I typed up the question. I updated the above to reflect and also below are the SQL queries.
Jiří Pospíšil - Great advice. I will update in my app but leave the same here so as not to create confusion.
Chumakoff. I force ltm to false if the user doesn't enter true with a before_save call so I don't think this is it but thanks for the thought.
So these are from the scenario where firm doesn't have any financials with ltm = true. As you can see, the scope request is making a second query to the database for all financials belonging to firm. Why is it doing that when it can't find it in the initial query?
firm.financials.ltm
Financial Load (4.6ms) SELECT "financials".* FROM "financials"
WHERE "financials"."financiable_id" = $1 AND
"financials"."financiable_type" = $2 AND "financials"."ltm" = 't'
ORDER BY "financials"."id" DESC LIMIT 1 [["financiable_id", 11],
["financiable_type", "Firm"]]
Financial Load (1.2ms) SELECT "financials".* FROM "financials" WHERE
"financials"."financiable_id"= $1 AND "financials"."financiable_type"
= $2 [["financiable_id", 11], ["financiable_type", "Firm"]]
firm.financials.where(ltm: true).last
Financial Load (16.8ms) SELECT "financials".* FROM "financials"
WHERE "financials"."financiable_id" = $1 AND
"financials"."financiable_type" = $2 AND "financials"."ltm" = 't'
ORDER BY "financials"."id" DESC LIMIT 1 [["financiable_id", 11],
["financiable_type", "Firm"]]
Your scope is not working correctly, .last in your scope get one results instead of an ActiveRecord relation, remove .last and the scope will work fine in getting all Financials with ltm=true

How to persist inverse-of relation through order, find, where, etc call?

Given the following classes with inverse-has-many-association:
class User < ActiveRecord:Base
has_many :accounts, inverse_of: :user
end
class Account < ActiveRecord:Base
belongs_to :user
end
And any given User object loaded as follows:
u = User.first
u.some_not_saved_variable = "123"
The following code shows that the inverse relation is not persisted after executing a new sql statement (order hits the database):
u.accounts.first.user.some_not_saved_variable #=> "123"
u.accounts.order(:created_at).first.user.some_not_saved_variable #=> nil
How to I make sure that my user object still remains the same after having executed an order, find, where, etc call?
If you want to persist the objects attribute but do not want to save the record then you can try using attributes=(new_attributes) or assign_attributes(new_attributes, options = {})
In your case:
u = User.first
u.attributes= :some_not_saved_variable => "123"
or
u = User.first
u.assign_attributes :some_not_saved_variable => "123"
sources: assign_attributes and attributes=
I don't think it is possible...
the inverse_of allows you to get the user object without pulling the database again, so the unsaved value of some_not_saved_variable (123) will persist.
However, if you call order, find, where, etc, a SQL will be generated and the stored data will replace the unsaved one.
Without inverse_of:
u.accounts.first.user.some_not_saved_variable
#SELECT "accounts".* FROM "accounts" WHERE "accounts"."user_id" = 1 LIMIT 1
#SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
=> "database value" #123 will be lost
Using inverse_of:
u.accounts.first.user.some_not_saved_variable
#no SQL statement
=> "123" #123 persist
Calling order, find, where, etc:
u.accounts.order(:created_at).first.user.some_not_saved_variable
#SQL to order by created_at
So, it will always access the database and the unsaved value will be replaced by the database one.

Resources