nandosola / dilithium-rb

A tiny framework to power your enterprise-ish stuff in Ruby
BSD 3-Clause "New" or "Revised" License
3 stars 3 forks source link

Implement Class Table Inheritance #40

Closed nandosola closed 10 years ago

nandosola commented 10 years ago

Depends on #41

Proposed configuration from an API client:

     PersistenceService.configure do |service|
        service.db = DatabaseService.db

        service.inheritance_mappers(
            # Inheritance roots
            :BaseEntity => :class,  #default
            :'Qux::Foo' => :leaf, #or :single
            :Baz=> :embed,  #or :reference, for document-based repositories, i.e. MongoDB
            #Inheritance mapping in :Baz, :Bat wouldn't apply (the whole hierarchy is saved)
        )

        # Future example
        service.persistence(
            # Relative to inheritance roots
            :BaseEntity => :pg_sql,   # default
            :'Qux::Foo' => :pg_sql,
            :Bar => :memory,
            :Baz => :pg_json,
            :Bat => :pstore, file:/var/myapp/foo.pstore  # why not?
        )
      end

No namespace-resolution is performed. The symbols are translated to class names at runtime. Please observe, that the only repository where inheritance mapping makes sense is a RDBMS (ie. Postgres) or a JSON-like repository (ie. MongoDB) since the :memory repository would have no persistence at all.

The Mapper uses a Strategy pattern for mapping entitites/attributes to tables. We would have an internal registry that maps inheritance roots to persistence types (which DB, which type of persistence, etc.). Registry keys have to be symbols, not classes, since all entity classes might not be loaded at configuration time.

One Strategy per inheritance hierarchy. We will not allow redefining per subclass (except for direct subclasses of BaseEntity). The default type of inheritance will be that assigned to BaseEntity.

Currently the Transaction constructor receives the Mapper instance. Probably change all references to @mapper.foo in the UoW to @mapper.for(klazz).foo

Finder classes (Repository) must inherit from parent class' finder classes but must redefine methods to get the correct class. Perhaps rethink the way Finders are implemented. (see #41)

We will probably need a type attribute even with CTI to make polymorphic finders work without having to read all tables in the inheritance hierarchy.

Careful with intermediate table names!

mcamou commented 10 years ago

The parametrization has to do with how class hierarchies are stored, not with how roots are stored. For example:

class User < BaseEntity
  children :computer 
  ...
end

class AdminUser < User
  ...
end

class Computer < BaseEntity
  parent :user
  ...
end

class Laptop < Computer
  ...
end

service.inheritance_mappers(
  :BaseEntity => :class,
  :User => :single
) 
# Computer and Laptop are persisted using Class-Table Inheritance (the default)
# User and AdminUser are persisted using Single-Table Inheritance

service.repositories(
  :BaseEntity => :pg_sql,
  :User => :pg_json
)
# Computer and Laptop are persisted in pg_sql (the default)
# User and AdminUser are persisted in pg_json
nandosola commented 10 years ago

Another GOTCHA here would be mapping the PKs to Integer(RDBMS), Strings(Mongo) or Ruby's Object#object_id. Be careful with that. Oh! And what about composite PKs?

Anyhow, IMHO we could deal with this later....

nandosola commented 10 years ago

When implementing the feature, please make sure Version is dealt with as an Active Record object so that all locking behavior is confined in a single object (no mapper, no repository)

mcamou commented 10 years ago

We should translate this and put it in the Wiki:

Por ejemplo:

class Person < Dilithium::BaseEntity
  attribute :name, String
  attribute :email, String
end

class User < Person
  attribute :password, String
end

class Admin < User
  attribute :organization, String
end
  1. Hay que configurar el tipo de herencia que se quiere para cada árbol de herencia. En este momento soportamos:

Leaf-table inheritance: Cada clase tiene todos sus datos en una tabla (que es lo que había antes de éste último cambio). Cada subclase tiene su propia secuencia de id's. En este caso tendríamos que tener las siguientes tablas:

persons: id, _version, active, name, email users: id, _version, active, name, email, password admins: id, _version, active, name, email, password, organization

Class-table inheritance: Cada clase tiene una tabla, con los datos comunes en la tabla de la superclase. Existe una única secuencia de id's para toda la jerarquía de clases. Las tablas serían las siguientes:

persons: id, _version, _type, active, name, email users: id, password admins: id, organization

En la tabla que representa la raíz de la jerarquía de herencia se agrega la columna _type que > contiene el nombre de la tabla que corresponde a la clase real del objeto. Por ejemplo, si un usuario es de tipo Admin, en la tabla persons la columna _type contendrá "admins". Es una columna de tipo varchar.

Para configurar el tipo de herencia, la issue https://github.com/nandosola/dilithium-rb/issues/40 pone algunas cosas a futuro y otras que fueron propuestas en su momento pero que han tenido ligeros cambios. Mejor mirar el ejemplo en spec/spec_base.rb. Queda algo así:

Dilithium::PersistenceService.configure do |config|
:'Dilithium::BaseEntity' => :leaf # Este será el default
:Person => :class
end

Como pone en el ejemplo, el tipo de herencia por defecto se configura para Dilithium::BaseEntity > (no existe un default, se tiene que configurar explícitamente). Sólo se permite configurar la herencia de subclases inmediatas de BaseEntity (es decir, no podemos, por ejemplo, dejar User con el default y configurar Person como :class, o configurar User como :class y luego Person como :leaf).

  1. Los finders

Los métodos fetch_by_id y fetch_all funcionan correctamente con herencia de tipo :class (de forma polimórfica), de forma que si hago un Person.fetch_by_id(3) y el tipo real del objeto es Admin, me devolverá un objeto de tipo Admin. Lo que hace por debajo es, primero buscar la tabla que corresponde a la raíz del árbol de herencia, obtener de ahí el tipo del objeto (mediante la columna _type) para saber qué tablas lo componen, y luego hace un join entre todas las tablas. En el caso de herencia de tipo :leaf, la herencia no es polimórfica. Para el caso de finders específicos, ya no vale nada más buscar en la tabla correspondiente (ya que se tiene que hacer un merge de todas las tablas de la jerarquía).

Se debe poner el finder en la clase donde se define un atributo. Por ejemplo, si quiero hacer un fetch_by_email, se debe definir en la clase User, no en la clase Person (ya que Person no tiene atributo email). Por ahora, hay que acceder a la tabla, obtener solamente la lista de id's, y luego hacer un fetch_by_id de cada resultado. Esto habrá que cambiarlo luego, hay que rehacer toda la estructura de los Finders.

module Dilithium::Repository
  module Sequel
    module UserCustomFinders
      def fetch_by_email email
        id_list = DB[:user].where(email: email).where(active:true).select_map(:id)
        id_list.map do |id|
          User.fetch_by_id(id)
        end
      end
    end
  end
end