Reference Source

Polymorphism using Views

One of pilars of the object oriented programming is the polymorphism, but this paradigm is very hard to apply in relational databases, and this complexity is increased in the ORM layer.

Sequelie have an example using the beforeFind hook and scopes.

There we explore an alternative using Views, with the adventage of the transparent usage in queries.

Stage

In this stage with have and entity called Vehicle, with a serie of common vehicles attributes, as the price or the name, and a attribute called type that define who type of vehicle is, the rest of properties are in two separate models: Car and Bike, this referers to the Vehicle model with an one to one relation.

Propouse

We propose to create two abstract class, VehicleChild extending of Model, and VehicleChildView extending of View.

Implement

Create parent model

First, we are going the Parent entity model:

// Create Vehicle Model
class Vehicle extends Model {};

Vehicle.init({
  type: DataTypes.ENUM('car', 'bike'),
  name: DataTypes.STRING,
  price: DataTypes.DECIMAL,
}, { sequelize, modelName: 'vehicle' });

This has the type of vehicle and the common information.

Create child abstract

Next, we are going to code a abstract class for use by the childs of Vehicle:

// Create Child Model Abstract
class VehicleChildren extends Model {
  // Override init for add id field
  static init(fields, options = {}) {
    super.init({
      id: {
        type: DataTypes.INTEGER,
        allowNull: false,
        primaryKey: true,
        autoIncrement: false,
      },
      ...fields,
    }, {
      timestamps: false,
      ...options,
    });
  }

  static associate(models) {
    Vehicle.hasOne(this, {
        foreignKey: 'id',
        sourceKey: 'id',
        constraints: true,
    });
  }
};

In this case, we are overriding the method init, to add an id field, in this case, it's neccesary the child id was the same of the parent model, for this reason set autoIncrement in false.

By other hand, set timestamps in false becouse the creation time is in the parent model. By practical propouses we will not to worry for the updatedAt field.

In the associate method, we are associating own child model with the parent.

Create child models

// Create Car Model
class Car extends VehicleChildren {};

Car.init({
    transmission: DataTypes.ENUM('automatic', 'manual'),
    doors: DataTypes.INTEGER,
    style: DataTypes.ENUM('sedan', 'coupe', 'convertible'),
    hp: DataTypes.INTEGER,
}, { sequelize, modelName: 'car'});

// Create Bike Model
class Bike extends VehicleChildren {};

Bike.init({
    style: DataTypes.ENUM('road', 'mountain', 'hybrid'),
    cicles: DataTypes.INTEGER,
    cc: DataTypes.INTEGER,
}, { sequelize, modelName: 'bike'});

// Create associations 
Car.associate();
Bike.associate();

We extends the VehicleChildren and define a serie of own properties for each vehicle type.

Create the abstract View

For DRY, we have to create an abstract View overriding some methods:

// Create View Base
class VehicleChildView extends View {

    // Override getQueryOptions using props
    static getQueryOptions(options) {
        return {
            model: Vehicle, 
            include: [{
                model: this.model,
                required: true,
            }],
            where: {
                type: this.type,
            },
            ...(options.viewQueryOptions || {}),
        };
    }

    static init(fields, options = {}) {

        const extraFields = Object.keys(fields).reduce((acc, key) => {
            acc[key] = `${this.type}.${key}`;
            return acc;
        }, {});

        const {viewQueryOptions}  = options;

        super.init({
            name: DataTypes.STRING,
            price: DataTypes.DECIMAL,
            ...fields,
        }, {
            ...options,
            viewQueryOptions: {
                fieldsMap: {
                    ...extraFields,
                    ...(viewQueryOptions || {}),
                },
                ...(viewQueryOptions && options.viewQueryOptions || {}),
            }, }); }

    // Override create 
    static async create(data, options = {}) {
        return sequelize.transaction(async (transaction) => {
            const {
                name,
                price,
                ...extra
            } = data;

            const vehicle = await Vehicle.create({
                type: this.type,
                name,
                price,
            }, { transaction });

            const child = await this.model.create({
                id: vehicle.id,
                ...extra,
            }, { transaction });

            return this.build({
                ...vehicle.get({plane: true}),
                ...child.get({plane: true}),
            });
        });
    }
};

In this case we are using the method getQueryOptions to define the View query, equal for all childrens. We expect two static properties in the definition of a VehicleChildView; type and model, this properties are used for abstract View to create the query options, the relation and to be used in the overrided logic of create method, in this case we are overriting create to desmotrate a use of a View like some kind of serializer.

We are auto-mapping the own-properties too.

Create the child views

With the code abstracted bellow, the definition of new Views that represent a child of Vehicle, must to be simple:

// Create Car View
class VehicleCar extends VehicleChildView {
    static type = 'car';
    static model = Car;
};

VehicleCar.init({
    transmission: DataTypes.STRING,
    doors: DataTypes.INTEGER,
    style: DataTypes.STRING,
    hp: DataTypes.INTEGER,
}, {
    sequelize,
});

// Create Bike View
class VehicleBike extends VehicleChildView {
    static type = 'bike';
    static model = Bike;
};

VehicleBike.init({
    style: DataTypes.STRING,
    cicles: DataTypes.INTEGER,
    cc: DataTypes.INTEGER,
}, {
    sequelize,
});

Only extending the class VehicleChildView and creating the properties typeand model, the rest of the logic runs over the abstract View.

Usage

The usage is very simple, we treat the final views like models with the full information of entitiy (parent and child attributes):

(async () => {
  await sequelize.sync();

  const car = await VehicleCar.create({
    name: 'Audi',
    price: 100000,
    transmission: 'automatic',
    doors: 4,
    style: 'sedan',
    hp: 200,
  });

  const bike = await VehicleBike.create({
    name: 'BMX',
    price: 10000,
    style: 'road',
    cicles: 100,
    cc: 100,
  });

  const cars = await VehicleCar.findAll();
  console.log(cars.map((car) => car.toJSON()));

  const bikes = await VehicleBike.findAll();
  console.log(bikes.map((bike) => bike.toJSON()));
})();

The result should to output:

[
  {
    id: 1,
    name: 'Audi',
    price: 100000,
    transmission: 'automatic',
    doors: 4,
    style: 'sedan',
    hp: 200,
    createdAt: 2022-03-01T03:58:18.941Z,
    updatedAt: 2022-03-01T03:58:18.941Z
  }
]
[
  {
    id: 2,
    name: 'BMX',
    price: 10000,
    style: 'road',
    cicles: 100,
    cc: 100,
    createdAt: 2022-03-01T03:58:18.962Z,
    updatedAt: 2022-03-01T03:58:18.962Z
  }
]