Change multiple counters in the same query with counter_culture - ruby-on-rails

I have the following class with several counter_culture counters for the same association:
class Book < ::ActiveRecord::Base
belongs_to :user
counter_culture :user
counter_culture :user, column_name: Proc.new { |b| b.comedy? ? 'comedy_books_count' : nil }
counter_culture :user, column_name: Proc.new { |b| b.hard_cover? ? 'hard_cover_books_count' : nil }
end
The issue I have with this is that it executes the updates using several queries:
UPDATE "users" SET "books_count" = COALESCE("books_count", 0) + 1 WHERE "users"."id" = $1 [["id", 1616]]
UPDATE "users" SET "comedy_books_count" = COALESCE("comedy_books_count", 0) + 1 WHERE "users"."id" = $1 [["id", 1616]]
UPDATE "users" SET "hard_cover_books_count" = COALESCE("hard_cover_books_count", 0) + 1 WHERE "users"."id" = $1 [["id", 1616]]
Is there a way to do this more efficiently with counter_culture by executing the updates in a single query?

Each line is detected as a special case, it would be difficult for them to know which one they can optimise and which one they cannot. So I guess no.

Related

N+1 in has_many :through

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

Rails query condition or relation

It's my DB scheme:
branches
-----------------------------------
id
name:string
active:boolean
..
course_contents
-----------------------------------
id
title:string
show_all_branches:boolean
active:boolean
..
branches_course_contents
-----------------------------
branch_id
course_content_id
And my model files:
class Branch < ApplicationRecord
has_and_belongs_to_many :course_contents
scope :active, -> { where(active: true) }
end
class CourseContent < ApplicationRecord
has_and_belongs_to_many :branches
scope :active, -> { where(active: true) }
scope :show_all_branches, -> { where(show_all_branches: true) }
end
I'm trying to CourseContent.show_all_branches.merge(-> { joins(:branches) }) this command. It returns show_all_branches selected and has relations with branches table. But i need show_all_branches selected or has relations with branches table.
Thanks for helps. I solved this problem and I writing my solution for other peoples. I said actually need to branch's course_contents with show_all_branches selected in the course_contents table. I tried merge method with this CourseContent.show_all_branches.merge(-> { joins(:branches) }) or other many methods with other conditions. Cuz I used to joins/includes method all of my conditions. It's never returns true result for me. And I finally tried left_joins method and that solved my problem here.
My commands here:
#course.course_contents.active.left_joins(:branches).where("course_contents.show_all_branches = ? OR branches_course_contents.branch_id = ?", true, #branch.id)
Its sql result
SELECT "course_contents".* FROM "course_contents" LEFT OUTER JOIN "branches_course_contents" ON "branches_course_contents"."course_content_id" = "course_contents"."id" LEFT OUTER JOIN "branches" ON "branches"."id" = "branches_course_contents"."branch_id" WHERE "course_contents"."course_id" = 3 AND "course_contents"."active" = 't' AND (course_contents.show_all_branches = 't' OR branches_course_contents.branch_id = 1)
and its explain:
CACHE (0.0ms) SELECT "course_contents".* FROM "course_contents" LEFT OUTER JOIN "branches_course_contents" ON "branches_course_contents"."course_content_id" = "course_contents"."id" LEFT OUTER JOIN "branches" ON "branches"."id" = "branches_course_contents"."branch_id" WHERE "course_contents"."course_id" = $1 AND "course_contents"."active" = $2 AND (course_contents.show_all_branches = 't' OR branches_course_contents.branch_id = 1) [["course_id", 3], ["active", true]]
Branch Load (1424.1ms) SELECT "branches".* FROM "branches" WHERE "branches"."active" = $1 AND "branches"."slug" = $2 LIMIT $3 [["active", true], ["slug", "izmir"], ["LIMIT", 1]]

Merge ActiveRecord_Relation

In model Banner
belongs_to :segment
belongs_to :basic_component
has_many :state_banners, dependent: :destroy
has_many :states, through: :state_banners
scope :banner_have_zero_cities, lambda { includes(state_banners: :state_banner_cities).where(state_banner_cities: {state_banner_id: nil}) }
scope :banner_by_state, lambda { |state_id| where("state_banners.state_id = ?", state_id) }
scope :banner_by_city, lambda { |city_id| joins(state_banners: :state_banner_cities).where("state_banner_cities.city_id = ?", city_id) }
In controller
def scoped_collection
#banners_cities = Banner.banner_by_city(city_id)
#banners_states =Banner.banner_by_state(city.state_id).banner_have_zero_cities
#banners = #banners_cities.concat(#banners_states)
return #banners.joins(:basic_component)
end
#banners_states.size
=> 1
#banners_cities.size
=> 2
#banners_states.merge(#banners_cities)
SQL (0.2ms) SELECT DISTINCT banners.id FROM banners INNER JOIN state_banners ON state_banners.banner_id = banners.id INNER JOIN state_banner_cities ON state_banner_cities.state_banner_id = state_banners.id WHERE (state_banners.state_id = 3) AND state_banner_cities.state_banner_id IS NULL AND (state_banner_cities.city_id = '260') LIMIT 25 OFFSET 0
=> []
I need 3
i try concat
#banners = #banners_cities.concat(#banners_states)
#banners.size => 3
but
#banners.joins(:basic_component).order("basic_component.order asc").size => 2
CACHE (0.0ms) SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM banners INNER JOIN state_banners ON state_banners.banner_id = banners.id INNER JOIN state_banner_cities ON state_banner_cities.state_banner_id = state_banners.id INNER JOIN basic_components ON basic_components.id = banners.basic_component_id WHERE (state_banner_cities.city_id = '260') LIMIT 25 OFFSET 0) subquery_for_count
:(, help
Your post is kind of hard to follow, but try .limit(3) at the end of the query?

Rails infinite loop while updating other record's value during `before_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.

How to compare Rails enum types

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

Resources