Rails STI override model_name in parent class for all subclasses - ruby-on-rails

I am using STI in a Rails app and in order to not have to define routes for all subclasses, I put the following in each subclass:
def self.model_name
Mapping.model_name
end
In the above example, Mapping is the parent model name. Example:
class UserMapping < Mapping; end
Having to put this in each subclass is not very DRY, so I'm looking for a way to set that in the parent somehow, so that each class that inherits from the parent automatically has the model name set as the parent model name.
Perhaps there is even a better way to overcome the routing issue that arises from STI unrelated to setting the model_name - I'm open to suggestions!
Thanks in advance!

Put this in your Mapping class:
class Mapping < ActiveRecord::Base
def self.inherited(subclass)
super
def subclass.model_name
superclass.model_name
end
end
end
Afterwards, all child classes of Mapping will also inherit the parent's model_name.

Another option is to override the model_name method in the superclass to return a custom ActiveModel::Name:
class Mapping < ActiveRecord::Base
def self.model_name
ActiveModel::Name.new(base_class)
end
end
By default model_name passes the current class as the first argument to ActiveModel::Name.new, so each sub-class will receive a different name based on their class. ActiveRecord models have a base_class method which we can use instead to get the base of a single-table inheritance hierarchy.
If you cared to, you could also name it something completely different. This might be useful when you're trying to transition a model to a new name:
class Mapping < ActiveRecord::Base
def self.model_name
ActiveModel::Name.new(self, nil, "AnotherMapping")
end
end
More in the docs

Related

In a STI table/modeling schema, how can one have type specific attributes that are shared by all types?

I have a set up where I have multiple models inheriting from a Base model - standard Single Table Inheritance:
class Parent < ActiveRecord::Base
end
class A < Parent
end
class B < Parent
end
My STI setup is correct and works great! However, I want to add :type specific attributes such as description.
For example, I want all A types of Parent to have the description, "I am the A type of Parent. My function is for..."
I want to avoid replicating data over and over (having each instance of A store the same description for example).
The first thing that came to mind for this was to have a model specific method on the Subclass. So something like:
class A < Parent
def self.description
"I am the A type of Parent. My function is for..."
end
end
I don't like this solution because this really is data on the specific type of subclass (rather than on the subclass instance itself) and you get all the problems that come with making this behavior (deployments to change data, etc.)
Is this the only way to do it or are there alternatives I just am not seeing?
What about creating a model for the description?
class Description < ActiveRecord::Base
has_many :as
end
class A < Parent
belongs_to :description
before_save { description_id = 1 }
end
This way, you manage the content of description in the database and can modify it through either a web interface or migrations. Furthermore, you can easily add different descriptions for the different subclasses, or even change them per instance if that is ever required.
One downside of this approach is that you need to create the model with the correct description. One potential solution could be the before_save or before_create hook, but I'm sure those are not the only way to do it.
for your case I prefer to use ruby Duck typing as follow
class ParentAll
def talk(object1)
object1.talk
end
end
class A < ParentAll
def talk
puts 'I am the A type of Parent. My function is for...'
end
end
class B < ParentAll
def talk
puts 'I am the B type of Parent. My function is for...'
end
end
#parent = ParentAll.new
puts 'Using the A'
#parent.talk(A.new)
# this will output talk from A
puts 'Using the B'
#parent.talk(B.new)
# this will output talk from B

Rails - Create instance of a model from within another model

I have an application I'm building where I need one model to create instances of another model. I want every Car to have 4 tires.
Car model
class Car < ActiveRecord::Base
has_many :tires
after_create :make_tires
def make_tires
4.times { Tire.create(car: self.id) }
end
end
Tire model
class Tire < ActiveRecord::Base
belongs_to :car
end
However, inside of make_tires there is an error that there is no activerecord method for create or new if I try it for Tire. When I inspect Tire it doesn't have those methods.
How can I remedy this?
The error is this: undefined method 'create' for ActiveRecord::AttributeMethods::Serialization::Tire::Module
I have tested two environments: Testing and Development and they both fail for the same error.
It is a name conflict. Sit down and relax while I explain.
Solution with explanation:
In Ruby classes are just instances of class Class (which is a subclass of class Module). Instances of Module (including instances of Class) are quite weird objects, especially weird is their connection with ruby constants. You can create a new class at any point using standard ruby notation:
my_class = Class.new { attr_accessor :a }
instance = my_class.new
instance.a = 3
insatnce.a #=>
instance.class.name #=> nil
Well, our class has no name. It is just an anonymous class. How do classes get their name? By assigning it to a constant (for the first time):
MyClass = my_class
my_class.name #=> 'MyClass'
When you define class using a class keyword:
class MyClass
...
end
You just create a new instance of Class and assign it to a constant. Because of that, Ruby compiler seeing a constant has no idea whether it is a class or a number under it - it has to make a full search for that constant.
The logic behind finding a constant is quite complex and depends on the current nesting. Your case is quite simple (as there is no nesting), so ruby will try to find Tire class inside your class first and when failed it's subclasses and included modules.
Your problem is that your class inherits from ActiveRecord::Base (which is correct), which includes ActiveRecord::AttributeMethods::Serialization module, which defines Tire constant already. Hence, ruby will use this constant instead, as this is the best match for that name in given context.
To fix it, you must tell the compiler not to look within the current class but directly in the "top namespace" (which in ruby is Object. Seriously, try Object.constants) - you can do that using :: in front of your constant, like ::Tire.
Note: even though it works, this issue is a first warning for you that your code starts to smell. You should look after this ActiveRecord::AttributeMethods::Serialization::Tire::Module thingy as it seems you will encounter it more than once in the future.
Other stuff:
You can simplify your method slightly:
def make_tires
4.times { tires.create }
end
At that point you might encounter some error you had initially. If you do, then please find what is going on with that Tire::Module thing. If you don't care about the smell:
has_many :tires, class_name: '::Tire'
I'm not sure what's causing the exception you are seeing but you have a number of issues. First, you need to pass in a car instance instead of the id in make_tires. Like this:
def make_tires
4.times { Tire.create(car: self) }
end
You also need to have attr_accessible :car in the Tire model. Like this:
class Tire < ActiveRecord::Base
belongs_to :car
attr_accessible :car
end

Override ActiveRecord::Base

some of my models has a "company_id" column, that I want to set automatically. So I thought to override some method in activerecord base.
I tried this, in config/initializers, but does not work:
class ActiveRecord::Base
after_initialize :init
def init
if (self.respond_to(:company_id))
self.company_id= UserSession.find.user.company_id
end
end
end
Solution after Simone Carletti answer:
I created a module:
module WithCompany
def initialize_company
self.company_id= UserSession.find.user.company_id
end
end
And included this in the model:
class Exam < ActiveRecord::Base
include WithCompany
after_initialize :init
def init
initialize_company
end
end
Is there something else that I can do?
update 2
Best practices says to do not set session related fields in models. Use controllers for that.
There are two problems here. The first, is that you are injecting a bunch of stuff into all ActiveRecord models, whereas it would be better to add the feature only to the relevant models.
Secondary, you are breaking the MVC pattern trying to inject into the model the session context.
What you should do instead, is to code your feature in a module, and mix the module only in the relevant models. As per the context, rather than overriding the default AR behavior, add a new method where you pass the current session context (dependency injection) and returns the model initialized with the required company, when the session is set properly and the model is company-aware.

Rails gets wrong class when two classes have the same name

In my Rails I have the following models:
A STI sub-class
class Subscription::Discount < Subscription
def self.new_with_url
...
end
end
and another model class (doing completely different things, this is a STI base class)
class Discount < ActiveRecord::Base
end
So in my controller, I uses Subscription::Discount when I create users:
#user.subscription = ::Subscription::Discount.new_with_url()
However it complains: undefined method 'new_with_url' for #<Class:0x007fbb499c6740>
I think Rails is not calling the right class with new_with_url. On top of that I am not sure what #<Class:0x007fbb499c6740> is. So, two questions:
Without renaming any model, how can I reference Subscription::Discount properly?
Why is the error message saying #<Class:0x007fbb499c6740>, I can understand if it is Discount instead of that anonymous class.
EDIT:
Here are all the relevant models:
app/model/discount.rb
app/model/coffee_discount.rb (CoffeeDiscount < Discount)
app/model/subscription.rb
app/model/subscription/discount.rb (Subscription::Discount < Subscription)
The method is named create_with_url but you're calling new_with_url.
Fix the method name.

Rails & ActiveRecord: Appending methods to models that inherit from ActiveRecord::Base

I have a standard ActiveRecord model with the following:
class MyModel < ActiveRecord::Base
custom_method :first_field, :second_field
end
At the moment, that custom_method is picked up by a module sent to ActiveRecord::Base. The functionality basically works, but of course, it attaches itself to every model class, not just MyModel. So if I have MyModel and MyOtherModel in the same action, it'll assume MyOtherModel has custom_method :first_field, :second_field as well.
So, my question is: How do I attach a method (eg: def custom_method(*args)) to every class that inherits from ActiveRecord::Base, but not by attaching it to ActiveRecord::Base itself?
Any ideas appreciated.
===
Edit
The custom_method is currently attached to ActiveRecord::Base by the following:
module MyCustomModule
def self.included(base)
base.extend(self)
end
def custom_method(*args)
# Zippity doo dah - code goes here
end
end
ActiveRecord::Base.send(:include, MyCustomModule)
Do you know about descendants?
ActiveRecord::Base.descendants
You have to be sure to touch the models before calling it.
See excellent discussion here:
Is there a way to get a collection of all the Models in your Rails app?
I concur with the commentors above that you may want to consider adding your methods to the meta class, or an intermediary class, or a Module mixin.

Resources