When using includes to eager load an active record, the updated_at does not get updated.
Simplified code:
class MyModel < ApplicationRecord
has_many :comments,
foreign_key: 'my_model_id',
dependent: :destroy,
inverse_of: :my_model
end
class Comments < ApplicationRecord
belongs_to :my_model, foreign_key: :my_model_id,
inverse_of: :comments,
touch: true
end
model = MyModel.includes(comments: :user).find_by!(my_id: 111)
params = {"comments_attributes"=>{"0"=>{"id"=>"2", "name"=>"aaa"}}
model.update(params)
# model.updated_at DOES NOT get updated.
model = MyModel.find_by!(my_id: 111)
params = {"comments_attributes"=>{"0"=>{"id"=>"2", "name"=>"aaa"}}
model.update(params)
# model.updated_at DOES get updated.
Why would the touch option not work when eager loading?
Related
class Sample
has_many :pictures
end
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
belongs_to :sample
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
What should be the association to get all product or employee of a given sample.
Sample.first.pictures.map(&:imageable). I want to get it as an activerecord association.
Workaround:
class Sample
has_many :pictures
has_many :imageable_employees, through: :pictures, source: :imageable, source_type: 'Employee'
has_many :imageable_products, through: :pictures, source: :imageable, source_type: 'Product'
end
Usage:
sample = Sample.first
employees = sample.imageable_employees
products = sample.imageable_products
...see docs
Explanation:
Sample.first.pictures.map(&:imageable). I want to get it as an activerecord association.
... is I don't think it's possible, but you can still get them all as an Array instead. The reason is that there is no table (model) that corresponds to the imageable association, but that it corresponds to ANY model instead, which complicates the SQL query, and thus I don't think it's possible.
As an example, consider the following query:
imageables_created_until_yesterday = Sample.first.something_that_returns_all_imageables.where('created_at < ?', Time.zone.now.beginning_of_day)
# what SQL from above should this generate? (without prior knowledge of what tables that the polymorphic association corresponds to)
# => SELECT "WHAT_TABLE".* FROM "WHAT_TABLE" WHERE (sample_id = 1 AND created_at < '2018-08-27 00:00:00.000000')
# furthermore, you'll notice that the SQL above only assumes one table, what if the polymorphic association can be at least two models / tables?
Alternative Solution:
Depending on the needs of your application and the "queries" that you are trying to do, you may or may not consider the following which implements an abstract_imageable (a real table) model for you to be able to perform queries on. You may also add more attributes here in this abstract_imageable model that you think are "shared" across all "imageable" records.
Feel free to rename abstract_imageable
class Sample
has_many :pictures
has_many :abstract_imageables, through: :pictures
end
class Picture
belongs_to :sample
has_many :abstract_imageables
end
# rails generate model abstract_imageable picture:belongs_to imageable:references{polymorphic}
class AbstractImageable
belongs_to :picture
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :abstract_imageables, as: :imageable
has_many :pictures, through: :abstract_imageables
end
class Product < ApplicationRecord
has_many :abstract_imageables, as: :imageable
has_many :pictures, through: :abstract_imageables
end
Usage:
sample = Sample.first
abstract_imageables = sample.abstract_imageables
puts abstract_imageables.first.class
# => AbstractImageable
puts abstract_imageables.first.imageable.class
# => can be either nil, or Employee, or Product, or whatever model
puts abstract_imageables.second.imageable.class
# => can be either nil, or Employee, or Product, or whatever model
# your query here, which I assumed you were trying to do because you said you wanted an `ActiveRecord::Relation` object
abstract_imageables.where(...)
I spent an hour debugging a very strange rails behavior.
Given:
app/models/user.rb
class User < ApplicationRecord
...
has_many :images
has_many :videos
...
has_many :tags
...
end
app/models/image.rb
class Image < ApplicationRecord
...
belongs_to :user
...
has_and_belongs_to_many :tags
...
include TagsFunctions
...
end
app/models/video.rb
class Video < ApplicationRecord
...
include TagsFunctions
...
belongs_to :user
...
has_and_belongs_to_many :tags
...
end
app/models/tag.rb
class Tag < ApplicationRecord
belongs_to :user
validates :text, uniqueness: {scope: :user}, presence: true
before_create :set_code
def set_code
return if self[:code].present?
loop do
self[:code] = [*'A'..'Z'].sample(8).join
break if Tag.find_by(code: self[:code]).nil?
end
end
end
app/models/concerns/tags_functions.rb
module TagsFunctions
extend ActiveSupport::Concern
# hack for new models
included do
attr_accessor :tags_after_creation
after_create -> { self.tags_string = tags_after_creation if tags_after_creation.present? }
end
def tags_string
tags.pluck(:text).join(',')
end
def tags_string=(value)
unless user
#tags_after_creation = value
return
end
#tags_after_creation = ''
self.tags = []
value.to_s.split(',').map(&:strip).each do |tag_text|
tag = user.tags.find_or_create_by(text: tag_text)
self.tags << tag
end
end
end
If I execute such code:
user = User.first
tags_string = 'test'
image = user.images.create(tags_string: tags_string)
video = user.videos.create(tags_string: tags_string)
It will give 1 item in image.tags, but 2 duplicate items in video.tags
But if we change code this way:
user = User.first
tags_string = 'test'
image = Image.create(user: user, tags_string: tags_string)
video = Video.create(user: user, tags_string: tags_string)
everything works fine, 1 tag for an image and 1 tag for a video
And even more...
If we move include TagsFunctions below has_and_belongs_to_many :tags, in a video.rb file, both code examples work fine.
I thought I know rails pretty well, but this behavior is really unclear for me.
Rails version: 5.1.1
It seems like you may want to check these 2 lines
tag = user.tags.find_or_create_by(text: tag_text)
self.tags << tag
What seems to be happening here is a tag is being create for the user but also the actual video record. But it is hard to know without seeing if there is anything in the tag model. It may be good to avoid associating the tag association with the user in the tags function.
I think what you have is an X and Y problem since the domain can be modeled better in the first place:
# rails g model tag name:string:uniq
class Tag < ApplicationRecord
has_many :taggings
has_many :tagged_items, through: :taggings, source: :resource
has_many :videos, through: :taggings, source: :resource, source_type: 'Video'
has_many :images, through: :taggings, source: :resource, source_type: 'Image'
end
# rails g model tagging tag:belongs_to tagger:belongs_to resource:belongs_to:polymorphic
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :tagger, class_name: 'User'
belongs_to :resource, polymorpic: true
end
class User < ApplicationRecord
has_many :taggings, foreign_key: 'tagger_id'
has_many :tagged_items, through: :taggings, source: :resource
has_many :tagged_videos, through: :taggings, source: :resource, source_type: 'Video'
has_many :tagged_images, through: :taggings, source: :resource, source_type: 'Image'
end
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :resource
has_many :tags, through: :taggings
end
# example
# #video.tag!('#amazeballs', '#cooking', tagger: current_user)
def tag!(*names, tagger:)
names.each do |name|
tag = Tag.find_or_create_by(name: name)
taggnings.create(tag: tag, tagger: tagger)
end
end
end
This creates a normalized tags table that we can use for lookup instead of comparing string values. has_and_belongs_to_many is only really useful in the simplest of cases where you are just joining two tables and never will need to query the join table directly (in other words using HABTM is a misstake 90% of the time).
Using callbacks to "hack into" HABTM just makes it worse.
You can use a bit of metaprogramming to cut the duplication when setting up different taggable classes.
class Video < ApplicationRecord
include Taggable
end
class Image< ApplicationRecord
include Taggable
end
I'm new to rails and working in this app where an User can create many Events. Many Users can be invited to these Events, therefore I have the following Models:
User:
class User < ActiveRecord::Base
...
has_many :events, dependent: :destroy
end
Event:
class Event < ActiveRecord::Base
belongs_to :user
has_many :event_guest
has_many :guests, :through => :event_guest, :source => :user
end
Event_Guest:
class EventGuest < ActiveRecord::Base
belongs_to :user
belongs_to :event
end
What I am looking for is being able to access (and add) the guest users to an event, for which I've tried all the variations I could thing of "Event.find(1).guests", only to get the following error:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: event_guests.event_id: SELECT "users".* FROM "users" INNER JOIN "event_guests" ON "users"."id" = "event_guests"."user_id" WHERE "event_guests"."event_id" = ? AND "users"."event_id" = 1
My event_guest migration was the following:
create_table :event_guest do |t|
t.belongs_to :user, index: true
t.belongs_to :event, index: true
end
Like #Dharam mentioned, your naming convention is incorrect. You need to rename the event_guest table.
$ rails g migration rename_event_guest_table
And then the migration looks like this:
def change
rename_table :event_guest, :event_guests
end
You then need to update your event.rb model to be
class Event < ActiveRecord::Base
belongs_to :user
has_many :event_guests
has_many :guests, :through => :event_guests, class_name: 'User'
end
And your user.rb model:
class User < ActiveRecord::Base
...
has_many :event_guests, dependent: :destroy
has_many :events, through: :event_guests
end
Move the dependent destroy off the events association and to the event_guests relationship. You don't want to destroy the event just because one guest isn't going...
Try adding self.table_name = "event_guest" to EventGuest as that's what you have in migration which is contrary to rails expectation of "event_guests" from the model name.
Rails 4.2 newbie:
2 Questions;
1) Is the first has_many redundant? Since its name is a plural of Save Class?
can I have only:
has_many :savers, through: :saves, source: :saver
Or even better;
has_many :savers, through: :saves
If the answer is yes, where can I set "dependent: :destroy"?
class Post < ActiveRecord::Base
belongs_to :user
has_many :saves, class_name: "Save", foreign_key: "saver_id", dependent: :destroy
has_many :savers, through: :saves, source: :saver
end
class Save < ActiveRecord::Base
belongs_to :saver, class_name: "User"
validates :saver_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
...
end
2) This is the typical blog model, where user can 'save' posts posted by another user to their timeline. Does this model make use best practices? Specially in db performance, doing a Join to get posts saved by a User. The 'Save' table that will have 100MM rows?
Lets first alter your example a bit to make the naming less confusing:
class User
has_many :bookmarks
has_many :posts, through: :bookmarks
end
class Post
has_many :bookmarks
has_many :users, through: :bookmarks
end
class Bookmark
belongs_to :user
belongs_to :post
end
Lets have a look at the query generated when we do #user.posts
irb(main):009:0> #user.posts
Post Load (0.2ms) SELECT "posts".* FROM "posts" INNER JOIN "bookmarks" ON "posts"."id" = "bookmarks"."post_id" WHERE "bookmarks"."user_id" = ? [["user_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy []>
Now lets comment out has_many :bookmarks and reload:
class User
# has_many :bookmarks
has_many :posts, through: :bookmarks
end
irb(main):005:0> #user.posts
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :bookmarks in model User
So no, the first has_many is not redundant - in fact its the very core of how has_many through: works. You setup a shortcut of sorts through another relation.
Note in has_many :posts, through: :bookmarks :bookmarks is the name relation we are joining through. Not the table which contains the joins.
To fix your original code you would need to do:
class Post < ActiveRecord::Base
has_many :saves, dependent: :destroy
has_many :savers, through: :saves
end
class Save < ActiveRecord::Base
belongs_to :saver, class_name: "User"
belongs_to :post # A join table with only one relation is pretty worthless.
validates :saver_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts
has_many :saves, dependent: :destroy
has_many :posts, through: :saves
end
Note that you don't need half the junk - if you have has_many :savers, through: :saves ActiveRecord will look for the relation saver by itself. Also you only want to use dependent: destroy on the join model - not on the post relation as that would remove all the posts a user has "saved" - even those written by others!
Teaching Rails myself, I want to learn the professional way to use the framework and following Rails guidelines best practices. That's not easy, because I usually find answers that 'just works'
I'll try to answer myself and maybe it could be useful for Rails Newbies:
Using has_many through, association, Rails firstly infers the association by looking at the foreign key of the form <class>_id where <class> is the lowercase of the class name, in this example; 'save_id'.
So, if we have the column name 'save_id', we will have the following simplified model:
class Post < ActiveRecord::Base
belongs_to :user
has_many :saves, through: :saves
end
class Save < ActiveRecord::Base
belongs_to :savers, class_name: "User"
validates :save_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
...
end
The model is a User, which has many Contacts. Posts and Images can be published to Contacts through a ContactPublishment.
User has the methods visible_posts and visible_images to allow easy access to Posts and Images published to.
The problem is that while user.visible_images and user.visible_posts work perfectly, specs that rely on these relations are going crazy:
If removing either the test for visible_images or visible_posts from the spec, the remaining tests pass. if I leave both, the 2nd one fails. I can switch the order of the tests, but still the 2nd one fails. weird huh?
This is the code sample, using Rails 3.2.15:
class User < ActiveRecord::Base
...
has_many :visible_posts, through: :contact_publishments, source: :publishable, source_type: 'Post'
has_many :visible_images, through: :contact_publishments, source: :publishable, source_type: 'Image'
end
class Contact < ActiveRecord::Base
...
belongs_to :represented_user, class_name: User.name
has_many :contact_publishments
end
class ContactPublishment < ActiveRecord::Base
...
belongs_to :contact
belongs_to :publishable, polymorphic: true
end
class Post < ActiveRecord::Base
...
has_many :contact_publishments, as: :publishable, dependent: :destroy
has_many :contacts, through: :contact_publishments
end
class Image < ActiveRecord::Base
...
has_many :contact_publishments, as: :publishable, dependent: :destroy
has_many :contacts, through: :contact_publishments
end
describe User do
...
it "#visible_images" do
user = create :user
image = create :image
image.contacts << create(:contact, represented_user: user)
user.visible_images.should == [image]
end
it "#visible_posts" do
user = create :user
post = create :post
post.contacts << create(:contact, represented_user: user)
user.visible_posts.should == [post]
end
end
So I solved it eventually, but not in the way I wanted. I just wrote a manual join query. The funny thing is that this triggers the same exact SQL query that my original solution triggered, but somehow only this one passes specs. Bug in Rails?
class User < ActiveRecord::Base
...
[Post, Image].each do |publishable|
define_method("visible_#{publishable.name.pluralize.underscore}") do
publishable.joins(:users).where('users.id' => self.id)
end
end
end