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
}
]
Reference
Source
