Why 'include' position in Rails model affets HABTM behavior? - ruby-on-rails

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

Related

has many relationship for polymorphic association

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(...)

How to create seeds of a polymorphic relationship

I have three objects:
class Picture < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
class Employee < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
How should I create test seed data, to associate an image with both a seed employee and seed product ?
to associate an image with both a seed employee and seed product
This would require a many-to-many relationship (either has_and_belongs_to_many or has_many :through):
#app/models/product.rb
class Product < ActiveRecord::Base
has_many :images, as: :imageable
has_many :pictures, through: :images
end
#app/models/employee.rb
class Employee < ActiveRecord::Base
has_many :images, as: :imageable
has_many :pictures, through: :images
end
#app/models/image.rb
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
belongs_to :picture
end
#app/models/picture.rb
class Picture < ActiveRecord::Base
has_many :images
end
This would allow you to use:
#db/seed.rb
#employee = Employee.find_or_create_by x: "y"
#picture = #employee.pictures.find_or_create_by file: x
#product = Product.find_or_create_by x: "y"
#product.pictures << #picture
ActiveRecord, has_many :through, and Polymorphic Associations
Because you're using a polymorphic relationship, you won't be able to use has_and_belongs_to_many.
The above will set the polymorphism on the join table; each Picture being "naked" (without a "creator"). Some hacking would be required to define the original creator of the image.
Create them from the has_many end instead:
employee = Employee.create! fields: 'values'
employee.pictures.create! fields: 'values'
product = Product.create! fields: 'values'
product.pictures.create! fields: 'values'
Although just one quick note: when seeding, you may already have the data you want in the database, so I would use instance = Model.where(find_by: 'values').first_or_create(create_with: 'values') instead.
NB. I've just noticed: you're not trying to associate one image with multiple owners, are you? Because each image only belongs to one Imageable, and that is either an Employee or a Product. If you want to do that, you'll have to set up a many-to-many join.

Best way to categorize products in rails 4 app

So, I'm trying to create a product categorization 'system' in my rails 4 app.
Here's what I have so far:
class Category < ActiveRecord::Base
has_many :products, through: :categorizations
has_many :categorizations
end
class Product < ActiveRecord::Base
include ActionView::Helpers
has_many :categories, through: :categorizations
has_many :categorizations
end
class Categorization < ActiveRecord::Base
belongs_to :category
belongs_to :product
end
Also, what gem should I use? (awesome_nested_set, has_ancestry)
Thanks!
This is what I did in one of my projects which is live right now and works very well.
First the category model, it has a name attribute and I am using a gem acts_as_tree so that categories can have sub categories.
class Category < ActiveRecord::Base
acts_as_tree order: :name
has_many :categoricals
validates :name, uniqueness: { case_sensitive: false }, presence: true
end
Then we will add something called a categorical model which is a link between any entity(products) that is categorizable and the category. Note here, that the categorizable is polymorphic.
class Categorical < ActiveRecord::Base
belongs_to :category
belongs_to :categorizable, polymorphic: true
validates_presence_of :category, :categorizable
end
Now once we have both of these models set up we will add a concern that can make any entity categorizable in nature, be it products, users, etc.
module Categorizable
extend ActiveSupport::Concern
included do
has_many :categoricals, as: :categorizable
has_many :categories, through: :categoricals
end
def add_to_category(category)
self.categoricals.create(category: category)
end
def remove_from_category(category)
self.categoricals.find_by(category: category).maybe.destroy
end
module ClassMethods
end
end
Now we just include it in a model to make it categorizable.
class Product < ActiveRecord::Base
include Categorizable
end
The usage would be something like this
p = Product.find(1000) # returns a product, Ferrari
c = Category.find_by(name: 'car') # returns the category car
p.add_to_category(c) # associate each other
p.categories # will return all the categories the product belongs to

Dynamic has_many class_name using polymorphic reference

I am trying to associate a polymorphic model (in this case Product) to a dynamic class name (either StoreOnePurchase or StoreTwoPurchase) based on the store_type polymorphic reference column on the products table.
class Product < ActiveRecord::Base
belongs_to :store, polymorphic: true
has_many :purchases, class_name: (StoreOnePurchase|StoreTwoPurchase)
end
class StoreOne < ActiveRecord::Base
has_many :products, as: :store
has_many :purchases, through: :products
end
class StoreOnePurchase < ActiveRecord::Base
belongs_to :product
end
class StoreTwo < ActiveRecord::Base
has_many :products, as: :store
has_many :purchases, through: :products
end
class StoreTwoPurchase < ActiveRecord::Base
belongs_to :product
end
StoreOnePurchase and StoreTwoPurchase have to be separate models because they contain very different table structure, as does StoreOne and StoreTwo.
I am aware that introducing a HABTM relationship could solve this like this:
class ProductPurchase < ActiveRecord::Base
belongs_to :product
belongs_to :purchase, polymorphic: true
end
class Product < ActiveRecord::Base
belongs_to :store, polymorphic: true
has_many :product_purchases
end
class StoreOnePurchase < ActiveRecord::Base
has_one :product_purchase, as: :purchase
delegate :product, to: :product_purchase
end
However I am interested to see if it is possible without an extra table?
Very interesting question. But, unfortunately, it is impossible without an extra table, because there is no polymorphic has_many association. Rails won't be able to determine type of the Product.purchases (has_many) dynamically the same way it does it for Product.store (belongs_to). Because there's no purchases_type column in Product and no support of any dynamically-resolved association types in has_many. You can do some trick like the following:
class Product < ActiveRecord::Base
class DynamicStoreClass
def to_s
#return 'StoreOnePurchase' or 'StoreTwoPurchase'
end
end
belongs_to :store, polymorphic: true
has_many :purchases, class_name: DynamicStoreClass
end
It will not throw an error, but it is useless, since it will call DynamicStoreClass.to_s only once, before instantiating the products.
You can also override ActiveRecord::Associations::association to support polymorphic types in your class, but it is reinventing the Rails.
I would rather change the database schema.

multiple has_many-through to same polymorphic table, but with different source class, fails in tests but work in reality

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

Resources