I am new to ruby and trying to figure out how the class declaration below from Sequel gem works behind the scene.
class Enumeration < Sequel::Model(DB[:enumerations]); end
Upon quick investigation of Sequel gem's code, it seems to me that the module method Sequel::Model returns a class instance with a configured class attribute. The return instance of Class is then used in inheritance hierarchy, so I tried to test my understanding through the code;
module MySequel
class MyModel
module ClassMethods
attr_accessor :table_name
def model(table_name)
klass = Class.new(self)
klass.table_name = table_name
puts klass.table_name # prints table_name, it does get set for the first class Class object klass
klass
end
end
extend ClassMethods
end
end
class Enumeration < MySequel::MyModel.model(:enumerations); end
class EnumerationValue < MySequel::MyModel.model(:enumeration_values); end
p Enumeration.table_name # prints nil
p EnumerationValue.table_name # prints nil
Based on my understanding the class variable table_name gets set while creating an instance of Class and gets propagated into it's child class. However, that doesn't seem to be the case.
Can someone please explain the concept behind Sequel::Model and the problem with my sample implementation to achieve the same result.
The behaviour in this example is similar to when using the anonymous classes above.
class TestModel
@@table_name = 'test'
def TestModel.table_name
@@table_name
end
def TestModel.table_name=(value)
@@table_name = value
end
end
TestModel.table_name = 'posts'
p TestModel.table_name # prints posts
class ChildTestModel < TestModel; end
puts Enumeration.table_name # prints nil
The Sequel library is seemingly propagating the information about the dataset set through call to module method to the child class (through anonymous class.)
However, I can't figure out; what construct of the Ruby language allows the Status class below to know that it's restricted to a record in table enumerations with name set to 'status'.
class Status < Sequel::Model(DB[:enumerations].where(name: 'status')); end
CodePudding user response:
So, majority of the code is correct and your assumption on how it all works is correct. Which is really great and definitely not a novice level of ruby!
So, what doesn't work. When you define a subclass of some class, the subclass inherits all the "class methods" (in quotes as there is really no such a thing), but instance variables are not inherited (that is - instance variable of an instance of a class Class):
class A
@foo = 1
end
A.instance_variables #=> [:@foo]
class B < A; end
B.instance_variables #=> []
That mean, that if you invoke a setter on the anonymous class you created in your model
method, it sets the instance variable on that class only. And that anonymous class's subclasses do not inherit that. You could see that this would work:
Enumeration = MySequel::MyModel.model(:enumerations)
EnumerationValue = MySequel::MyModel.model(:enumeration_values)
Enumeration.table_name #=> :enumerations
EnumerationValue.table_name #=> :enumeration_values
And to be honest, there's really not much gain subclassing your anonymous classes here. This would only make sense if you also had been caching those classes and when separate calls to that method had returned the same Class object twice.
If you still want the table_name
to be inheritable, you need to be very explicit about this and instead of using attr_accessor
, you need to look directly into a superclass:
attr_setter :table_name
def table_name
@table_name || (superclass.table_name if superclass.respond_to?(:table_name))
end
There are a few tweaks you might want to consider here. The above version
CodePudding user response:
The Sequel library is seemingly propagating the information about the dataset set through call to module method to the child class
Yes, indeed. Sequel uses the inherited
callback to copy the class instance variables over to the subclass (see lib/sequel/model/base.rb#843
).
Here's a very basic version for your example class:
module MySequel
class MyModel
module ClassMethods
attr_accessor :table_name
def model(table_name)
klass = Class.new(self)
klass.table_name = table_name
klass
end
def inherited(subclass)
instance_variables.each do |var|
value = instance_variable_get(var)
subclass.instance_variable_set(var, value)
end
end
end
extend ClassMethods
end
end
As a result, the subclasses will have their class instance variables (here @table_name
) set to the same values as the (anonymous) parent class:
class Enumeration < MySequel::MyModel.model(:enumerations); end
class EnumerationValue < MySequel::MyModel.model(:enumeration_values); end
p Enumeration.table_name #=> :enumerations
p EnumerationValue.table_name #=> :enumeration_values
CodePudding user response:
The table name is set when defining the Enumeration or EnumerationValue and is kept within the parent class (which is an anonymous class).
It is much simpler to grasp the concept, for me personally when comparing it with prototypical inheritance from javascript.
It can be accessed through the superclass of Enumeration and EnumerationValue class.
# frozen_string_literal: true
module MySequel
class MyModel
module ClassMethods
attr_accessor :table_name
def model(table_name)
klass = Class.new(self)
klass.table_name = table_name
klass
end
end
extend ClassMethods
end
end
class Enumeration < MySequel::MyModel.model(:enumerations); end
class EnumerationValue < MySequel::MyModel.model(:enumeration_values); end
p Enumeration.superclass.table_name # prints :enumerations
p EnumerationValue.superclass.table_name # prints :enumeration_values