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?
Related
In my Rails application I have two classes :
UserPost and GroupPost (inheriting from ApplicationRecord), that I want to make subclasses of a new class, Post.
What would be the best way to add this in?
Is it as simple as making the class manually, and adding in the inheritance or are there problems that could cause?
I'm making the assumption that UserPost and GroupPost are inherited from ActiveRecord::Base, i.e. you want to save them in your DB
If you have class UserPost and GroupPost inherit from Post in the following way, this is known as Single Table Inheritance
class Post < ApplicationRecord
end
class GroupPost < Post
end
class UserPost < Post
end
Rails will, by default, expect you to have a column called "type" (can be configured to any other name) in the Posts table. GroupPost and UserPost will populate their respective values in the same table, adding in "GroupPost" or "UserPost" to the type. You can read up on it here
Rails also provides you the ability to have the parent class be "abstract", i.e. it shouldn't be able to persist to the database. Perhaps you noticed it in the the app/models/application_record.rb file as well. This way the parent serves as just a concept where you can add in common behaviour, STI will not kick in. There will need to be separate tables for children.
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
If you plan on having separate tables for the classes, I'd recommend trying composition instead of inheritance using mixins. Basically you write common functionality in a "module" and include it to the classes that need the separate functionality. Concerns are a nice way to append class methods, associations etc without having to rely on arcane ruby syntax. I'd recommend having a look at the documentation
module Fooable
extend ActiveSupport::Concern
included do |base|
scope :enabled, -> { where(enabled: true) }
has_many :boos, dependent: :destroy
end
class_methods do
def having_name
where('name ILIKE ?', "#{name}%")
end
end
def hi
return 'hi'
end
end
class UserPost < ApplicationRecord
include Fooable
end
UserPost.first.boos # relationship
UserPost.enabled # calling a scope
UserPost.having_name('name') # calling a class method
UserPost.new.hi #calling an instance method
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.
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.
More specifically, let's say I have a model
class User < ActiveRecord::Base
:has_many => (xxxImages)
end
Where xxx can be one of different models in my application. For example:
class ABCImages < ActiveRecord::Base
:belongs_to => User
end
class EFGImages < ActiveRecord::Base
:belongs_to => User
end
What I'm basically asking is: is there any way to pick one of those models at runtime to be inserted into the User models has_many association? Or do I need to take the polymorphic route (which I've only read about slightly so I'm not too familiar with it yet)
Thanks!
I think Single Table Inheritance is the way you should go which will harness the power of having different models but rather store in the same table. The only thing needed for this is to add a database field called :type.
So, in you case, I would create a table names base_images and other two classes would be just a subclass of this class.
So, the migration for this base class would look like this,
class CreateBaseImages < ActiveRecord::Migration
def change
create_table :base_images do |t|
t.string :type
t.string :url
t.references :user
t.timestamps
end
end
end
Now, after you migrate this, it will create a model base_image.rb, as below,
class BaseImage < ActiveRecord::Base
belongs_to :user
end
That's it. Now, that we have the BaseImage we will create two different models namely AbcImage and EfgImage which would inherit from BaseImage class.
class EfgImage < BaseImage
end
class AbcImage < BaseImage
end
And our user class would look like this,
class User < ActiveRecord::Base
has_many :base_images
has_many :abc_images
has_many :efg_images
end
With this code in place, you can create association called abc_images or egf_images to user through association which works like a single table. And if you were to call base_images, it would fetch all the images, irrespective of which subclasses they belong.
You will find this so much resuable that later if you intend to create hij_images association then creating class HijImage and inheriting it from the BaseImage class and it simply works. By, the way there is no magic here, rails stores the name of the class into the type field in database. And so when you query for a certain class, it creates the predicate with type and the name of the class.
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