Skip to content
This repository was archived by the owner on Dec 5, 2024. It is now read-only.

Suggestion for Inheritance API

ludydoo edited this page Jan 16, 2014 · 35 revisions

So here's the deal. I'd like to implement the inheritance functionality to sequelize models.

Those would be the main API add ons :

  • AModel.extends(AnotherModel) would serve as indicating which type is the model's "supermodel"
  • AModel.extends(AnotherModel) also to test if AModel is child of AnotherModel
  • ADao.extends(AnotherDao) to test if ADao is child of AnotherDao
  • AModel.superclasses(AnotherModel) to test if AModel superclasses AnotherModel
  • ADao.superClasses(AnotherDao) to test if ADao is parent of AnotherDao
  • virtual would just be a parameter indicating that this model cannot be instantiated
  • AModel.super() and ADao.super() would be called from a class method, instance method and validator methods. It would access the superclass overridden method(?)
  • AModel.super(AnotherModel) and ADao.super(AnotherDao) would call the overridden method in the parent Model/Dao
  • model and includeDescendants would be options for find and findAll where clause. Model would be the sought Model, and includeDescendants would allow all Model descendent Models. e.g. AnotherModel.findAll(where : {model : Model, includeDescendants : true})
  • serial || hierarchical || hash would be options in findAll. Would represent output type. Like : AnotherModel.findAll({model : Model}, 'serial || hierarchical || hash')
var Feline = sequelize.define('Feline', {}, {virtual : true})
var Cat= sequelize.define('Cat', {})
var Lion = sequelize.define('Lion', {})

Cat.extends(Feline)
Lion.extends(Cat)

var FelineFood = sequelize.define('AnimalFood', {for : Feline},{virtual : true})
var CatFood = sequelize.define('CatFood', {name : sequelize.STRING, for : Cat})
var LionFood = sequelize.define('LionFood')

var GarfieldFood = sequelize.define('CatFood', {
  for : { //Possible to redefine a field's validation properties without creating a new column
    validate : function(instance){
      super()
      if (instance.name !== 'Garfield') { //Can access instance attributes
        raise ("This ain't for Garfield") 

} } } }

Let's take a use case :

var Animal = sequelize.define(Animal, { paws : sequelize.INTEGER, name : sequelize.STRING })
var Reptile = sequelize.define(Reptile, { eggs : sequelize.INTEGER })
var Mammal = sequelize.define(Mammal, { mammae : sequelize.INTEGER }, {virtual : true})

Reptile.extends(Animal)
Mammal.extends(Animal)

var gekko = Reptile.create({ name : 'gekko', paws : 4, eggs : 2}).complete(function(err, reptile){ }) 
// would create an animal and a reptile entry in the database

Reptile.findAll({where : {paws : 4}})
// would return all reptiles which are animals with 4 paws

Reptile.extends(Mammal) // would return false

var animal = Animal.create({ paws : 3 })
// would return an error because it is not allowed to instantiate a virtual class

Basically, this feature would mimic the class-based inheritance hierarchy. I think it would be useful to create complex data structures.

Bigger Example

Let's take another more complex example

// Parties models
// Parties
var Party = sequelize.define('Party', {}, {virtual : true})

// A person..
var Person = sequelize.define('Person', {firstname : sequelize.STRING, lastname : sequelize.STRING})

// An organization..
var Organization = sequelize.define('Organization, {name : sequelize.STRING})

// A position (job) available or not
var Position = sequelize.define('Position',{description : sequelize.STRING})


// Person, Position and Organization are parties
Person.extends(Party)
Organization.extends(Party)
Position.extends(Party)

// Organizations can be hierarchical
Party.hasOne(Party, {as : 'Parent'})

In this example, the Relation model is used to link two Parties, regardless of their type. A Responsibility relation is a subtype of Relation. Represents a Responsibility towards another Party type An Authorization relation is a subtype of Relation. Represents an Authorization towards another Party type A Family relation is a subtype of Relation. Represents a familial link between two parties. It is restricted between two instances of Party of type Person.

// Relationship Models
// Relationships
var Relation = sequelize.define('Relation' { begin : sequelize.DATE, end : sequelize.DATE }, {virtual : true})

// Responsibility relationship
var Responsibility = sequelize.define('Responsibility', {})

// Authorization relationship
var Authorization = sequelize.define('Authorization', {accessLevel : sequelize.INTEGER})

// Family relationship
var Family = sequelize.define('Family', {accessLevel : sequelize.INTEGER})

// Responsibility and Authorization are relationships
Responsibility.extends(Relation)
Authorization.extends(Relation)
Family.extends(Relation)

// A relation identifies a source party with a target party
Relation.hasOne(Party, {as : 'Source'})
Relation.hasOne(Party, {as : 'Target})

The Locator type is used to represent an address, a telephone number or something used to communicate with a specified party, be it an Organization, a Person or a Position. It has a property Located which represents the Party being located by this Locator The Locator model is the base type for locators The TelephoneLocator model extends Locator. Used to represent a phone number The ExtensionLocator model extends Locator. Used to represent an extension number. Is linked to another TelephoneLocator. If the linked TelephoneLocator number changes, the ExtensionLocator still has the reference to that number.

//Locator Models
//Locators is used to represent an address, phone number, etc. identifying a person's mean of communication
var Locator = sequelize.define('Locator', { })

//The party being identifier (organization, person, position)
Locator.hasOne(Party, { as : 'Located'}) 

//Plain telephone number
var TelephoneLocator = sequelize.define('TelephoneLocator', { number : sequelize.STRING }) 
TelephoneLocator.extends(Locator)

//The extension of a telephone number. E.g. office / post number
var ExtensionLocator = sequelize.define('ExtensionLocator', { extension : sequelize.INTEGER })
ExtensionLocator.extends(Locator)

//An ExtensionLocator identifies a Party, but is dependent on a base telephone number. 
// Does not subclass it but instead is linked to it
ExtensionLocator.hasOne(TelephoneLocator, {as : 'LocatedPhone'}) 

Practical Example

Let's dive into a practical example.

John is a man.

var john;
// John is a nice man
Person.create({firstName : 'John', lastName : 'Doe'}).complete(function(err, person){
   john = person
})

Acme is a company in his town

var acme;
Organization.create({name : 'Acme'}).complete(function(err, organization){
   acme = organization
})

This is their phone number

var phone
TelephoneLocator.create({located : acme, number : '872-394-1834'}).complete(function(err, telLocator)){
  phone = telLocator;
}

They hire a security guard

var securityGuard
Position.create({parent : acme, description : 'Security Guard'}).complete(function(err, position){
  securityGuard = position
})

He has special authorizations

var auth
Authorization.create({source : acme, target : securityGuard, accessLevel : 101}).complete(function(err, anAuth){
  auth = anAuth;
})

This is his extension number

var extension
ExtensionLocator.create({located : securityGuard, locatedPhone : phone, extension : '911'}).complete(function(err, anExtension){
  extension = anExtension 
})

John works as a security guard

var job
Responsibility.create({source : john, target : securityGuard, begin : now(), end : null})
.complete(function(err, responsibility){
  job = responsibility
})

Also has a cellphone for lonely nights

var cellNumber
TelephoneLocator.create({located : john, number : '222-143-1234'}).complete(function(err, cellNumber)){
  cellNumber = telLocator;
}

(Let's say this method is in the instance methods)

Person.quits = function(instance){
   Responsibility.find(where : {source : instance}).complete(err, responsibility){
     responsibility.setEndDate(now()).complete(function(err){
       return 'Free time!';
     })
   })
}

John doesn't like his job

john.quits();

Finds another job

var anotherJob
Responsibility.create({source: john, target : anotherJob}).complete(function(err, resp){
  anotherJob = resp
})

This is helen

var helen
Person.create({firstName : 'Hellen', lastName: 'Altfest'}).complete(function(err, person){
  helen = person;
})

His sister

var family // (family relationship)
Family.create({source : john, target: helen}).complete(function(err, sister){
 sister = relation 
})

FindAll return types

Let's see john's relations

Relation.findAll(where : {source : john}).complete(function(err, relations){
   console.log(relations)
   // Outputs a hash with type as key
   // Responsibilities : 
   //     [0] job
   //     [1] anotherJob
   // Family
   //     [0] helen
})

// And Helen's relations
   // Outputs a hash with type as key
   // Family
   //     [0] john

Let's define a BigResponsibility

BigResponsibility = sequelize.define('BigResponsibility', {})
BigResponsibility.extends(Responsibility)

var bigResponsibility
BigResponsibility.create({source : john, target : acme}).complete(function(err, bigResp){
  bigResponsibility = bigResp;
})

// Johns relations are now : 
   // Outputs a hash with type as key
   // Responsibilities : 
   //     [0] job
   //     [1] anotherJob
   // BigResponsibilities
   //     [0] bigResponsibility
   // Family
   //     [0] helen

Hierarchical Form

Let's get this data in hierarchical form

Person.findAll({where : {name : 'john'}, {hierarchical : true}})
// Option to have hierarchical structure
// Johns relations
   // Outputs a hash with type as key
   // Relations : 
   //   Children : 
   //     Responsibilities :
   //       Children : 
   //         BigResponsibilities : 
   //           [0] : bigResponsibility
   //       Data :  
   //         [0] job
   //         [1] anotherJob
   //     Family
   //       Data : 
   //         [0] helen

Serial Form

And in serial form

Person.findAll({where : {name : 'john'}, {serial : true}})
// Option to have hierarchical structure
// Johns relations
   // [0] job
   // [1] helen
   // [2] anotherJob
   // [3] bigResponsibility

model and includeDescendants

The model and includeDescendants option

Person.find({where : {name : 'john', type : {model : responsibility, includeDescendants : true }}} //Default
// Johns relations
   // Outputs a hash with type as key
   // Responsibilities : 
   //     [0] job
   //     [1] anotherJob
   // BigResponsibilities
   //     [0] bigResponsibility

If it is set to false

Person.find({where : {name : 'john', type : {model : Responsibility, includeDescendants : false }}}
// Johns relations
   // Outputs a hash with type as key
   // Responsibilities : 
   //     [0] job
   //     [1] anotherJob

Validation

var Responsibility = sequelize.define('Responsibility', { 

   // Sequelize does not add another 'source' field as 'source' exists already a parent class
   source : {
      validate : {
         type : function(instance){
           super() //Calls the parent type validator method
           if (instance.Model !== 'Person') {raise...}} // It just validates the type
      }
   }
   // same here, do not add another 'target' field as it already exists.
   target : {
      type : sequelize.INTEGER // error when trying to redefine overridden field type
      validate : {
         type : function(instance){
           super()
           if ((instance.Model !== Organization) || (instance.Model !== Position))
             raise...
           if (!instance.Model.inheritsFrom(Party))
             raise
         }
      }
   } 
})

Redefinition of Association Rules

Would also be nice to be able to redefine association rules, like : (But could also be done with validation methods)

Animal = sequelize.define('Animal', {})
Cat = sequelize.define('Cat', {})
Dog = sequelize.define('Dog', {})

Cat.extends(Animal)
Dog.extends(Animal)

Person = sequelize.define('Person',{})
GrandMa = sequelize.define('GrandMa', {})
GrandMa.extends(Person)

Person.hasMany(Animals, {as : 'pets'}) //Person type can have any type of animal as pets
Grandma.hasMany(Cats, {as : 'pets'}) //GrandMa type can only haz cats as pets

GrandMa.getPets() //Would return all GrandMa's pets
Dog.create().complete(function(err, dog){
  GrandMa.addPet(dog).complete(function(err){ //Throws an error. Grandma can only have cats!
  })
})

And more to come

Home

Getting Started

  • Installation
  • Basic Setup
  • With Express.JS
  • Sequelize CLI
  • Relationships
    • 1 : 1
    • 1 : n
    • n : m
    • Polymorph
  • Hooks
  • Pooling
  • Debugging
  • Migrations

How To

Resources

Upgrade Guides

Help

Clone this wiki locally