I ran into problem N + 1
in association :
class Category < ApplicationRecord
has_many :categories_designs, dependent: :destroy
has_many :designs, through: :categories_designs
has_many :templates, ->{ where(is_template: true) }, through: :categories_designs, class_name: 'Design', source: :design
def marked_designs_as_new?
designs.select(:mark_design_as_new_until).where("mark_design_as_new_until >= ?", Time.now.in_time_zone.beginning_of_day).exists?
end
end
And I want to use the marked_designs_as_new? method in the view.
- #categories.each do |category|
= category.title.titleize
- if category.marked_designs_as_new?
.design-type-marked
NEW
In my controller I call:
#categories = Category.includes(categories_designs: :design).visible
And I'm faced with the problem of N + 1.
Category Load (0.4ms) SELECT "categories".* FROM "categories" WHERE "categories"."hidden" = $1 ORDER BY "categories"."position" ASC LIMIT $2 OFFSET $3 [["hidden", false], ["LIMIT", 100], ["OFFSET", 0]]
CategoriesDesign Load (0.4ms) SELECT "categories_designs".* FROM "categories_designs" WHERE "categories_designs"."category_id" IN (1, 3, 4, 5, 6, 7, 8)
Design Load (0.5ms) SELECT "designs".* FROM "designs" WHERE "designs"."id" IN (1, 4, 3, 6)
(0.7ms) SELECT COUNT(*) FROM "designs" INNER JOIN "categories_designs" ON "designs"."id" = "categories_designs"."design_id" WHERE "categories_designs"."category_id" = $1 AND "designs"."is_template" = $2 [["category_id", 1], ["is_template", true]]
Design Exists (0.7ms) SELECT 1 AS one FROM "designs" INNER JOIN "categories_designs" ON "designs"."id" = "categories_designs"."design_id" WHERE "categories_designs"."category_id" = $1 AND (mark_design_as_new_until >= '2018-03-13 00:00:00') LIMIT $2 [["category_id", 1], ["LIMIT", 1]]
(0.5ms) SELECT COUNT(*) FROM "designs" INNER JOIN "categories_designs" ON "designs"."id" = "categories_designs"."design_id" WHERE "categories_designs"."category_id" = $1 AND "designs"."is_template" = $2 [["category_id", 3], ["is_template", true]]
............. etc.
why?
Ok, your .select(:mark_design_as_new_until) performs another query to the database. What you should do is use an array select method in the following way:
.select(&:mark_design_as_new_until)
This gives you an array of designs loaded in the memory on which you can perform .any? method to check your condition:
.select(&:mark_design_as_new_until).any? { |design| design.mark_design_as_new_until >= Time.now.in_time_zone.beginning_of_day }
And of course, include designs in your Category.
Category.includes(:designs, ...)
Did you try Category.includes([:categories_designs, :design]) Also, you can change the marked_designs_as_new? method as follows,
def marked_designs_as_new?
designs.select{ |x| x.marked_designs_as_new? }.any?
end
design.rb
class Design
def marked_designs_as_new?
mark_design_as_new_until >= Time.now.in_time_zone.beginning_of_day
end
end
Related
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 []>
I'm trying to use the acts as taggable on gem with my rails 5 app.
I have a model called Proposal, which I'm trying to tag with predefined tags from my Randd::Fields model.
In my proposal.rb, I have:
class Proposal < ApplicationRecord
acts_as_taggable
acts_as_taggable_on :randd_maturities, :randd_fields, :randd_purposes, :randd_activities
In my proposal controller, I have whitelisted the randd_fields_list attribute:
params.require(:proposal).permit(:title, :randd_fields_list)
In my Randd::Fields table, I have one record saved:
Randd::Field.all
Randd::Field Load (0.5ms) SELECT "randd_fields".* FROM "randd_fields"
=> #<ActiveRecord::Relation [#<Randd::Field id: 1, created_at: "2016-11-26 08:38:11", updated_at: "2016-11-26 08:38:11", anz_reference: "test ref", title: "test title">]>
In the console, I'm trying to add the predefined Randd::Field.title to the proposal:
Proposal.first.randd_field_list.add(Randd::Field.find_by(id: 1).title)
But - then when I try: Proposal.first.randd_field_list, I get:
p.randd_field_list
ActsAsTaggableOn::Tagging Load (0.7ms) SELECT "taggings".* FROM "taggings" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 [["taggable_id", 17], ["taggable_type", "Proposal"]]
ActsAsTaggableOn::Tag Load (0.9ms) 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 = 'randd_fields' AND taggings.tagger_id IS NULL) [["taggable_id", 17], ["taggable_type", "Proposal"]]
=> []
So - that hasn't worked.
How do I make the randd_field_list update with my defined tags in the Randd::Field table?
To save tag you need to save Proposal object:
proposal = Proposal.first
proposal.randd_field_list.add(Randd::Field.find_by(id: 1).title)
proposal.save
I have this model in Rails (trimmed to the relevant parts)
class Session < ActiveRecord::Base
belongs_to :user
before_save :invalidate_existing_sessions
def invalidate_existing_sessions
Session.where(user_id: user.id, current: true).each { |sess| sess.update_attributes(current: false) }
end
end
However, when a record is created and about to be saved, the server goes into an infinite loop.
Here are the server logs
Processing by V1::SessionsController#create as */*
Parameters: {"email"=>"user#example.com", "password"=>"[FILTERED]", "session"=>{}}
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT 1 [["email", "user#example.com"]]
(0.2ms) BEGIN
Session Load (0.7ms) SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = $1 AND "sessions"."current" = $2 [["user_id", 1
], ["current", true]]
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
CACHE (0.0ms) SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = $1 AND "sessions"."current" = $2 [["user_id", 1], ["cu
rrent", true]]
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
CACHE (0.0ms) SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = $1 AND "sessions"."current" = $2 [["user_id", 1], ["cu
rrent", true]]
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
CACHE (0.0ms) SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = $1 AND "sessions"."current" = $2 [["user_id", 1], ["cu
rrent", true]]
A bit later, this is what the log turns into
app/models/session.rb:12:in `invalidate_existing_sessions'
app/models/session.rb:12:in `block in invalidate_existing_sessions'
app/models/session.rb:12:in `invalidate_existing_sessions'
app/models/session.rb:12:in `block in invalidate_existing_sessions'
app/models/session.rb:12:in `invalidate_existing_sessions'
app/models/session.rb:12:in `block in invalidate_existing_sessions'
app/models/session.rb:12:in `invalidate_existing_sessions'
Any ideas? I'm using Rails 5 alpha.
It's because your before_save method does this...
sess.update_attributes(current: false)
Since update_attributes calls before_save you are (as you say) in an infinite loop.
So you need to skip the callbacks
class Session < ActiveRecord::Base
attr_accessor :skip_callbacks
before_save :invalidate_existing_sessions, unless: :skip_callbacks
def invalidate_existing_sessions
Session.where(user_id: user.id, current: true).each do |sess|
sess.skip_callbacks = true
sess.update_attributes(current: false)
end
end
Even though all of the above answers worked for me, this is what I found simplest and I ended up using.
def invalidate_existing_sessions
Session.where(user_id: user.id, current: true).each { |sess| sess.update_column(:current, false) }
end
Turns out update_column doesn't call any callbacks, but as an disadvantage it doesn't update updated_at if you're using timestamps in your model.
You're running update_attributes in before_save, that means you're saving before save. That's why it goes into an infinite loop.
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
I have the following enum model in my Rails (4) application:
class Dual < ActiveRecord::Base
enum dual: [:dual, :not_dual]
validates :dual, uniqueness: true
validates :dual, presence: true
end
And I have another model which has many Duals:
class SillColour < ActiveRecord::Base
has_many :sill_colour_duals, dependent: :destroy
has_many :duals, through: :sill_colour_duals
end
I want to be able to test if an instance of SillColour has a Dual enum. This is all I could get to work:
dual = Dual.find(1)
not_dual = Dual.find(2)
sill_colour.duals.include?(dual)
sill_colour.duals.include?(not_dual)
Obviously this is extremely unreliable as the ID of the Duals could be anything in production (for testing IDs are fixed). I tried this:
dual = Dual.where(dual: 0)
not_dual = Dual.where(dual: 1)
and even given the database duals table looks like this:
id | dual
----+------
1 | 0
2 | 1
My tests fail and it seems to be because dual and non_dual are no longer comparing correctly. I've examined them using pry and they appear to be the same as before, but clearly they're not.
Surely there must be a better way? I envisaged being able to do this:
sill_colour.duals.include?(Dual.dual)
sill_colour.duals.include?(Dual.not_dual)
but this doesn't work either.
Any suggestions?
I'll try to answer with a code from my app
class User < ActiveRecord::Base
has_many :contacts
end
class Contact < ActiveRecord::Base
enum status: [:dual, :not_dual]
# see scopes
scope :dual, -> {where(status: Contact.statuses['dual']) }
scope :not_dual, -> {where(status: Contact.statuses['not_dual']) }
end
Now console, check if a User instance has a contact with dual status:
2.1.5 :001 > u = User.last
User Load (0.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT 1
=> #<User id: 1, email: "...", ...>
2.1.5 :003 > u.contacts.dual
Contact Load (0.3ms) SELECT "contacts".* FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 0 [["user_id", 1]]
=> #<ActiveRecord::AssociationRelation []>
2.1.5 :004 > u.contacts.not_dual
Contact Load (0.3ms) SELECT "contacts".* FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 1 [["user_id", 1]]
=> #<ActiveRecord::AssociationRelation []>
As you can see both return empty array, so calling any? on empty array will return false both. Because I don't have any contact with status dual or not_dual, let's create one.
Find a contact:
2.1.5 :005 > c = Contact.last
Contact Load (0.5ms) SELECT "contacts".* FROM "contacts" ORDER BY "contacts"."id" DESC LIMIT 1
=> #<Contact id: 3, user_id: 1, ..., ...., status: nil>
Set it as dual:
2.1.5 :006 > c.dual!
(0.1ms) BEGIN
SQL (15.1ms) UPDATE "contacts" SET "status" = $1, "updated_at" = $2 WHERE "contacts"."id" = 3 [["status", 0], ["updated_at", "2014-12-29 13:16:45.576778"]]
(25.5ms) COMMIT
=> true
Now check if user has dual or not_dual contacts:
2.1.5 :009 > u.contacts.dual.any?
(0.2ms) SELECT COUNT(*) FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 0 [["user_id", 1]]
=> true
2.1.5 :010 > u.contacts.not_dual.any?
(0.3ms) SELECT COUNT(*) FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 1 [["user_id", 1]]
=> false
2.1.5 :011 >
Checking if user instance has a dual enum returns true. In your case instead of user it will be sill_colour.
If you don't like scopes you can use where:
u.contacts.where(status: Contact.statuses['dual']).any?
=> true
http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html
class Dual < ActiveRecord::Base
enum status: [:dual, :not_dual]
end
Having the class as the one above you can check the status like this:
dual = Dual.find(params[:id])
dual.dual? # will return true or false depends on the status you set.
dual.not_dual? # same, true or false
dual.status = "dual" # if status was set to 0 or not_dual if status was set to 1