Rails Preloading is not working with polymorphic relation - ruby-on-rails

class Project < ActiveRecord::Base
has_many :photos, -> { order(main: :desc, id: :asc) }, as: :photoable, class_name: 'Photo', dependent: :destroy
class Photo < ActiveRecord::Base
belongs_to :photoable, polymorphic: true
projects = Project.limit(10).includes(:photos)
SELECT "projects".* FROM "projects" WHERE "projects"."is_deleted" = $1 LIMIT 10 [["is_deleted", "f"]]
Photo Load (0.5ms) SELECT "photos".* FROM "photos" WHERE "photos"."photoable_type" = 'Project' AND "photos"."photoable_id" IN (1, 403, 371, 8784, 12, 34, 11, 1111, 31, 22) ORDER BY "photos"."main" DESC, "photos"."id" ASC
projects.first.photo
Photo Load (0.6ms)
It is sending a DB query which gets executed in (0.6ms). Any idea how i can avoid a DB query ?
I'm using Rails 4.2.6 & Ruby 2.3.1p112

projects = Project.includes(:photos).limit(10)
Then iterate through the projects. It wont trigger another query.
projects.each do |project|
project.photo
end

Related

Eager load Rails' has_many with two primary_keys and foreign_keys

I have two models
class TimeEntry < ApplicationRecord
belongs_to :contract
end
class Timesheet < ApplicationRecord
belongs_to :contract
has_many :time_entries, primary_key: :contract_id, foreign_key: :contract_id
end
Additionally, both models have a date column.
The problem: A Timesheet is only for a fixed date and by scoping only to contract_id I always get all time_entries of a contract for each Timesheet.
I tried to scope it like this:
has_many :time_entries, ->(sheet) { where(date: sheet.date) }, primary_key: :contract_id, foreign_key: :contract_id
This works, but unforunately it is not eager loadable:
irb(main):019:0> Timesheet.where(id: [1,2,3]).includes(:time_entries).to_a
Timesheet Load (117.9ms) SELECT "timesheets".* FROM "timesheets" WHERE "timesheets"."id" IN ($1, $2, $3) [["id", 1], ["id", 2], ["id", 3]]
TimeEntry Load (0.3ms) SELECT "time_entries".* FROM "time_entries" WHERE "time_entries"."date" = $1 AND "time_entries"."contract_id" = $2 [["date", "2014-11-21"], ["contract_id", 1]]
TimeEntry Load (0.3ms) SELECT "time_entries".* FROM "time_entries" WHERE "time_entries"."date" = $1 AND "time_entries"."contract_id" = $2 [["date", "2014-11-22"], ["contract_id", 1]]
TimeEntry Load (0.3ms) SELECT "time_entries".* FROM "time_entries" WHERE "time_entries"."date" = $1 AND "time_entries"."contract_id" = $2 [["date", "2014-11-23"], ["contract_id", 1]]
Is it possible, to provide Rails with two primary_keys AND foreign_keys? Or how could I make the example above eager loadable to avoid n+1 queries?
You can use a custom SQL query for the association to retrieve the TimeEntry records for a given Timesheet in this way:
class Timesheet < ApplicationRecord
belongs_to :contract
has_many :time_entries, lambda {
select('*')
.from('time_entries')
.where('time_entries.date = timesheets.date')
.where('time_entries.contract_id = timesheets.contract_id')
}, primary_key: :contract_id, foreign_key: :contract_id
end
Then, can use
timesheets = Timesheet.where(id: [1,2,3]).eager_load(:time_entries)
time_entries = timesheets.first.time_entries
Note:- this will only work with while eager loading, not preloading. That's why explicitly using the keyword instead of includes.

Rails 6 table association user table in association with items table

Hi I'm trying to create an app that has items users are selling. I have a table for Users selling the item and a table for items, but I'm a little confused about how I should setup the next table for the buyer. I have a separate table that is many to many between User and Item tracking user_id and item_id. Should I be creating a similar table tracking buyer_id and item_id? I want to be able to track what item has been bought from which user and vs versa. User and Buyers are from the same User table.
Thanks!
Edit:
class UsersController < ApplicationController
def my_page
#user = current_user
#seller_items = current_user.seller_orders.map { |so| so.order_items.map { |oi| { item: oi.item } } }.flatten
#seller_items.to_a
end
end
A more complete answer, with less models, and named joins. You might want to "merge" the order and order_items tables, and remove the multiple if you're dealing with singular items for sale, e.g. cars, but for anything that is either bought in bulk or might be sold at the same time as something else you might want this layout:
generate your models:
rails g model User name:string
rails g model Item name:string
rails g model Order order_date:time status:string
rails g model OrderItem order:references item:references multiple:integer
modify create_order to add in the additional references:
def change
create_table :orders do |t|
t.time :order_date, index: true, null: false
t.string :status
t.references :buyer, index: true, null: false, foreign_key: {to_table: :users}
t.references :seller, index: true, null: false, foreign_key: {to_table: :users}
t.timestamps
end
migrate the models:
rake db:migrate
== 20201126090851 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0036s
== 20201126090851 CreateUsers: migrated (0.0039s) =============================
== 20201126090858 CreateItems: migrating ======================================
-- create_table(:items)
-> 0.0030s
== 20201126090858 CreateItems: migrated (0.0032s) =============================
== 20201126091129 CreateOrders: migrating =====================================
-- create_table(:orders)
-> 0.0077s
== 20201126091129 CreateOrders: migrated (0.0081s) ============================
== 20201126091209 CreateOrderItems: migrating =================================
-- create_table(:order_items)
-> 0.0065s
== 20201126091209 CreateOrderItems: migrated (0.0067s) ========================
modify the models to add the joins:
app/models/user.rb
::::::::::::::
class User < ApplicationRecord
has_many :buyer_orders, class_name: "Order", foreign_key: :buyer, inverse_of: :buyer
has_many :seller_orders, class_name: "Order", foreign_key: :seller, inverse_of: :seller
end
::::::::::::::
app/models/item.rb
::::::::::::::
class Item < ApplicationRecord
has_many :order_items, inverse_of: :item
end
::::::::::::::
app/models/order.rb
::::::::::::::
class Order < ApplicationRecord
has_many :order_items, inverse_of: :order
belongs_to :seller, class_name: "User", inverse_of: :seller_orders
belongs_to :buyer, class_name: "User", inverse_of: :buyer_orders
end
::::::::::::::
app/models/order_item.rb
::::::::::::::
class OrderItem < ApplicationRecord
belongs_to :order, inverse_of: :order_items
belongs_to :item, inverse_of: :order_items
end
insert data:
User.create(name: "hello")
User.create(name: "again")
Item.create(name: "whatever")
Order.create(buyer: User.first, seller: User.last, order_date: Time.now())
OrderItem.create(item: Item.first, order: Order.first, multiple: 1)
test the output:
Check the status of the order:
2.7.0 :002 > Order.first
(0.5ms) SELECT sqlite_version(*)
Order Load (0.2ms) SELECT "orders".* FROM "orders" ORDER BY "orders"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Order id: 1, order_date: "2000-01-01 09:26:22", status: nil, buyer_id: 1, seller_id: 2, created_at: "2020-11-26 09:26:22", updated_at: "2020-11-26 09:26:22">
2.7.0 :003 > Order.first.seller
Order Load (0.2ms) SELECT "orders".* FROM "orders" ORDER BY "orders"."id" ASC LIMIT ? [["LIMIT", 1]]
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
=> #<User id: 2, name: "again", created_at: "2020-11-26 09:25:26", updated_at: "2020-11-26 09:25:26">
2.7.0 :004 > Order.first.buyer
Order Load (0.2ms) SELECT "orders".* FROM "orders" ORDER BY "orders"."id" ASC LIMIT ? [["LIMIT", 1]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "hello", created_at: "2020-11-26 09:25:18", updated_at: "2020-11-26 09:25:18">
Check the "buyer orders" of the first user:
2.7.0 :013 > User.first.buyer_orders
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
Order Load (0.3ms) SELECT "orders".* FROM "orders" WHERE "orders"."buyer_id" = ? LIMIT ? [["buyer_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Order id: 1, order_date: "2000-01-01 09:26:22", status: nil, buyer_id: 1, seller_id: 2, created_at: "2020-11-26 09:26:22", updated_at: "2020-11-26 09:26:22">]>
check the "seller orders" of the second user:
2.7.0 :014 > User.last.seller_orders
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]
Order Load (0.3ms) SELECT "orders".* FROM "orders" WHERE "orders"."seller_id" = ? LIMIT ? [["seller_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Order id: 1, order_date: "2000-01-01 09:26:22", status: nil, buyer_id: 1, seller_id: 2, created_at: "2020-11-26 09:26:22", updated_at: "2020-11-26 09:26:22">]>
for peace of minds sake, check that the first user doesn't have any seller orders:
2.7.0 :015 > User.first.seller_orders
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
Order Load (0.2ms) SELECT "orders".* FROM "orders" WHERE "orders"."seller_id" = ? LIMIT ? [["seller_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
2.7.0 :016 >
Controller
class UsersController < ApplicationController
def my_page
#user = current_user
#seller_orders = current_user.seller_orders
# remember that you might want to filter this in the future
# from_date = params[:from_date].present? ? params[:from_date] : Time.at(0)
# to_date = params[:to_date].present? ? params[:to_date] : Time.now()
# #seller_orders = #seller_orders.where(order_date: from_date..to_date)
end
end
view (I use haml)
%h1
= #user.username
Seller Orders
- #seller_orders.each do |so|
%table.seller_order{id: "seller_order_#{so.id}"}
%tr
%th Order Date:
%td= so.order_date
%tr
%th Buyer:
%td= so.buyer.username
%tr.spacer
%td{colspan: 2}
%tr
%th Item
%th Multiple
- so.order_items.each do |oi|
%tr
%td= oi.item.name
%td= oi.multiple
seller items
#seller_items = #seller_orders.map{|so| so.order_items.map{|oi| {multiple: oi.multiple, item: oi.item} }.flatten
or possibly (written from memory, not tested)
#seller_items = OrderItem.select("sum(order_items.multiple) as multiple, order_items.item_id as item_id").joins(:orders).joins(:buyer).where("users.id = ?", User.first.id).group("item_id")
You need something in the middle indeed - that's typicall called an "Order" in e-commerce with:
Link to User
Link to Item
but also maybe some more attributes:
Date of the sale
Status (cart/order/paid/delivered)
So indeed - you'll want an additional model there
class Seller < ApplicationRecord
belongs_to :user
has_many selling_items
has_many :items, through: :selling_items
end
class Buyer < ApplicationRecord
belongs_to :user
has_many bought_items
has_many :items, through: :bought_items
end
class Items < ApplicationRecord
end
class BoughtItem < ApplicationRecord
belongs_to :buyer
belongs_to :item
end
class SellingItem < ApplicationRecord
belongs_to :seller
belongs_to :item
end
class User < ApplicationRecord
end

Rails: Querying has_one through belongs_to adds PK null in query

I'm getting an error when trying to query a relation. These are my models:
class Course < ApplicationRecord
belongs_to :subject, inverse_of: :courses
# This is the important relation
has_one :department, through: :subject
end
class Subject < ApplicationRecord
belongs_to :department, inverse_of: :subjects
has_many :courses, dependent: :destroy, inverse_of: :subject
end
class Department < ApplicationRecord
has_many :subjects, dependent: :destroy
end
The thing is that if I do:
> Course.where(department: Department.first)
Department Load (0.3ms) SELECT `departments`.* FROM `departments` ORDER BY `departments`.`id` ASC LIMIT 1
Course Load (0.7ms) SELECT `courses`.* FROM `courses` WHERE `courses`.`id` IS NULL
=> []
As you can see, it includes a WHERE courses.id IS NULL. I'm able anyway to access this relation by doing:
> Course.joins(:subject).where(subjects: { department: Department.first })
Department Load (0.4ms) SELECT `departments`.* FROM `departments` ORDER BY `departments`.`id` ASC LIMIT 1
Course Load (0.5ms) SELECT `courses`.* FROM `courses` INNER JOIN `subjects` ON `subjects`.`id` = `courses`.`subject_id` WHERE `subjects`.`department_id` = 1
And I get many objects as result, but I'd like to know if I'm missing something to get this to work.
BTW, if I try to access the method directly, it works correctly:
> Course.first.department
Course Load (0.9ms) SELECT `courses`.* FROM `courses` ORDER BY `courses`.`id` ASC LIMIT 1
Department Load (0.6ms) SELECT `departments`.* FROM `departments` INNER JOIN `subjects` ON `departments`.`id` = `subjects`.`department_id` WHERE `subjects`.`id` = 1 LIMIT 1
=> #<Department:0x00007fc40dfab5f8
id: 1,
name: "Matemática",
slug: "matematica",
code: 1,
created_at: Sat, 15 Jun 2019 20:49:33 UTC +00:00,
updated_at: Sat, 15 Jun 2019 20:49:33 UTC +00:00>
Thank you!

How do you return a single value from a scope on a has_many relation?

How do I return a single record from this scope? I tried both ways.
class Subscription < ApplicationRecord
has_many :invoices, dependent: :destroy
class Invoice < ApplicationRecord
belongs_to :subscription
scope :current, -> do
# where(paid: nil).order(:created_at).last
where(paid: nil).order(created_at: :desc).limit(1).first
end
The first way correctly adds order by ... desc limit 1, but then it executes another query without the where condition!
irb(main):004:0> s.invoices.current
Invoice Load (22.0ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."subscription_id" = $1 AND "invoices"."paid" IS NULL ORDER BY "invoices"."created_at" DESC LIMIT $2 [["subscription_id", 16], ["LIMIT", 1]]
Invoice Load (2.0ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."subscription_id" = $1 [["subscription_id", 16]]
=> #<ActiveRecord::AssociationRelation [#<Invoice id: 8, subscription_id: 16, user_id: 21, paid: "2018-03-15", created_at: "2018-03-14 22:42:48">]>
The second way also does another query, obliterating the correct results.
irb(main):007:0> s.invoices.current
Invoice Load (2.0ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."subscription_id" = $1 AND "invoices"."paid" IS NULL
ORDER BY "invoices"."created_at" DESC LIMIT $2 [["subscription_id", 16], ["LIMIT", 1]]
Invoice Load (2.0ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."subscription_id" = $1 [["subscription_id", 16]]
=> #<ActiveRecord::AssociationRelation [#<Invoice id: 8, subscription_id: 16, user_id: 21, paid: "2018-03-15", created_at: "2018-03-14 22:42:48">]>
Also, how do I get just the record, not an ActiveRecord::AssociationRelation?
Ruby 5.0.6
You might try something like:
class Invoice < ApplicationRecord
belongs_to :subscription
class << self
def for_subscription(subscription)
where(subscription: subscription)
end
def unpaid
where(paid: nil)
end
def newest
order(created_at: :desc).first
end
end
end
Which, if you have an instance of Subscription called #subscription you could use like:
Invoice.unpaid.for_subscription(#subscription).newest
I believe that should fire only one query and should return an invoice instance.
I replaced the scope with
def self.current
where(paid: nil).order(:created_at).last
end
And it worked. However I don't know why, as this method is in Invoice class, while the relation is ActiveRecord::Associations::CollectionProxy class. I wish I knew why it works. And I wish I knew why scope didn't work and performed two queries.

Two belong_to referring the same table + eager loading

First of all, based on this (Rails association with multiple foreign keys) I figured out how to make two belong_to pointing to the same table.
I have something like that
class Book < ApplicationRecord
belongs_to :author, inverse_of: :books
belongs_to :co_author, inverse_of: :books, class_name: "Author"
end
class Author < ApplicationRecord
has_many :books, ->(author) {
unscope(:where).
where("books.author_id = :author_id OR books.co_author_id = :author_id", author_id: author.id)
}
end
It's all good. I can do either
book.author
book.co_author
author.books
However, sometimes I need to eager load books for multiple authors (to avoid N queries).
I am trying to do something like:
Author.includes(books: :title).where(name: ["Lewis Carroll", "George Orwell"])
Rails 5 throws at me: "ArgumentError: The association scope 'books' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported."
I am trying to figure out what I should do?
Should I go with many-to-many association? It sounds like a solution. However, it looks like it will introduce it's own problems (I need "ordering", meaning that I need explicitly differentiate between main author and co-author).
Just trying to figure out whether I am missing some simpler solution...
Why do you not use HABTM relation? For example:
# Author model
class Author < ApplicationRecord
has_and_belongs_to_many :books, join_table: :books_authors
end
# Book model
class Book < ApplicationRecord
has_and_belongs_to_many :authors, join_table: :books_authors
end
# Create books_authors table
class CreateBooksAuthorsTable < ActiveRecord::Migration
def change
create_table :books_authors do |t|
t.references :book, index: true, foreign_key: true
t.references :author, index: true, foreign_key: true
end
end
end
You can use eagerload like as following:
irb(main):007:0> Author.includes(:books).where(name: ["Lewis Carroll", "George Orwell"])
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."name" IN (?, ?) LIMIT ? [["name", "Lewis Correll"], ["name", "George Orwell"], ["LIMIT", 11]]
HABTM_Books Load (0.1ms) SELECT "books_authors".* FROM "books_authors" WHERE "books_authors"."author_id" IN (?, ?) [["author_id", 1], ["author_id", 2]]
Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."id" IN (?, ?) [["id", 1], ["id", 2]]
Try this:
Author.where(name: ["Lewis Carroll", "George Orwell"]).include(:books).select(:title)

Resources