Search code examples
rubysinatradatabase-migrationruby-datamapper

How does DataMapper's auto_upgrade work?


Can you please tell me how Data Mapper's auto upgrade thing works?

For example:

include 'data_mapper'
DataMapper.setup :default, "sqlite://#{Dir.pwd}/base.db"

class User
   include DataMapper::Resource
   property :id, Serial
   property :name, String
end

DataMapper.auto_upgrade!

Here, how does that Data Mapper's auto upgrade understand that they have to create a Data Base


Solution

  • First of all, the User class is not a database, it is a model, which is linked to a table in a database.

    When you include DataMapper::Resource, it will automatically call extend DataMapper::Model:

    # https://github.com/datamapper/dm-core/blob/master/lib/dm-core/resource.rb#L55-L58
    
    module DataMapper
      module Resource
        # ...
    
        def self.included(model)
          model.extend Model
          super
        end
    
        # ...
      end
    end
    

    this call to extend is intercepted by another hook in DataMapper::Model, which keeps track of the module's descendants:

    # https://github.com/datamapper/dm-core/blob/master/lib/dm-core/model.rb#L209-L223
    
    module DataMapper
      module Model
        # ...
    
        def self.descendants
          @descendants ||= DescendantSet.new
        end
    
        # ...
    
        def self.extended(descendant)
          descendants << descendant
          # ...
          super
        end
    
        # ...
      end
    end
    

    later, DataMapper can find all models via DataMapper::Model.descendants:

    # https://github.com/datamapper/dm-migrations/blob/8bfcec08286a12ceee1bc3e5a01da3b5b7d4a74d/lib/dm-migrations/auto_migration.rb#L43-L50
    
    module DataMapper
      module Migrations
        module SingletonMethods
          # ...
    
          def auto_upgrade!(repository_name = nil)
            repository_execute(:auto_upgrade!, repository_name)
          end
    
          # ...
    
          def repository_execute(method, repository_name)
            models = DataMapper::Model.descendants
            models = models.select { |m| m.default_repository_name == repository_name } if repository_name
            models.each do |model|
              model.send(method, model.default_repository_name)
            end
          end
    
          # ...
        end
      end
    end
    

    Here's a minimal example:

    module Model
      def self.descendants
        @descendants ||= []
      end
    
      def self.extended(base)
        descendants << base
        super
      end
    end
    
    module Resource
      def self.included(base)
        base.extend(Model)
      end
    end
    
    class Car
      include Resource
    end
    
    class Bike
      include Resource
    end
    
    class Train
      include Resource
    end
    
    Model.descendants
    #=> [Car, Bike, Train]