31-Nodejs Course 2023: Database Models: Create Base Model
Hasan Zohdy
Posted on November 3, 2022
So we got to know about database models in the previous article, now let's create a base model that we'll use to create other models.
Model Structure
Before we getting started, let's illustrate how the model structure works, we have 4 main parts:
- Static Variables: These variables will be access directly without creating an instance of the model, for example, the collection name.
- Static Methods: These methods will be access directly without creating an instance of the model, for example, the
find
method. - Instance Variables: These variables will be access after creating an instance of the model, for example, the
name
variable. - Instance Methods: These methods will be access after creating an instance of the model, for example, the
save
method.
Static variables will be mainly for the meta data of the model which will be used to collect information about the model like the collection name, the initial id value and so on.
The static method will be used for quick access and operations that are not related to a specific instance of the model, for example, the find
method.
Base Model
Go to src/core/database
folder and create model
folder inside it then create model.ts
file.
// src/core/database/model/model.ts
export default abstract class Model {
/**
* Collection Name
*/
public static collectionName = "";
}
Nothing fancy here, just a base model class that we'll extend in other models.
We're making it abstract because we don't want to use it directly, we'll use it as a base class for other models to inherit from.
The collection name will be a static property that we'll override in the child classes so we can access the collection name from the model class directly without creating an instance of it.
You might wonder that we did a file called model.ts
inside model
directory not index.ts
that's because we're going to create more files inside the model
directory so we'll make the index.ts
file for the exports only.
Now let's create the index.file and export the base model from it.
// src/core/database/model/index.ts
export { default as Model } from './model';
Then go to database/index.ts
file and export all from the model/index.ts
fike.
// src/core/database/index.ts
export * from './model';
This will allow us to access the Model
class by importing it like this:
import { Model } from 'core/database';
Users Model
Now let's create a new file called user.ts
in src/app/users/models
directory.
// src/app/users/models/user.ts
import { Model } from 'core/database';
export default class User extends Model {
/**
* Collection Name
*/
public static collectionName = "users";
}
Now we have a base model and a user model that extends it then we added the collection name to the user model.
Pretty easy, nothing fancy here but still these are just an empty files without soul (not database yet).
We can now get the collection name directly like this:
import User from 'app/users/models/user';
console.log(User.collectionName); // users
Let's create its soul.
Database Connection
Heading back to our base model, let's define a database connection property.
// src/core/database/model/model.ts
import connection, { Connection } from './../connection';
export default abstract class Model {
/**
* Collection Name
*/
public static collectionName = "";
/**
* Database Connection
*/
public static connection: Connection = connection;
}
We'll use our own connection so we can use it to access the connection (If needed) and we can of course access the database handler directly from it.
Collection Query
Now let's define a collection method to return the collection query handler.
// src/core/database/model/model.ts
import { Collection } from "mongodb";
import connection, { Connection } from './../connection';
export default abstract class Model {
/**
* Collection Name
*/
public static collectionName = "";
/**
* Database Connection
*/
public static connection: Connection = connection;
/**
* Get Collection query
*/
public static query(): Collection {
return this.connection.database.collection(this.collectionName);
}
}
In javascript, accessing static properties can be used with this
keyword.
We need to make sure that the connection's database property is always exists as it may throw an error as it is possibly undefined.
// src/core/database/connection.ts
/**
* Database instance
*/
// ππ»Replace the ?: with !:
public database!: Database;
So far so good, now we're don with static methods, let's implement the non static ones that we need.
At this point, we can now access the users collection like this:
import User from 'app/users/models/user';
// get the collection query
const usersCollection = User.query();
await usersCollection.find().toArray();
Accessing Child Static Property From Parent Class
In the model it self when we create a new instance (object) of it, we need to access the collection of the model to make internal operations, like saving the user's data to the database, so we need to access the query
method from the child class.
We can't access the static property of a child class from the parent class
if we created an instance of that child, so we need to do a trick to access it.
To access the child class static members, we'll use this.constructor
feature.
// src/core/database/model/model.ts
import connection, { Connection } from './../connection';
export default abstract class Model {
/**
* Collection Name
*/
public static collectionName = "";
/**
* Database Connection
*/
public static connection: Connection = connection;
/**
* Get Collection Name
*/
public getCollectionName(): string {
return this.constructor.collectionName;
}
}
But Typescript will start complaining, because it doesn't know that the constructor
property is a class.
To fix this, we'll use the typeof
keyword to tell Typescript that the constructor
property is a class.
// src/core/database/model/model.ts
export default abstract class Model {
// ...
/**
* Get Collection Name
*/
public getCollectionName(): string {
return (this.constructor as typeof Model).collectionName;
}
}
We can wrap it in a private method so we can use it in other methods by passing only the static property that we need to get.
// src/core/database/model/model.ts
export default abstract class Model {
// ...
/**
* Get Static Property
*/
protected getStaticProperty<T>(property: keyof typeof Model): T {
return (this.constructor as any)[property];
}
/**
* Get Collection Name
*/
public getCollectionName(): string {
return this.getStaticProperty('collectionName');
}
}
The keyof typeof Model
will tell Typescript that the property
parameter is a key of the Model
class so we can use the auto complete feature to get the static property that we need to get.
Getting Connection, Database and Collection Query
Now we can use the getStaticProperty
method to get the connection, database and collection query.
// src/core/database/model/model.ts
export default abstract class Model {
// ...
/**
* Get Collection Name
*/
public getCollectionName(): string {
return this.getStaticProperty("collectionName");
}
/**
* Get database connection
*/
public getConnection(): Connection {
return this.getStaticProperty("connection");
}
/**
* Get database instance
*/
public getDatabase(): Database {
return this.getConnection().database;
}
/**
* Get Collection Query
*/
public getQuery(): Collection {
return this.getStaticProperty("query");
}
}
So our final model class will look like this:
import { Collection } from "mongodb";
import { Database } from "../database";
import connection, { Connection } from "./../connection";
export default abstract class Model {
/**
* Collection Name
*/
public static collectionName = "";
/**
* Database Connection
*/
public static connection: Connection = connection;
/**
* Get Collection query
*/
public static query(): Collection {
return this.connection.database.collection(this.collectionName);
}
/**
* Get Collection Name
*/
public getCollectionName(): string {
return this.getStaticProperty("collectionName");
}
/**
* Get database connection
*/
public getConnection(): Connection {
return this.getStaticProperty("connection");
}
/**
* Get database instance
*/
public getDatabase(): Database {
return this.getConnection().database;
}
/**
* Get Collection Query
*/
public getQuery(): Collection {
return this.getStaticProperty("query");
}
/**
* Get Static Property
*/
protected getStaticProperty<T>(property: keyof typeof Model): T {
return (this.constructor as any)[property];
}
}
Let's give it a try
import User from 'app/users/models/user';
// get the collection query
const user = new User;
// get the collection name
user.getCollectionName();
// get the database connection
user.getConnection();
// get the database instance
user.getDatabase();
// get the collection query
user.getQuery();
We can split it into 4 parts:
- Public Static Variables/Properties: to define the collection name and the connection.
- Public Static Methods: to define the collection query.
- Public Methods: to get the collection name, connection, database and collection query.
- Protected Methods: to get the static property of the child class.
The protected method is intended to not be used outside the model or one of its children as its an internal operation.
π¨ Conclusion
We've learned couple of good things here today, how and why to use static and non static methods, how to access static properties from a child class in the parent class, and how to use the this.constructor
feature.
Then we created base model that allows us to get the collection query of the current model's collection name.
Then we added another feature to access the collection query or any other static methods when we create a new instance of the model.
π¨ Project Repository
You can find the latest updates of this project on Github
π Join our community
Join our community on Discord to get help and support (Node Js 2023 Channel).
ποΈ Video Course (Arabic Voice)
If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.
π° Bonus Content π°
You may have a look at these articles, it will definitely boost your knowledge and productivity.
General Topics
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Posted on November 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.