Home > Blockchain >  Dynamic classes in Ruby, Class.new, unpacking Sequel::Model
Dynamic classes in Ruby, Class.new, unpacking Sequel::Model

Time:12-16

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
  • Related