Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 397
user3162553 Avatar asked Jan 18 '16 22:01

user3162553


1 Answers

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'
like image 87
BroiSatse Avatar answered Oct 16 '22 13:10

BroiSatse