Using :counter_cache and :touch in the same association - ruby-on-rails

I have a Comment model that belongs_to a Message. In comments.rb I have the following:
class Comment < ActiveRecord::Base
belongs_to :message, :counter_cache => true, :touch => true
end
I've done this because updating the counter_cache doesn't update the updated_at time of the Message, and I'd like it to for the cache_key.
However, when I looked in my log I noticed that this causes two separate SQL updates
Message Load (4.3ms) SELECT * FROM `messages` WHERE (`messages`.`id` = 552)
Message Update (2.2ms) UPDATE `messages` SET `comments_count` = COALESCE(`comments_count`, 0) + 1 WHERE (`id` = 552)
Message Update (2.4ms) UPDATE `messages` SET `updated_at` = '2009-08-12 18:03:55', `delta` = 1 WHERE `id` = 552
Is there any way this can be done with only one SQL call?
Edit I also noticed that it does a SELECT of the Message beforehand. Is that also necessary?

It probably does two queries because it's not been optimised yet.
Why not branch and create a patch :D

Related

how to return values in json that contain models to be associated using JBuilder

I would like to return values in json that contain models to be associated using JBuilder.
But I don’t know how to do it. And I encounter the error that “undefined method xx”
Here is my setting of rails.
Model
app/models/item.rb
belongs_to :user
app/models/user.rb
include UserImageUploader[:image]
has_many :item
vim app/uploaders/user_image_uploader.rb
# MiniMagick
require 'image_processing/mini_magick'
class UserImageUploader < Shrine
include ImageProcessing::MiniMagick
# The determine_mime_type plugin allows you to determine and store the actual MIME type of the file analyzed from file content.
plugin :determine_mime_type
plugin :store_dimensions
plugin :pretty_location
plugin :processing
plugin :recache
#The versions plugin enables your uploader to deal with versions,
#by allowing you to return a Hash of files when processing.
plugin :versions
process(:store) do |io, context|
original = io.download
thumbnail = ImageProcessing::MiniMagick
.source(original)
.resize_to_limit!(600, nil)
original.close!
{ original: io, thumbnail: thumbnail }
end
#plugin :versions
#plugin :delete_promoted
#plugin :delete_raw
end
items_controller.rb
#items = Item.includes(:user).page(params[:page] ||= 1).per(8).order('created_at DESC')
render 'index', formats: 'json', handlers: 'jbuilder'
Item/index.json.jbuilder
json.array! #items do |t|
json.id t.id //get the value normally
json.created_at t.created_at //get the value normally
Json.user_id t.user.id //undefined method `id’
json.user_original_img t.user.image_url(:original) //undefined method `image_url'
end
As above, I could not get the value of the model being associated.
By the way, I could check the value correctly with rails console.
Bundle exec rails c
Item.first.user.image_url(:original)
Item Load (1.5ms) SELECT `items`.* FROM `items` ORDER BY `items`.`id` ASC LIMIT 1
User Load (0.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> "https://xx.s3.ap-northeast-1.amazonaws.com/store/user/1/image/original-xx”
Item.first.user.id
(19.0ms) SET NAMES utf8mb4, ##SESSION.sql_mode = CONCAT(CONCAT(##sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), ##SESSION.sql_auto_is_null = 0, ##SESSION.wait_timeout = 2147483
Item Load (0.9ms) SELECT `items`.* FROM `items` ORDER BY `items`.`id` ASC LIMIT 1
User Load (0.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> 1
Let me know what points I am wrong with.
Thank you for reading my question.
It seems that some items in #items list, doesn't have associated user, or their user_id field is nil. Then item.user would be nil. When you do nil.image_url you get NoMethodError: undefined method 'image_url' for nil:NilClass .
You could add a foreign key constraint between Item and User in your migration, to avoid problems like this:
add_foreign_key :items, :users
NOTE:
Adding the foreign key would still allow empty values. You'd also have to add the following in your migration file to avoid empty values in user_id column:
change_column_null :items, :user_id, false
Thanks to #3limin4t0r for pointing this out.
app/models/user.rb
include UserImageUploader[:image]
has_many :item
should be has_many :items, not terribly confident on this, but this may be the reason you're finding blank columns in your db. A has_many, belongs_to relationship should default to required.

Rails association= return value and behavior

The guide does not say what return value would be for association= methods. For example the has_one association=
For the simple case, it returns the assigned object. However this is only when assignment succeeds.
Sometimes association= would persist the change in database immediately, for example a persisted record setting the has_one association.
How does association= react to assignment failure? (Can I tell if it fails?)
Is there a bang! version in which failure raises exception?
How does association= react to assignment failure? (Can I tell if it fails?)
It can't fail. Whatever you assign, it will either work as expected:
Behind the scenes, this means extracting the primary key from this
object and setting the associated object's foreign key to the same
value.
or will save the association as a string representation of passed in object, if the object is "invalid".
Is there a bang! version in which failure raises exception?
Nope, there is not.
The association= should not be able to fail. It is a simple assignment to a attribute on your attribute. There are no validations called by this method and the connection doesn't get persisted in the database until you call save.
The return value of assignments is the value you pass to it.
http://guides.rubyonrails.org/association_basics.html#has-one-association-reference-when-are-objects-saved-questionmark
So another part of the guide does talk about the return behavior for association assignment.
If association assignment fails, it returns false.
There is no bang version of this.
Update
Behaviors around :has_many/has_one through seems to be different.
Demo repository: https://github.com/lulalalalistia/association-assignment-demo
In the demo I seeded some data in first commit, and hard code validation error in second commit. Demo is using rails 4.2
has_many through
class Boss < ActiveRecord::Base
has_many :room_ownerships, as: :owner
has_many :rooms, through: :room_ownerships
end
When I add a room, exception is raised:
irb(main):008:0> b.rooms << Room.first
Boss Load (0.2ms) SELECT "bosses".* FROM "bosses" ORDER BY "bosses"."id" ASC LIMIT 1
Room Load (0.1ms) SELECT "rooms".* FROM "rooms" ORDER BY "rooms"."id" ASC LIMIT 1
(0.1ms) begin transaction
(0.1ms) rollback transaction
ActiveRecord::RecordInvalid: Validation failed: foo
irb(main):014:0> b.rooms
=> #<ActiveRecord::Associations::CollectionProxy []>
has_one through
class Employee < ActiveRecord::Base
has_one :room_ownership, as: :owner
has_one :room, through: :room_ownership
end
When I add a room I don't get exception:
irb(main):021:0> e.room = Room.first
Room Load (0.2ms) SELECT "rooms".* FROM "rooms" ORDER BY "rooms"."id" ASC LIMIT 1
RoomOwnership Load (0.1ms) SELECT "room_ownerships".* FROM "room_ownerships" WHERE "room_ownerships"."owner_id" = ? AND "room_ownerships"."owner_type" = ? LIMIT 1 [["owner_id", 1], ["owner_type", "Employee"]]
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Room id: 1, created_at: "2016-10-03 02:32:33", updated_at: "2016-10-03 02:32:33">
irb(main):022:0> e.room
=> #<Room id: 1, created_at: "2016-10-03 02:32:33", updated_at: "2016-10-03 02:32:33">
This makes it difficult to see whether the assignment succeeds or not.

Optimize eager loading in Rails

Rails 3.2. I have the following:
# city.rb
class City < ActiveRecord::Base
has_many :zones, :dependent => :destroy
end
# zone.rb
class Zone < ActiveRecord::Base
belongs_to :city
has_many :zone_shops, :dependent => :destroy
has_many :shops, :through => :zone_shops
end
# zone_shop.rb
class ZoneShop < ActiveRecord::Base
belongs_to :zone
belongs_to :shop
end
# shop.rb
class Shop < ActiveRecord::Base
end
# cities_controller.rb
def show
#trip = City.find(params[:id], :include => [:user, :zones => [:shops]])
#zones = #trip.zones.order("position")
# List out all shops for the trip
shops_list_array = []
#zones.each do |zone, i|
zone.shops.each do |shop|
shops_list_array << shop.name
end
end
#shops_list = shops_list_array.join(', ')
end
# development.log
City Load (0.3ms) SELECT `cities`.* FROM `cities` WHERE `cities`.`id` = 1 LIMIT 1
Zone Load (0.3ms) SELECT `zones`.* FROM `zones` WHERE `zones`.`trip_id` IN (1) ORDER BY position asc
ZoneShop Load (0.3ms) SELECT `zone_shops`.* FROM `zone_shops` WHERE `zone_shops`.`zone_id` IN (26, 23, 22) ORDER BY position asc
Shop Load (0.5ms) SELECT `shops`.* FROM `shops` WHERE `shops`.`id` IN (8, 7, 1, 9)
Zone Load (0.5ms) SELECT `zones`.* FROM `zones` WHERE `zones`.`trip_id` = 1 ORDER BY position asc, position
Shop Load (0.5ms) SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 26
Shop Load (0.6ms) SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 23
Shop Load (0.4ms) SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 22
Notice in my log, the last 3 lines with shops with ID 26, 23, 22 are redundant. How should I rewrite my cities_controller.rb to reduce the query to the system?
Many thanks.
#zones = #trip.zones.includes(:shops).order("position")
This eager-loads the shops association and should elimitate the n+1 query problem caused by zone.shops.each
For more information, have a look at the Ruby on Rails Guide section 12 on Eager Loading associations, which was also linked by #Benjamin M
I'd suggest that this
zone.shops.each do |shop|
shops_list_array << shop.name
end
produces the 3 last lines of your log. This means: You currently have only one zone inside your database. If you put more zones in there, you will get a lot more Zone Load entries in log.
The problem obviously is Rails' each method, which triggers the lazy loading:
#zones.each do |zone, i|
...
The solution depends on your needs, but I'd suggest that you read everything about Rails' eager loading feature. (There's exactly your problem: The each thing). Look it up here: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
It's pretty easy, short and straightforward :)

syntax for ROR associations

Apologies in advance for another newbie question, but the ROR syntax is just not clicking with me, I can't get my head around the shortcuts and conventions (despite reading a couple of books already!) -
I effectively copied this from a book, but I"m trying to work out what is build, create etc?
#cart = current_cart
product = Catalog::Product.find(params[:product_id])
Rails.logger.debug { "Value in cart id " + #cart.id.to_s }
#checkout_line_item = #cart.line_items.build(product: product)
respond_to do |format|
if #checkout_line_item.save...
The output from log is this:
Processing by Checkout::LineItemsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"9NH+xgDPTf/iN7RCdPd8H9rAIqWsSVB/f/rIT++Kk7M=", "product_id"=>"7"}
Created a line item
(0.1ms) BEGIN
SQL (2.0ms) INSERT INTO `checkout_carts` (`created_at`, `discounts`, `grand_total`, `loyalty_points`, `order_date`, `subtotal`, `timestamps`, `total_tax`, `updated_at`) VALUES ('2012-08-21 11:06:15', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2012-08-21 11:06:15')
(0.2ms) COMMIT
Catalog::Product Load (0.2ms) SELECT `products`.* FROM `products` WHERE `products`.`id` = 7 LIMIT 1
Value in cart id 8
Completed 500 Internal Server Error in 5ms
NoMethodError (undefined method `save' for nil:NilClass):
app/controllers/checkout/line_items_controller.rb:55:in `block in create'
app/controllers/checkout/line_items_controller.rb:54:in `create'
I'm guessing the problem lies with the build syntax where it builds the checkout line item, or possibly I've set up the has_many associations wrong. Is this enough for someone to help me troubleshoot? Or should I post the model declarations?
Update with models:
class Checkout::LineItem < ActiveRecord::Base
attr_accessible :customer_update_date, :inventory_status, :line_item_color, :line_item_description, :line_item_size, :line_item_tagline, :line_item_total, :quantity, :sku_id, :style_id, :tax, :tax_code, :timestamps, :unit_price, :product
belongs_to :cart
belongs_to :product, :class_name => 'Catalog::Product'
end
class Checkout::Cart < ActiveRecord::Base
attr_accessible :discounts, :grand_total, :loyalty_points, :order_date, :subtotal, :timestamps, :total_tax
has_many :line_items, dependent: :destroy
end
module Catalog
class Product < ActiveRecord::Base
attr_accessible :assoc_product,:product_id, :merch_associations, :aux_description, :buyable, :long_description, :name, :on_special, :part_number, :release_date, :short_description, :withdraw_date, :occasion
<<clipped for brevity>>
has_many :line_items, :class_name => 'Checkout::LineItem'
...
end
Can't answer my own question, but I think I got the answer:
It looks like I needed to add the cart to the build call...
this appears to have worked (I think, there's another blocking problem, but I can sort that one):
#cart = current_cart
product = Catalog::Product.find(params[:product_id])
Rails.logger.debug { "Value in cart id " + #cart.id.to_s }
#checkout_line_item = #cart.line_items.build(product: product, cart: #cart)
Build essentially creates and reserves space for a blank object. This allows you to create an associated object without saving it. The only use case I've experienced this with is a nested form where I had multiple dates for an event, so I used 5.times.build to create 5 empty date associations.

Why doesn't this model use a new starting point for a time based find on every request?

course.rb
has_many :current_users, :through => :user_statuses, :source => :user, :conditions => ['user_statuses.updated_at > ?', 1.hour.ago]
console
Loading development environment (Rails 3.2.2)
>> course = Course.find(1)
Course Load (0.3ms) SELECT `courses`.* FROM `courses` WHERE `courses`.`id` = 1 LIMIT 1
=> #<Course id: 1, title: "Course 1", created_at: "2012-04-17 19:17:15", updated_at: "2012-04-17 19:17:15">
>> Time.now
=> 2012-04-23 08:29:45 -0400
>> course.current_users.count
(0.4ms) SELECT COUNT(*) FROM `users` INNER JOIN `user_statuses` ON `users`.`id` = `user_statuses`.`user_id` WHERE `user_statuses`.`user_id` = 1 AND (user_statuses.updated_at > '2012-04-23 12:28:40')
=> 0
>> Time.now
=> 2012-04-23 08:30:07 -0400
>> course.current_users.count
(0.4ms) SELECT COUNT(*) FROM `users` INNER JOIN `user_statuses` ON `users`.`id` = `user_statuses`.`user_id` WHERE `user_statuses`.`user_id` = 1 AND (user_statuses.updated_at > '2012-04-23 12:28:40')
=> 0
>>
Notice when checking the 1.hour.ago condition it uses the same time as a starting point despite the 30 second difference between the times when I made the request. Exiting console and restarting it clears it out, but it happens again with a new time. This behavior exists in testing and a browser as well. How do I get a model to use a time based condition for a has_many :through find?
I believe you want to use a dynamic condition on your models relation.
Have a look at this SO question
Basically when your model loads, 1.hour.ago is evaluated only once. If I understand your question, you want it to be evaluated on each request.
Something like this (rails 3.1+) :
:conditions => lambda { |course| "user_statuses.updated_at > '#{1.hour.ago}'" }
Putting the query in the model at all didn't work, either in a has_many :through setup or in a method. So I ended up removing the association and putting the query in the controller. This allows the current time to be calculated when the request is made.
model:
has_many :user_statuses
controller:
#course = Course.find(params[:id])
#current_users = #course.user_statuses.where('updated_at > ?', 1.hour.ago)

Resources