How to simplify association name? - ruby-on-rails

I have two models:
# /app/models/service_user.rb
class ServiceUser < ActiveRecord::Base
has_many :service_user_images
end
# /app/models/service_user_image.rb
class ServiceUserImage < ActiveRecord::Base
belongs_to :service_user
end
And when I want get all images for a service user I need to call:
#service_user.service_user_images
#^^^^^^^^^^^^^^^^^^^^^^^^^
How I can simplify the name 'service_user_images' to 'images' exactly for ServiceUser model? And should I do it? Are calls like 'service_user.service_user_xxxs' normal for ActiveRecord?
Now I see three solutions:
1) using with the ':class_name' option, but I think that it's overhead for my code and it will make the code dirtier if I have a few associations
2) using with method alias, but I don't think that it's a good solution, because ActiveRecord creates multiple methods for each association and I will be forced to use too many aliases
3) using nested classes like 'ServiceUser::Image', but it creates another difficulties, for example I need to define the name of this class in each factory (I use FactoryGirl), because it translates names like ':service_user_image' to 'ServiceUserImage' by default:
FactoryGirl.define do
factory :service_user_image do |image| # works for ServiceUserImage model, does not work for ServiceUser::Image model
# ...
end
factory :service_user_image, class: ServiceUser::Image do |image| # works for ServiceUser::Image model
# ...
end
end
Ideally I search solution like:
class ServiceUser < ActiveRecord::Base
has_many :service_user_images, alias: :images # JUST EXAMPLE! DO NOT USE THIS!
end
Now I found solution:
class ServiceUser < ActiveRecord::Base
has_many :service_user_images
alias_attribute :images, :service_user_images
end
It works perfectly and it has no "side effects", which were described by me.

Related

Automatically creating missing join models

Lately I've grown weary of littering my app/models directory with pointless boilerplate models such as:
Join models that always contain a couple belongs_tos and nothing else.
Status log models that just include SomeConcern and make a couple macro calls.
Revision tracking models that again, just include a concern and call a macro.
These models only exist to support has_many and has_many ... through: associations.
Adding model concerns that generate these models as needed clears simplifies the app/models directory. So instead of:
has_many :model_things
has_many :things, through: :model_things
and a trivial app/models/model_thing.rb that says:
class ModelThing < ApplicationRecord
belongs_to :model
belongs_to :thing
end
I can have a ThingSupport concern with a has_things macro that:
Creates the has_many :model_things association based on the class name and some options to has_things.
Creates the has_many :things, through: :model_things association.
Find or create the Model::Thing (see below for why this name is used) class with a call like:
ModuleUtil.find_or_create(join_model_name) do
Class.new(ApplicationRecord) do
# Set the table name, call belongs_to as needed, call concern methods, ...
end
end
where ModuleUtil.find_or_create is a simple method that uses String#constantize to find the desired module (if it exists) or create it using the block and Object#const_set if it can't be found.
All the model and association names can be built using the usual Rails conventions from the caller's class name and some options to has_things for special cases.
The question is am I playing with fire here? What can go wrong with this sort of chicanery?
One problem that I've already come across is that the model classes that are generated don't exist on their own so they cannot be directly referenced from an ActiveJob (such as a deliver_later mailer). For example, if loading Model creates the ModelThing association model then you can't reference a ModelThing in a mailer argument because ActiveJob won't know that you have to load the Model class before ModelThing exists. However, this can be solved by using Model::Thing instead so that constantize will look for Model (and find it in app/models/model.rb) before trying to find Model::Thing (which will exist because constantize will have just loaded Model which creates Model::Thing). Am I missing something else?
I have no idea if I'm following you or not. So, if this is way off target, please say so and I'll delete.
Focusing in on the join model bit, I also got tired of that flim flam. So, I created a model like:
module ActsAsHaving
class HasA < ActiveRecord::Base
validates :haser_type, :haser_id, :hased_type, :hased_id, presence: true
belongs_to :hased, polymorphic: true
belongs_to :haser, polymorphic: true
acts_as_taggable
def haser=(thing)
self.haser_type = thing.class.name
self.haser_id = thing.id
end
def haser
haser_type.constantize.find_by(id: haser_id)
end
def hased=(thing)
self.hased_type = thing.class.name
self.hased_id = thing.id
end
def hased
hased_type.constantize.find_by(id: hased_id)
end
end
end
I didn't use the built-in accessors and validations because I sometimes use this to join non-AR records (which I grab from remote API services some of which belong to me and some of which don't but that's a longer story).
Anyway, I then wrote an acts_as_having macro that let me do stuff like:
class Person < ActiveRecord::Base
acts_as_having :health_events, class_name: "Foo::Event", tag_with: "health_event", remote: true
acts_as_having :program_events, class_name: "Foo::Event", tag_with: "program_event", remote: true
acts_as_having :email_addresses, :phone_numbers, :physical_addresses
end
Which gives me stuff like:
#person.email_addresses
#person.email_addresses << #email_address
etc...
I can do the inverse like:
class EmailAddress < ActiveRecord::Base
acts_as_had_by :person
end
Which gives me stuff like:
#email_address.person
etc...
Then, I wrapped all that junk up into a gem. Now I rarely create join models unless they have some specific requirements that I can't shoe horn into my acts_as_having bit.
Anyway, I don't know if it's playing with fire or not. I don't even know if I'm making sense or addressing your concept. But, I started my gem about three years ago and I haven't regretted it. So, there's that.

Rails don't save if duplicate

I have a somewhat complex Rails model setup that I'll try to simplify as much as possible. The goal of this setup is to be able to have objects (Person, Pet) that are long-lived, but with relationships between them changing each year via TemporalLink. Basically, I have these models:
class Person < ActiveRecord::Base
include TemporalObj
has_many :pet_links, class_name: "PetOwnerLink"
has_many :pets, through: :pet_links
end
class Pet < ActiveRecord::Base
include TemporalObj
has_many :owner_links, class_name: "PetOwnerLink"
has_many :owners, through: :owner_links
end
class PetOwnerLink < ActiveRecord::Base
include TemporalLink
belongs_to :owner
belongs_to :pet
end
and these concerns:
module TemporalLink
extend ActiveSupport::Concern
# Everything that extends TemporalLink must have a `year` attribute.
end
module TemporalObj
extend ActiveSupport::Concern
# Everything that extends TemporalObj must have a find_existing() method.
####################
# Here be dragons! #
####################
end
The desired behavior is:
When creating a TemporalObj (Pet, Person):
1) Check to see if there is an existing one, based on certain conditions, with find_existing().
2) If an existing duplicate is found, don't perform the create but still perform necessary creations to associated objects. (This seems to be the tricky part.)
3) If no duplicate is found, perform the create.
4) [Existing magic already auto-creates the necessary TemporalLink objects.]
When destroying a TemporalObj:
1) Check to see if the object exists in more than one year. (This is simpler in actuality than in this example.)
2) If the object exists in only one year, destroy it and associated TemporalLinks.
3) If the object exists in more than one year, just destroy one of the TemporalLinks.
My problem is I have uniqueness validations on many TemporalObjs, so when I try to create a new duplicate, the validation fails before I can perform any around_create magic. Any thoughts on how I can wrangle this to work?
You can (and should) use Rails' built-in validations here. What you've described is validates_uniqueness_of, which you can scope to include multiple columns.
For example:
class TeacherSchedule < ActiveRecord::Base
validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
end
http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of
In response to JacobEvelyn's comment, this is what I did.
Created a custom validate like so
def maintain_uniqueness
matching_thing = Thing.find_by(criteria1: self.criteria1, criteria2: self.criteria2)
if !!matching_thing
self.created_at = matching_thing.created_at
matching_thing.delete
end
true
end
Added it to my validations
validate :maintain_event_uniqueness
It worked.

How to add methods to a has_many collection

Assuming a typical has_many association
class Customer < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :customer
end
How can I add a method to the collection of orders? For the sake of code organization, I'm trying to reactor this method (this is a made-up example) inside of my Customer class:
def update_orders
ThirdPartyAPI.look_up(self.orders) do |order|
# Do stuff to the orders
# May need to access 'self', the Customer...
end
end
I don't like this because it puts a lot of knowledge about the Order class inside my Customer class. I can't use an instance method off of an order, since the ThirdPartyAPI can do a batch lookup on multiple orders. I could make a static method off of Order and pass in the array of orders, and their parent customer, but this feels clunky.
I found this in the rails docs, but I couldn't find any good examples of how to use this in practice. Are there any other ways?
I think this should do it
has_many :entities do
def custom_function here
end
def custom_function here
end
end

Why polymorphic association doesn't work for STI if type column of the polymorphic association doesn't point to the base model of STI?

I have a case of polymorphic association and STI here.
# app/models/car.rb
class Car < ActiveRecord::Base
belongs_to :borrowable, :polymorphic => true
end
# app/models/staff.rb
class Staff < ActiveRecord::Base
has_one :car, :as => :borrowable, :dependent => :destroy
end
# app/models/guard.rb
class Guard < Staff
end
In order for the polymorphic assocation to work, according to the API documentation on Polymorphic Assocation, http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations that I have to set borrowable_type to the base_classof STI models, that is in my case is Staff.
The question is: Why doesn't it work if the borrowable_type set to STI class?
Some test to prove it:
# now the test speaks only truth
# test/fixtures/cars.yml
one:
name: Enzo
borrowable: staff (Staff)
two:
name: Mustang
borrowable: guard (Guard)
# test/fixtures/staffs.yml
staff:
name: Jullia Gillard
guard:
name: Joni Bravo
type: Guard
# test/units/car_test.rb
require 'test_helper'
class CarTest < ActiveSupport::TestCase
setup do
#staff = staffs(:staff)
#guard = staffs(:guard)
end
test "should be destroyed if an associated staff is destroyed" do
assert_difference('Car.count', -1) do
#staff.destroy
end
end
test "should be destroyed if an associated guard is destroyed" do
assert_difference('Car.count', -1) do
#guard.destroy
end
end
end
But it seems to be true only with Staff instance. The results are:
# Running tests:
F.
Finished tests in 0.146657s, 13.6373 tests/s, 13.6373 assertions/s.
1) Failure:
test_should_be_destroyed_if_an_associated_guard_is_destroyed(CarTest) [/private/tmp/guineapig/test/unit/car_test.rb:16]:
"Car.count" didn't change by -1.
<1> expected but was
<2>.
Thanks
Good question. I had exactly the same problem using Rails 3.1. Looks like you can not do this, because it does not work. Probably it is an intended behavior. Apparently, using polymorphic associations in combination with Single Table Inheritance (STI) in Rails is a bit complicated.
The current Rails documentation for Rails 3.2 gives this advice for combining polymorphic associations and STI:
Using polymorphic associations in combination with single table
inheritance (STI) is a little tricky. In order for the associations to
work as expected, ensure that you store the base model for the STI
models in the type column of the polymorphic association.
In your case the base model would be "Staff", i.e. "borrowable_type" should be "Staff" for all items, not "Guard". It is possible to make the derived class appear as the base class by using "becomes" : guard.becomes(Staff). One could set the column "borrowable_type" directly to the base class "Staff", or as the Rails Documentation suggests, convert it automatically using
class Car < ActiveRecord::Base
..
def borrowable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
An older question, but the issue in Rails 4 still remains. Another option is to dynamically create/overwrite the _type method with a concern. This would be useful if your app uses multiple polymorphic associations with STI and you want to keep the logic in one place.
This concern will grab all polymorphic associations and ensure that the record is always saved using the base class.
# models/concerns/single_table_polymorphic.rb
module SingleTablePolymorphic
extend ActiveSupport::Concern
included do
self.reflect_on_all_associations.select{|a| a.options[:polymorphic]}.map(&:name).each do |name|
define_method "#{name.to_s}_type=" do |class_name|
super(class_name.constantize.base_class.name)
end
end
end
end
Then just include it in your model:
class Car < ActiveRecord::Base
belongs_to :borrowable, :polymorphic => true
include SingleTablePolymorphic
end
Just had this issue in Rails 4.2. I found two ways to resolve:
--
The problem is that Rails uses the base_class name of the STI relationship.
The reason for this has been documented in the other answers, but the gist is that the core team seem to feel that you should be able to reference the table rather than the class for a polymorphic STI association.
I disagree with this idea, but am not part of the Rails Core team, so don't have much input into resolving it.
There are two ways to fix it:
--
1) Insert at model-level:
class Association < ActiveRecord::Base
belongs_to :associatiable, polymorphic: true
belongs_to :associated, polymorphic: true
before_validation :set_type
def set_type
self.associated_type = associated.class.name
end
end
This will change the {x}_type record before the creation of the data into the db. This works very well, and still retains the polymorphic nature of the association.
2) Override Core ActiveRecord methods
#app/config/initializers/sti_base.rb
require "active_record"
require "active_record_extension"
ActiveRecord::Base.store_base_sti_class = false
#lib/active_record_extension.rb
module ActiveRecordExtension #-> http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase
extend ActiveSupport::Concern
included do
class_attribute :store_base_sti_class
self.store_base_sti_class = true
end
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)
####
module AddPolymorphic
extend ActiveSupport::Concern
included do #-> http://stackoverflow.com/questions/28214874/overriding-methods-in-an-activesupportconcern-module-which-are-defined-by-a-cl
define_method :replace_keys do |record=nil|
super(record)
owner[reflection.foreign_type] = ActiveRecord::Base.store_base_sti_class ? record.class.base_class.name : record.class.name
end
end
end
ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, AddPolymorphic)
A more systemic way to fix the issue is to edit the ActiveRecord core methods which govern it. I used references in this gem to find out which elements needed to be fixed / overridden.
This is untested and still needs extensions for some of the other parts of the ActiveRecord core methods, but seems to work for my local system.
There is a gem.
https://github.com/appfolio/store_base_sti_class
Tested and it works on various versions of AR.
You can also build a custom scope for a has_* association for the polymorphic type:
class Staff < ActiveRecord::Base
has_one :car,
->(s) { where(cars: { borrowable_type: s.class }, # defaults to base_class
foreign_key: :borrowable_id,
:dependent => :destroy
end
Since polymorphic joins use a composite foreign key (*_id and *_type) you need to specify the type clause with the correct value. The _id though should work with just the foreign_key declaration specifying the name of the polymorphic association.
Because of the nature of polymorphism it can be frustrating to know what models are borrowables, since it could conceivably be any model in your Rails application. This relationship will need to be declared in any model where you want the cascade deletion on borrowable to be enforced.
This is how I solved that problem using aforementioned hints:
# app/models/concerns/belongs_to_single_table_polymorphic.rb
module BelongsToSingleTablePolymorphic
extend ActiveSupport::Concern
included do
def self.belongs_to_sti_polymorphic(model)
class_eval "belongs_to :#{model}, polymorphic: true"
class_eval 'before_validation :set_sti_object_type'
define_method('set_sti_object_type') do
sti_type = send(model).class.name
send("#{model}_type=", sti_type)
end
end
end
end
and with that, for any model in which I would find belongs_to :whatever, polymorphic: true I do:
class Reservation < ActiveRecord::Base
include BelongsToSingleTablePolymorphic
# .....
belongs_to_sti_polymorphic :whatever
# .....
end
I agree with the general comments that this ought to be easier. That said, here is what worked for me.
I have a model with Firm as the base class and Customer and Prospect as the STI classes, as so:
class Firm
end
class Customer < Firm
end
class Prospect < Firm
end
I also have a polymorphic class, Opportunity, which looks like this:
class Opportunity
belongs_to :opportunistic, polymorphic: true
end
I want to refer to opportunities as either
customer.opportunities
or
prospect.opportunities
To do that I changed the models as follows.
class Firm
has_many opportunities, as: :opportunistic
end
class Opportunity
belongs_to :customer, class_name: 'Firm', foreign_key: :opportunistic_id
belongs_to :prospect, class_name: 'Firm', foreign_key: :opportunistic_id
end
I save opportunities with an opportunistic_type of 'Firm' (the base class) and the respective customer or prospect id as the opportunistic_id.
Now I can get customer.opportunities and prospect.opportunities exactly as I want.

rails model templates (or instance inheritance) options?

I have a situation where I want to make 'parametric' models in rails; for example I'd like to define PrototypeRecipe, and then be able to make multiple DerivedRecipe's; maybe one derived recipe uses more sugar and another uses less eggs or something. The critical point is that I want all the 'derived' instances to inherit properties from a single shared PrototypeRecipe, but be able to make local modifications.
Ideally, I'd like to be able to define methods on the prototype (say, putting together a shopping list), and have these methods respond to local changes in derived instances (so if I specified 3 eggs instead of 2, i could call the prototype's make_shopping_list function and it would reflect that).
Is there an existing method for accomplishing something like this? Here's the best I can come up with so far:
class Ingredient << ActiveRecord::Base
belongs_to :recipe, :polymorphic => true
# uuid => UUID String (for grouping ingredients which change between prototype and derived instances)
end
class PrototypeRecipe << ActiveRecord::Base
has_many :ingredients
def make_ingredient_list(derived_recipe = nil)
self.ingredients.map {|i| derived_recipe.nil? ? i : derived_recipe.ingredients.where(:ingredient_uuid => i.uuid).first }
end
end
class DerivedRecipe << ActiveRecord::Base
belongs_to :prototype_recipe
has_many :ingredients
def method_missing(sym, *args)
self.prototype_recipe.send( sym, *args, self)
end
end
I know this code can be made a lot cleaner, I'm more wondering if the general approach can be improved on. The basic idea is that ingredients would each have a unique ID. To modify a prototype recipe, you simply create an instance of DerivedRecipe, link it to the prototype, and then add an ingredient with the same UUID as one of the prototype's ingredients.
I'm not 100% on what behavior you are looking to have, so here's my attempted solution.
Single-Table Inheritance (STI). Your base class will be PrototypeRecipe and your child class will be DerivedRecipe.
In your prototype_recipes table, specify a type column (text). This signals to Rails you want to use STI. If you put your make_ingredients_list method inside the base class, it will be accessible from your child classes.
# app/models/ingredient.rb
class Ingredient < ActiveRecord::Base
belongs_to :recipe, :class_name => "PrototypeRecipe"
...
end
# app/models/prototype_recipe.rb
class PrototypeRecipe < ActiveRecord::Base
has_many :ingredients
has_many :derived_recipes
def make_ingredient_list
...
end
end
# app/models/derived_recipe.rb
class DerivedRecipe < PrototypeRecipe
belongs_to :prototype_recipe
end
Now you can do something like:
#cupcakes = PrototypeRecipe.create
#cupcakes_with_extra_eggs = #cupcakes.derived_recipes.create
print #cupcakes_with_extra_eggs.make_ingredient_list
Is this what you were looking for?

Resources