Active Record vs Data Mapper

What is the Active Record pattern?

In TypeORM you can use both the Active Record and the Data Mapper patterns.
Using the Active Record approach, you define all your query methods inside the model itself, and you save, remove, and load objects using model methods.
Simply said, the Active Record pattern is an approach to access your database within your models. You can read more about the Active Record pattern on Wikipedia.
Example:
1
import {BaseEntity, Entity, PrimaryGeneratedColumn, Column} from "typeorm";
2
​
3
@Entity()
4
export class User extends BaseEntity {
5
​
6
@PrimaryGeneratedColumn()
7
id: number;
8
​
9
@Column()
10
firstName: string;
11
​
12
@Column()
13
lastName: string;
14
​
15
@Column()
16
isActive: boolean;
17
​
18
}
Copied!
All active-record entities must extend the BaseEntity class, which provides methods to work with the entity. Example of how to work with such entity:
1
// example how to save AR entity
2
const user = new User();
3
user.firstName = "Timber";
4
user.lastName = "Saw";
5
user.isActive = true;
6
await user.save();
7
​
8
// example how to remove AR entity
9
await user.remove();
10
​
11
// example how to load AR entities
12
const users = await User.find({ skip: 2, take: 5 });
13
const newUsers = await User.find({ isActive: true });
14
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" });
Copied!
BaseEntity has most of the methods of the standard Repository. Most of the time you don't need to use Repository or EntityManager with active record entities.
Now let's say we want to create a function that returns users by first and last name. We can create such functions as a static method in a User class:
1
import {BaseEntity, Entity, PrimaryGeneratedColumn, Column} from "typeorm";
2
​
3
@Entity()
4
export class User extends BaseEntity {
5
​
6
@PrimaryGeneratedColumn()
7
id: number;
8
​
9
@Column()
10
firstName: string;
11
​
12
@Column()
13
lastName: string;
14
​
15
@Column()
16
isActive: boolean;
17
​
18
static findByName(firstName: string, lastName: string) {
19
return this.createQueryBuilder("user")
20
.where("user.firstName = :firstName", { firstName })
21
.andWhere("user.lastName = :lastName", { lastName })
22
.getMany();
23
}
24
​
25
}
Copied!
And use it just like other methods:
1
const timber = await User.findByName("Timber", "Saw");
Copied!

What is the Data Mapper pattern?

In TypeORM you can use both the Active Record and Data Mapper patterns.
Using the Data Mapper approach, you define all your query methods in separate classes called "repositories", and you save, remove, and load objects using repositories. In data mapper your entities are very dumb - they just define their properties and may have some "dummy" methods.
Simply said, data mapper is an approach to access your database within repositories instead of models. You can read more about data mapper on Wikipedia.
Example:
1
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
2
​
3
@Entity()
4
export class User {
5
​
6
@PrimaryGeneratedColumn()
7
id: number;
8
​
9
@Column()
10
firstName: string;
11
​
12
@Column()
13
lastName: string;
14
​
15
@Column()
16
isActive: boolean;
17
​
18
}
Copied!
Example of how to work with such entity:
1
const userRepository = connection.getRepository(User);
2
​
3
// example how to save DM entity
4
const user = new User();
5
user.firstName = "Timber";
6
user.lastName = "Saw";
7
user.isActive = true;
8
await userRepository.save(user);
9
​
10
// example how to remove DM entity
11
await userRepository.remove(user);
12
​
13
// example how to load DM entities
14
const users = await userRepository.find({ skip: 2, take: 5 });
15
const newUsers = await userRepository.find({ isActive: true });
16
const timber = await userRepository.findOne({ firstName: "Timber", lastName: "Saw" });
Copied!
Now let's say we want to create a function that returns users by first and last name. We can create such a function in a "custom repository".
1
import {EntityRepository, Repository} from "typeorm";
2
import {User} from "../entity/User";
3
​
4
@EntityRepository()
5
export class UserRepository extends Repository<User> {
6
​
7
findByName(firstName: string, lastName: string) {
8
return this.createQueryBuilder("user")
9
.where("user.firstName = :firstName", { firstName })
10
.andWhere("user.lastName = :lastName", { lastName })
11
.getMany();
12
}
13
​
14
}
Copied!
And use it this way:
1
const userRepository = connection.getCustomRepository(UserRepository);
2
const timber = await userRepository.findByName("Timber", "Saw");
Copied!
Learn more about custom repositories.

Which one should I choose?

The decision is up to you. Both strategies have their own cons and pros.
One thing we should always keep in mind with software development is how we are going to maintain our applications. The Data Mapper approach helps with maintainability, which is more effective in bigger apps. The Active record approach helps keep things simple which works well in smaller apps. And simplicity is always a key to better maintainability.
Last modified 1mo ago