Eager loading model instances using single SQL statement with Mobility table backend - ruby-on-rails

Using Rails 7 and Mobility 1.2.9:
# config/initializers/mobility.rb
Mobility.configure do |config|
config.plugins do
backend :table
active_record
reader
writer
backend_reader
query
cache
dirty
presence
end
end
# app/models/content.rb
class Content < ApplicationRecord
extend Mobility
translates :slug, :title
end
How to find contents by slug, instantiate them into Content objects and query their translated attributes using one single SQL statement (for an unchanging locale)? The examples below with and without eager_loading run 2 SQL statements.
> content = Content.i18n.find_by(slug: "foo")
Content Load (0.7ms) SELECT "contents".* FROM "contents" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'foo' LIMIT $1 [["LIMIT", 1]]
> content.title
Content::Translation Load (0.3ms) SELECT "content_translations".* FROM "content_translations" WHERE "content_translations"."content_id" = $1 [["content_id", "..."]]
> content = Content.i18n.eager_load(:translations).find_by(slug: "foo")
SQL (0.6ms) SELECT DISTINCT "contents"."id" FROM "contents" LEFT OUTER JOIN "content_translations" ON "content_translations"."content_id" = "contents"."id" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'blog-1-en' LIMIT $1 [["LIMIT", 1]]
SQL (0.8ms) SELECT "contents"."id" AS t0_r0, "contents"."created_at" AS t0_r1, "contents"."updated_at" AS t0_r2, ..., "content_translations"."id" AS t1_r0, "content_translations"."locale" AS t1_r1, "content_translations"."created_at" AS t1_r2, "content_translations"."updated_at" AS t1_r3, "content_translations"."slug" AS t1_r4, "content_translations"."title" AS t1_r5, "content_translations"."content_id" AS t1_r6 FROM "contents" LEFT OUTER JOIN "content_translations" ON "content_translations"."content_id" = "contents"."id" INNER JOIN "content_translations" "content_translations_en" ON "content_translations_en"."content_id" = "contents"."id" AND "content_translations_en"."locale" = 'en' WHERE "content_translations_en"."slug" = 'blog-1-en' AND "contents"."id" = $1 [["id", "..."]]
> content.title
# no DB statement, but there were 2 statements at instantiation
NB: I do not want to pluck attributes but instead to create model instances.

Related

How can I use a scope in a join query?

I want to use a scope of a joined table.
The goal is to write a scope for autors that have reports with a specific stat_id (for example 15)
Rails 5.2.3
class Author < ApplicationRecord
belongs_to :report
class Report < ApplicationRecord
has_many :authors
scope :with_stat, ->(s) {
where(stat_id: s)
}
This works fine:
Autor.joins(:report).where(reports: {stat_id: 15})
If the scope is more complex. How can I use the scope from class Report?
This doesn't work:
Autor.joins(:report).where(reports: {with_stat(15)})
What is the correct syntax?
That scope will not give you the correct query.
What you want is Author.joins(:report).where(reports: { stat_id: 1 }). Which gives a single query:
Author Load (1.0ms) SELECT "authors".* FROM "authors" INNER JOIN "reports" ON "reports"."id" = "authors"."report_id" WHERE "reports"."stat_id" = $1 LIMIT $2
This is what happens if you use the scope instead:
irb(main):004:0> Author.joins(:report).where(Report.with_stat(1))
Report Load (1.6ms) SELECT "reports".* FROM "reports" WHERE "reports"."stat_id" = $1 [["stat_id", 1]]
Author Load (0.6ms) SELECT "authors".* FROM "authors" INNER JOIN "reports" ON "reports"."id" = "authors"."report_id" LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
irb(main):005:0> Author.joins(:report).where(report: Report.with_stat(1))
Author Load (2.1ms) SELECT "authors".* FROM "authors" INNER JOIN "reports" ON "reports"."id" = "authors"."report_id" WHERE "authors"."report_id" IN (SELECT "reports"."id" FROM "reports" WHERE "reports"."stat_id" = $1) LIMIT $2 [["stat_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
The later uses a subquery which should give the same result but should be less effective.
What you can do is place the scope on the other side of the association:
class Author < ApplicationRecord
belongs_to :report
scope :with_stat, ->(s){
joins(:report).where(reports: {stat_id: s})
}
end
irb(main):010:0> Author.joins(:report).where(reports: { stat_id: 1 })
Author Load (1.1ms) SELECT "authors".* FROM "authors" INNER JOIN "reports" ON "reports"."id" = "authors"."report_id" WHERE "reports"."stat_id" = $1 LIMIT $2 [["stat_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>

Rails 4 - Eager loading with multiple ON conditions

I have an Article and each Article has exactly one User and either zero or one ArticleVote.
My code is:
#articles = Article
.eager_load(:user)
.eager_load(:article_vote)
.where(article_votes: { user_id: session[:user_id]})
.order(created_at: :desc)
Which produces the following SQL:
SELECT
`articles`.`article_id` AS t0_r0,
`articles`.`user_id` AS t0_r1,
`users`.`user_id` AS t1_r0,
`article_votes`.`article_vote_id` AS t2_r0,
`article_votes`.`article_id` AS t2_r1,
`article_votes`.`user_id` AS t2_r2
FROM `articles`
LEFT OUTER JOIN `users` ON `users`.`user_id` = `articles`.`user_id`
LEFT OUTER JOIN `article_votes` ON `article_votes`.`article_id` = `articles`.`article_id`
WHERE `article_votes`.`user_id` = 1
ORDER BY `articles`.`created_at` DESC
The problem with this query is if no vote exists (which sometimes it won't), then no records are returned. What I really need is this (note the where is gone and the condition is moved to the left join):
SELECT
`articles`.`article_id` AS t0_r0,
`articles`.`user_id` AS t0_r1,
`users`.`user_id` AS t1_r0,
`article_votes`.`article_vote_id` AS t2_r0,
`article_votes`.`article_id` AS t2_r1,
`article_votes`.`user_id` AS t2_r2
FROM `articles`
LEFT OUTER JOIN `users` ON `users`.`user_id` = `articles`.`user_id`
LEFT OUTER JOIN `article_votes` ON `article_votes`.`article_id` = `articles`.`article_id` and `article_votes`.`user_id` = 1
ORDER BY `articles`.`created_at` DESC
I saw in the documentation I can do something like:
has_one :article_vote, -> { where(user_id: 1) }
Which will put it in the left join vs the where, but this doesn't let me specify the user_id at query time, which makes it not viable.
Can this be done?

group by in view or controller

i am trying to loop through a list of products which a supplier sells via its different variants. i can get the list of products to display, but i wish to group these by the product id as to only display it once.
in my controller i have
#supplier = Supplier.joins(products: :variants).find(params[:id])
in my view i have
- #supplier.variants.group_by(&:product_id).each do |product_id, item|
= render :partial => 'product', :locals => {:item => item }
and my partial
= link_to shopping_supplier_path(item) do
%li.mdl-list__item.mdl-list__item--three-line
%span.mdl-list__item-primary-content
%span= item.product.name
%span.mdl-list__item-text-body
= item.product.description.downcase
%span.mdl-list__item-secondary-content
%i.material-icons
chevron_right
%hr
which when the sql executes returns the following query
Started GET "/shopping/suppliers/latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1" for 127.0.0.1 at 2016-04-19 23:22:08 +1000
Processing by Shopping::SuppliersController#show as HTML
Parameters: {"id"=>"latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1"}
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 1]]
Supplier Load (32.7ms) SELECT "suppliers".* FROM "suppliers" WHERE "suppliers"."permalink" = $1 ORDER BY "suppliers"."id" ASC LIMIT 1 [["permalink", "latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1"]]
Supplier Load (41.9ms) SELECT "suppliers".* FROM "suppliers" INNER JOIN "variant_suppliers" ON "variant_suppliers"."supplier_id" = "suppliers"."id" INNER JOIN "variants" ON "variants"."id" = "variant_suppliers"."variant_id" INNER JOIN "products" ON "products"."id" = "variants"."product_id" INNER JOIN "variants" "variants_products" ON "variants_products"."product_id" = "products"."id" WHERE "suppliers"."permalink" = $1 ORDER BY "suppliers"."id" ASC LIMIT 1 [["permalink", "latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1"]]
Variant Load (0.9ms) SELECT "variants".* FROM "variants" INNER JOIN "variant_suppliers" ON "variants"."id" = "variant_suppliers"."variant_id" WHERE "variant_suppliers"."supplier_id" = $1 [["supplier_id", 1]]
Rendered shopping/suppliers/_product.html.haml (53.5ms)
error
NoMethodError at /shopping/suppliers/latte-cartelle-drive-thru-coffee-241---245-princes-hwy--ha-1
undefined method `name' for #<Array:0x007faa5302d5a0>
Use SQL joins. When you create a query joining the tables that you are going to use, they will be previously loaded in memory, so the famous n+1 queries will not occur.
#supplier = Supplier.joins(variants: :products).find(params[:id])
# This will translate to something like this
SELECT * FROM suppliers
INNER JOIN variants ON variants.supplier_id = suppliers.id
INNER JOIN products ON products.variant_id = variants.id
WHERE suppliers.id = ?
Remember to always avoid the lazy loading of your associations.

ActsAsTaggableOn: tags not persisting in DB

I'm using the acts_as_taggable_on (3.4) plugin for tagging with Rails (4.2.4). I've tried adding custom tags both via my seed file and the console and while it appears to add the attributes, I can't then access them.
My model:
class Recipe < ActiveRecord::Base
acts_as_taggable_on :tags
acts_as_taggable_on :dietaries, :meals, :cuisines, :sources
end
Seed file:
tarte = Recipe.create(title: "Caramelized Tomato Tarte Tatin", url: "www.chocolateandzucchini.com", notes: "Lorem ipsum", favorite: false)
tarte.dietary_list.add("vegetarian," "vegan")
tarte.meal_list.add("appetizers", "mains", "dinner")
tarte.cuisine_list.add("French")
tarte.source_list.add("Chocolate and Zucchini")
Console steps (after running seed to create the recipe in the seed file above):
tarte = Recipe.first
tarte.dietary_list.add("vegetarian," "vegan")
tarte.meal_list.add("appetizers", "mains", "dinner")
tarte.cuisine_list.add("French")
tarte.source_list.add("Chocolate and Zucchini")
When I call Recipe.first.dietary_list, it runs a query
Recipe Load (0.6ms) SELECT "recipes".* FROM "recipes" ORDER BY "recipes"."id" ASC LIMIT 1
ActsAsTaggableOn::Tag Load (0.7ms)SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND (taggings.context = 'dietaries' AND taggings.tagger_id IS NULL) [["taggable_id", 1], ["taggable_type", "Recipe"]]
But it returns an empty array:
=> []
If I call Recipe.first.dietaries, it returns an empty Collection Proxy:
Recipe Load (0.6ms) SELECT "recipes".* FROM "recipes" ORDER BY "recipes"."id" ASC LIMIT 1
ActsAsTaggableOn::Tag Load (0.5ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND "taggings"."context" = $3 [["taggable_id", 1], ["taggable_type", "Recipe"], ["context", "dietaries"]]
=> #<ActiveRecord::Associations::CollectionProxy []>
Is there something about using this tool that I'm missing? Alternatively, are there better tagging tools out there?
Solved by calling .save after entering all tags

Rails: How would I retrieve records where child element includes any of another array?

I have a User and Document model, which both have tag_lists (from acts_as_taggable)
I want to find the Documents that have any of the tags a certain user has. So for example,
user.tag_list = ["ceo" , "sales"]
documentA.tag_list = ["ceo"]
documentB.tag_list = ["all-users']
documentC.tag_list = ["sales", "marketing"]
documentD.tag_list = ["marketing"]
I want to do something like
Document.where(tag_list.includes_any?("all-users" || user.tag_list))
-> which would retrieve document A,B,C.
Obviously the syntax is wrong, but you get the idea. How would I do this in an efficient way?
-------update
Just fyi, the tag_list is not a direct attribute of either User or Document model, but a active record relation from the acts_as_taggable gem: how would I call this in a .where operator?
2.1.1 :036 > Document.last.tag_list
Document Load (3.7ms) SELECT "assignable_objects".* FROM "assignable_objects" WHERE "assignable_objects"."type" IN ('Document') ORDER BY "assignable_objects"."id" DESC LIMIT 1
ActsAsTaggableOn::Tag Load (3.5ms) SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL) [["taggable_id", 52], ["taggable_type", "AssignableObject"]]
=> []
This might help you:
Document.where((tag_list-["all-users", user.tag_list].flatten).size != tag_list.size)

Resources