ORM (Object-Relational Mapping) đã trở thành công cụ quan trọng giúp developer dễ dàng làm việc với cơ sở dữ liệu hơn. Trong bài viết này, cùng mình đi tìm hiểu chi tiết về MikroORM - một ORM mạnh mẽ dành cho Nodejs, dựa trên Data Mapper, Unit of Work và Identity Map.
1. Mikro là gì?
MikroORM là một ORM cho Nodejs được viết bằng TypeScript, dựa trên các design patterns như: Data Mapper, Unit of Work và Identity Map.
MikroORM hỗ trợ nhiều cơ sở dữ liệu: PostgreSQL, MySQL, MariaDB, SQLite, MongoDB và SQL Server. Với MikroORM, bạn có thể định nghĩa các model bằng TypeScript, giúp tương tác với cơ sở dữ liệu một cách linh hoạt và an toàn.
Các bạn đang thắc mắc dựa trên Data Mapper, Unit of Work và Identity Map là như thế nào phải không? Mình sẽ giải thích tổng quan qua các design patterns - được sử dụng để giảm thiểu sự phức tạp trong việc truy cập, quản lý, và xử lý dữ liệu giữa ứng dụng database.
- Data Mapper: là design patterns tách logic truy cập cơ sở dữ liệu khỏi logic nghiệp vụ bằng cách sử dụng một lớp trung gian (Mapper), giúp model không phụ thuộc vào cơ sở dữ liệu, dễ dàng kiểm thử và bảo trì.
- Unit of Work: là design patterns quản lý các thay đổi đối với các thực thể (entity) trong transaction. Nó theo dõi các đối tượng đã được truy xuất từ cơ sở dữ liệu và biết đối tượng nào đã thay đổi, cho phép tối ưu hóa số lượng truy vấn.
- Identity Map: là design patterns đảm bảo rằng trong một phiên làm việc, mỗi thực thể chỉ tồn tại một đối tượng duy nhất trong bộ nhớ. Điều này ngăn chặn việc tải lại nhiều bản sao của cùng một thực thể (entity), đảm bảo tính nhất quán dữ liệu.
2. Các tính năng nổi bật của MikroORM
- MikroORM hỗ trợ cả cơ sở dữ liệu quan hệ và NoSQL, cung cấp sự linh hoạt trong việc lựa chọn cơ sở dữ liệu phù hợp.
Ví dụ cấu hình kết nối với MySQL:
import { Options } from '@mikro-orm/core';
import { User } from './entities/User';
const config: Options = {
entities: [User],
dbName: 'my_database',
type: 'mysql',
user: 'root',
password: 'password',
};
export default config;
Cấu hình sử dụng MongoDB:
import { Options } from '@mikro-orm/core';
import { User } from './entities/User';
const config: Options = {
entities: [User],
clientUrl: 'mongodb://localhost:27017/my_database',
type: 'mongo',
};
export default config;
- Sử dụng TypeScript giúp phát hiện lỗi ngay trong quá trình biên dịch.
Ví dụ định nghĩa entity:
import { MikroORM } from '@mikro-orm/core';
import { User } from './entities/User';
import config from './mikro-orm.config';
(async () => {
const orm = await MikroORM.init(config);
const em = orm.em.fork();
const user = new User();
user.name = 'John Doe';
user.email = 'john@example.com';
// TypeScript sẽ cảnh báo nếu bạn gán sai kiểu dữ liệu
// user.name = 123; // Error: Type 'number' is not assignable to type 'string'
await em.persistAndFlush(user);
await orm.close(true);
})();
- MikroORM cung cấp một Query Builder mạnh mẽ, cho phép bạn xây dựng các truy vấn phức tạp một cách dễ dàng.
import { MikroORM } from '@mikro-orm/core';
import { User } from './entities/User';
import config from './mikro-orm.config';
(async () => {
const orm = await MikroORM.init(config);
const em = orm.em.fork();
// Truy vấn người dùng với tên '200Lab', sắp xếp theo 'createdAt' giảm dần, và phân trang
const users = await em.find(User, {
name: { $like: '%200Lab%' },
}, {
orderBy: { createdAt: 'DESC' },
limit: 10,
offset: 0,
});
console.log(users);
await orm.close(true);
})();
Điều kiện: { name: { $like: '%200Lab%' } } tìm user có tên chứa "200Lab"
Sắp xếp: orderBy: { createdAt: 'DESC' } sắp xếp kết quả theo ngày tạo mới nhất.
Phân trang: limit và offset dùng để phân trang.
- Sử dụng các decorator của TypeScript để định nghĩa schema và quan hệ giữa các thực thể (entity).
Ví dụ về định nghĩa quan hệ One-to-Many giữa User và Post
import { Entity, PrimaryKey, Property, OneToMany, Collection } from '@mikro-orm/core';
import { Post } from './Post';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@OneToMany(() => Post, post => post.author)
posts = new Collection<Post>(this);
}
import { Entity, PrimaryKey, Property, ManyToOne } from '@mikro-orm/core';
import { User } from './User';
@Entity()
export class Post {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@ManyToOne(() => User)
author!: User;
}
- Có thể dễ dàng tích hợp với các framework phổ biến như ExpressJS, Koa, NestJS.
Mình sẽ ví dụ tích hợp với framework ExpressJS
import express from 'express';
import { MikroORM, RequestContext } from '@mikro-orm/core';
import { User } from './entities/User';
import config from './mikro-orm.config';
(async () => {
const orm = await MikroORM.init(config);
const app = express();
app.use((req, res, next) => {
RequestContext.create(orm.em, next);
});
app.get('/users', async (req, res) => {
const em = orm.em.fork();
const users = await em.find(User, {});
res.json(users);
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
})();
- RequestContext: đảm bảo mỗi request sử dụng một EntityManager riêng biệt.
- Integration: dễ dàng sử dụng các method của MikroORM trong route handler.
3. Nhược điểm của Mikro
3.1 Độ phức tạp
- Bạn cần dành thời gian để làm quen với các design pattern.
- Bạn cần phải chú ý thiết lập chính xác một số tuỳ chọn.
- Đối với người mới thì sẽ cảm thấy khá phức tạp do sự trừu tượng cao.
3.2 Tài liệu ít, cộng đồng nhỏ
- Đối với các tính năng nâng cao thiếu ví dụ cụ thể khó hình dung đối với người mới tiếp cận với Mikro.
- Tài liệu, và cộng đồng khá nhỏ khá khó khăn trong việc tiếp cận vấn đề khi gặp lỗi.
4. So sánh TypeORM, Sequelize, MikroORM và Prisma
TypeORM | Sequelize | MikroORM | Prisma | |
---|---|---|---|---|
Ngôn ngữ | TypeScript, JavaScript | JavaScript | TypeScript | TypeScript |
Type-safe | Một phần | Không | Có | Có |
Hỗ trợ CSDL | MySQL, PostgreSQL, MariaDB,... | MySQL, PostgreSQL, SQLite,... | MySQL, PostgreSQL, MongoDB,... | MySQL, PostgreSQL, SQLite, SQL Server |
Query Builder | Có | Có | Có | Sử dụng Prisma Client |
Design Pattern | Active Record & Data Mapper | Active Record | Data Mapper, Unit of Work, Identity Map | Không rõ ràng |
Cộng đồng | Lớn | Rất lớn | Đang phát triển | Đang phát triển |
Dễ học | Trung bình | Dễ | Khó | Dễ |
Hiệu suất | Tốt | Tốt | Cao | Rất cao |
Hỗ trợ TypeScript | Tốt | Hạn chế | Tuyệt vời | Tuyệt vời |
Migration | Có | Có | Có | Có |
5. Hướng dẫn
Trước khi đi vào cài đặt, bạn cần có một dự án TypeScript cơ bản. Nếu bạn chưa biết cách cũng đừng quá lo lắng bạn có thể tham khảo tại đây.
B1: Cài đặt packages
Sau khi đã sẵn sàng, cùng mình chạy lệnh sau để cài đặt MikroORM:
npm install @mikro-orm/core @mikro-orm/mysql reflect-metadata
Lưu ý: nếu bạn sử dụng cơ sở dữ liệu khác như: PostgreSQL, SQLite, MongoDB, hãy cài đặt tương ứng, ví dụ: @mikro-orm/postgresql.
B2: Cấu hình TypeScript
{
"compilerOptions": {
"target": "ES2017",
"module": "CommonJS",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strict": true,
"outDir": "dist",
"esModuleInterop": true,
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
emitDecoratorMetadata
vàexperimentalDecorators
: để sử dụng decorator trong TypeScript.
• strict
: bật chế độ kiểm tra chặt chẽ, giúp phát hiện lỗi sớm.
• rootDir
: đặt folder gốc cho source code là src (nhớ tạo folder src nha)
B3: Định nghĩa thực thế (entity)
Mình sẽ tiến hành định nghĩa user entity. Bạn tạo file src/entities/User.ts
:
import { Entity, PrimaryKey, Property, OneToMany, Collection } from '@mikro-orm/core';
import { Post } from './Post';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Property({ unique: true })
email!: string;
@OneToMany(() => Post, post => post.author)
posts = new Collection<Post>(this);
@Property()
createdAt = new Date();
@Property({ onUpdate: () => new Date() })
updatedAt = new Date();
}
- @Entity(): đánh dấu lớp là một thực thể.
- @PrimaryKey(): định nghĩa khóa chính.
- @Property(): định nghĩa một thuộc tính.
- @OneToMany(): quan hệ một-nhiều với thực thể Post.
- Collection: quản lý tập hợp các Post liên quan.
Tiếp theo, bạn tạo file src/entities/Post.ts
:
import { Entity, PrimaryKey, Property, ManyToOne } from '@mikro-orm/core';
import { User } from './User';
@Entity()
export class Post {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property({ type: 'text' })
content!: string;
@ManyToOne(() => User)
author!: User;
@Property()
createdAt = new Date();
@Property({ onUpdate: () => new Date() })
updatedAt = new Date();
}
- @ManyToOne(): quan hệ nhiều-một với thực thể User.
- type: 'text': định nghĩa kiểu dữ liệu cho trường content.
B4: Cấu hình MikroORM
Tạo file mikro-orm.config.ts
ở folder gốc của dự án:
import { Options } from '@mikro-orm/core';
import { User } from './src/entities/User';
import { Post } from './src/entities/Post';
import path from 'path';
const config: Options = {
entities: [User, Post],
dbName: 'mikroorm_demo',
type: 'mysql',
user: 'root',
password: 'password',
debug: true,
migrations: {
path: path.join(__dirname, './migrations'),
pattern: /^[\w-]+\d+\.[tj]s$/,
transactional: true,
disableForeignKeys: false,
allOrNothing: true,
emit: 'ts',
},
};
export default config;
- entities: danh sách các entity sử dụng.
- dbName, type, user, password: thông tin kết nối đến cơ sở dữ liệu.
- debug: hiển thị truy vấn SQL trong quá trình chạy.
- migrations: cấu hình cho tính năng Migration.
Lưu ý: bạn cần phải đảm bảo rằng cơ sở dữ liệu mikroorm_demo
đã được tạo và thông tin đăng nhập là chính xác.
B7: Thiết lập Migration
npm install @mikro-orm/migrations
Tạo folder migrations
mkdir migrations
Tạo migrations để tạo bảng cho các thực thể
npx mikro-orm migration:create --initial
Chạy migration để update vào cơ sở dữ liệu
npx mikro-orm migration:up
Tạo file src/seed.ts
để thêm dữ liệu mẫu vào cơ sở dữ liệu
import { MikroORM } from '@mikro-orm/core';
import { User } from './entities/User';
import { Post } from './entities/Post';
import config from '../mikro-orm.config';
(async () => {
const orm = await MikroORM.init(config);
const em = orm.em.fork();
// Create user
const user = new User();
user.name = 'Test';
user.email = 'test@example.com';
// Create post
const post1 = new Post();
post1.title = 'First Post';
post1.content = 'This is the content of the first post';
post1.author = user;
const post2 = new Post();
post2.title = 'Second Post';
post2.content = 'This is the content of the second post.';
post2.author = user;
// Save database
await em.persistAndFlush([user, post1, post2]);
console.log('Data seeded successfully.');
await orm.close(true);
})();
Sau đó, bạn chạy lệnh này để thêm dữ liệu vào database:
ts-node src/seed.ts
Viết các CRUD đơn giản
import { MikroORM } from '@mikro-orm/core';
import { User } from './entities/User';
import { Post } from './entities/Post';
import config from '../mikro-orm.config';
(async () => {
const orm = await MikroORM.init(config);
const em = orm.em.fork();
// READ
const users = await em.find(User, {}, { populate: ['posts'] });
console.log('Users:', users);
// CREATE
const newUser = new User();
newUser.name = 'Bob';
newUser.email = 'bob@example.com';
await em.persistAndFlush(newUser);
console.log('New user added:', newUser);
// UPDATE
newUser.name = 'Bob Smith';
await em.persistAndFlush(newUser);
console.log('User updated:', newUser);
// DELETE
await em.removeAndFlush(newUser);
console.log('User deleted:', newUser);
await orm.close(true);
})();
Chạy lệnh:
ts-node src/index.ts
B8: Tích hợp để tạo một API đơn giản với MikroORM và ExpressJS
npm install express
npm install @types/express --save-dev
Tạo file src/server.ts
import 'reflect-metadata';
import express from 'express';
import { MikroORM, RequestContext } from '@mikro-orm/core';
import { User } from './entities/User';
import config from '../mikro-orm.config';
const app = express();
app.use(express.json());
let orm: MikroORM;
(async () => {
orm = await MikroORM.init(config);
})();
// Middleware để tạo RequestContext cho mỗi request
app.use((req, res, next) => {
RequestContext.create(orm.em, next);
});
// Endpoint lấy danh sách người dùng
app.get('/users', async (req, res) => {
const em = orm.em.fork();
const users = await em.find(User, {}, { populate: ['posts'] });
res.json(users);
});
// Endpoint thêm người dùng mới
app.post('/users', async (req, res) => {
const em = orm.em.fork();
const user = em.create(User, req.body);
await em.persistAndFlush(user);
res.status(201).json(user);
});
// Khởi chạy server
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
Sau khi bạn thiết lập xong, bạn chạy lệnh: ts-node src/server.ts
sau đó bạn truy cập API tại http://localhost:3000/users
.
- GET /users: Lấy danh sách người dùng.
- POST /users: Thêm người dùng mới (Gửi dữ liệu JSON trong body).
Ví dụ test API sử dụng cURL để thêm người dùng mới:
curl -X POST http://localhost:3000/users \
-H 'Content-Type: application/json' \
-d '{"name": "Charlie", "email": "charlie@example.com"}'
Sử dụng Query Builder để thực hiện truy vấn phức tạp
Ví dụ, bạn muốn tìm các bài viết có tiêu đề chứa từ "First" và sắp xếp theo ngày tạo mới nhất
const posts = await em.createQueryBuilder(Post)
.select('*')
.where({ title: { $like: '%First%' } })
.orderBy({ createdAt: 'DESC' })
.limit(5)
.execute();
console.log('Posts:', posts);
- createQueryBuilder(): tạo một Query Builder mới.
- where(), orderBy(), limit(): xây dựng truy vấn với điều kiện, sắp xếp và giới hạn kết quả.
6. Kết luận
Hy vọng qua bài viết này, bạn đã nắm rõ tổng quan về MikroORM, cách cài đặt và sử dụng vào dự án TypeScript của bạn. Ban đầu, dành thời gian để hiểu rõ cách hoạt động của MikroORM và thực hành vào các dự án.
Các bài viết liên quan:
Bài viết liên quan
Tìm Hiểu Single Sign-On (SSO): Giải pháp đăng nhập một lần
Oct 26, 2024 • 11 min read
Giới thiệu FFmpeg: Hướng dẫn Encode Video và Streaming với HLS
Oct 25, 2024 • 14 min read
SQL Injection là gì? Những cách phòng ngừa SQL Injection
Oct 25, 2024 • 10 min read
So sánh hiệu suất Query của PostgreSQL và MySQL
Oct 24, 2024 • 9 min read
Fastify là gì? So sánh hiệu suất của Fastify và ExpressJS
Oct 22, 2024 • 12 min read
Sequelize là gì? So sánh Sequelize và Prisma
Oct 21, 2024 • 7 min read