TypeORM và Query Builder trong TypeORM

Giới thiệu

TypeORM là một ORM có thể chạy trong các nền tảng NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo và Electron và có thể được sử dụng với TypeScript và JavaScript (ES5, ES6, ES7, ES8). Mục tiêu của nó là luôn hỗ trợ các tính năng JavaScript mới nhất và cung cấp các tính năng bổ sung giúp bạn phát triển bất kỳ loại ứng dụng nào sử dụng cơ sở dữ liệu – từ các ứng dụng nhỏ với một vài bảng đến các ứng dụng doanh nghiệp quy mô lớn với nhiều cơ sở dữ liệu.

ORM là một loại công cụ ánh xạ các thực thể với các bảng cơ sở dữ liệu. ORM cung cấp đơn giản hóa quá trình phát triển bằng cách tự động hóa chuyển đổi đối tượng sang bảng và từ bảng sang đối tượng.

Tổng quan

TypeORM là một thư viện Object Relational Mapper chạy trong node.js và được viết bằng TypeScript. TypeScript là một cải tiến đối với JavaScript với kiểu gõ tùy chọn. TypeScript là một ngôn ngữ biên dịch. Nó không được diễn giải tại thời điểm chạy run-time. Trình biên dịch TypeScript biên dịch file TypeScript (.ts) thành file JavaScript (.js).

TypeORM hỗ trợ nhiều cơ sở dữ liệu như MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana và WebSQL. TypeORM là ORM dễ sử dụng để tạo mới ứng dụng kết nối với cơ sở dữ liệu. Chức năng TypeORM là các khái niệm dành riêng cho RDBMS.

Đặc điểm của TypeORM

Tự động tạo các lược đồ bảng cơ sở dữ liệu dựa trên các mô hình của bạn
Dễ dàng chèn, cập nhật và xóa đối tượng trong cơ sở dữ liệu
Tạo ánh xạ (một-một, một-nhiều và nhiều-nhiều) giữa các bảng
Cung cấp các lệnh CLI đơn giản

Lợi ích của TypeORM

TypeORM rất dễ sử dụng ORM framework với coding đơn giản. Nó có những lợi ích sau:

  1. Các ứng dụng chất lượng cao và được ghép nối lỏng lẻo
  2. Các ứng dụng có thể mở rộng
  3. Dễ dàng tích hợp với các modules khác
  4. Hoàn toàn phù hợp với mọi kiến trúc từ ứng dụng nhỏ đến ứng dụng doanh nghiệp

Trong typeorm có rất nhiều tính năng nhưng trong blog này mình sẽ viết về cách sử dụng query builder của typeorm với typescript.

Query Builder

Query builder được sử dụng để xây dựng các truy vấn SQL phức tạp một cách dễ dàng. Nó được khởi tạo từ phương thức Connection.

Connection

Hãy xem xét một ví dụ đơn giản về cách sử dụng QueryBuilder bằng phương thức kết nối:

import {getConnection} from "typeorm";

const user = await getConnection()
.createQueryBuilder()
.from("user", "user")
.select("user")
.where("user.id = :id", { id: 1 })
.getOne();

Repository

Chúng ta có thể sử dụng repository  để tạo trình tạo truy vấn. Nó được mô tả dưới đây,

import {getRepository} from "typeorm"; 
const user = await getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id", { id: 1 }) 
.getOne();

Adding expression use getConnection (no model needed)

table user: id, name, age

where được sử dụng để lọc các bản ghi nếu điều kiện được khớp.

getConnection().createQueryBuilder().from("user", "user").select("user") .where("user.id = :id", { id: 1 }) .getRawOne();

Truy vấn này tương đương với:

select * from user where user.id=1;

orderBy được sử dụng để sắp xếp các bản ghi dựa trên trường

getConnection().createQueryBuilder().from("user", "user").orderBy("user.name");

Truy vấn này tương đương với:

select * from user order by user.name;

groupBy: Nó được sử dụng để nhóm các bản ghi dựa trên cột được chỉ định.

getConnection().createQueryBuilder().from("user", "user").groupBy("user.id")

Truy vấn này tương đương với:

select * from user group by user.id; 

limit: được sử dụng để giới hạn việc chọn rows.

getConnection().createQueryBuilder().from("user", "user").limit(5);

Truy vấn này tương đương với:

select * from user limit 5; 

offset được sử dụng để chỉ định, có bao nhiêu rows để bỏ qua kết quả.

getConnection().createQueryBuilder().from("user", "user").offset(5);

Truy vấn này tương đương với:

select * from user offset 5;

joins: mệnh đề nối được sử dụng để kết hợp các hàng từ hai hoặc nhiều bảng, dựa trên một cột có liên quan. Ví dụ bên dưới là 2 bảng student với project  có mối quan hệ là 1-n:

student:  id, name, email, phone

project : id, name, student_id

leftJoinAndSelect

const student = await getConnection().createQueryBuilder().from("student", "student")
.leftJoinAndSelect("project", "project", "project.student_id = student.id")
.where("student.name = :name", { name: "Student1" })
.getOne();

Truy vấn này tương đương với:

SELECT student.*, project.* FROM student student LEFT JOIN project project 
ON project.student_id = student.id WHERE student.name = 'Student1'

innerJoinAndSelect

const student = await getConnection().createQueryBuilder().from("student", "student") 
.innerJoinAndSelect("project", "project", "project.student_id = student.id") .where("student.name = :name", { name: "student1" }) .getOne();

Truy vấn trên tương đương với:

SELECT student.*, project.* FROM student student INNER JOIN project project 
ON project.student_id = student.id WHERE student.name = 'student1';

Insert

import {getConnection} from "typeorm"; 

await getConnection()
.createQueryBuilder()
.insert() 
.into('student') 
.values([ { name: "test", email: "test@gmail.com", phone: "09011112222"},
 { name: "test2", email: "test2@gmail.com", phone: "09011112222"}
]) 
.execute();

Update

import {getConnection} from "typeorm"; 

await getConnection().createQueryBuilder() 
.update('student') 
.set({ name: "test3", email: "test3@gmail.com"}) 
.where("id = :id", { id: 1 }) 
.execute(); 

Delete

import {getConnection} from "typeorm"; 

 await getConnection() 
.createQueryBuilder() 
.delete()
.from('student')
.where("id = :id", { id: 1 }) .execute();

Subqueries

Ví dụ 1: Sử dụng subQuery để select field name trong bảng student các record mà có email chứa chuỗi test

Ví dụ 2: Sử dụng subQuery để select id lớn nhất trong bảng project thỏa field name có chứa chuỗi test và select ra studentId, studentName, projectId

const student = await getConnection().createQueryBuilder().select("student.name", "name") 
.from((subQuery) => { return subQuery.select("student.name", "name") 
                     .from("student", "student").where("student.email like :email", { email: '%test%'}) },
                   "student") .getRawMany();

const student = await getConnection().createQueryBuilder()
.select(`student.id as studentId,student.name as studentName,project.id as projectId`)) 
.from("student", "student").leftJoin("project", "project", "project.student_id = student.id")
.where ("project.id = (select max(id) from project where name like '%test%' ")).getRawMany();

Adding expression use getRepository() with model

Ví dụ có model User sau:

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

@Entity('user') 
export class User { 
@PrimaryGeneratedColumn() 
id: number;
 
@Column('varchar', {name: 'name', nullable: true, length: 50}) 
name: string | null; 

@Column('int', {name: 'age', nullable: true}) 
name: number | null; 
}

where được sử dụng để lọc các bản ghi nếu điều kiện được khớp.

getRepository(User).createQueryBuilder("user").where("user.id= :id", { id: 1 });

Truy vấn này tương đương với:

select * from user where user.id=1;

orderBy được sử dụng để sắp xếp các bản ghi dựa trên trường

getRepository(User).createQueryBuilder("user").orderBy("user.name");

Truy vấn này tương đương với:

select * from user order by user.name;

groupBy: Nó được sử dụng để nhóm các bản ghi dựa trên cột được chỉ định.

getRepository(User).createQueryBuilder("user") .groupBy("user.id")

Truy vấn này tương đương với:

select * from user group by user.id; 

limit: được sử dụng để giới hạn việc chọn rows.

getRepository(User).createQueryBuilder("user").limit(5);

Truy vấn này tương đương với:

select * from user limit 5; 

offset được sử dụng để chỉ định, có bao nhiêu rows để bỏ qua kết quả.

getRepository(User).createQueryBuilder("user") .offset(5);

Truy vấn này tương đương với:

select * from user offset 5;

joins: mệnh đề nối được sử dụng để kết hợp các hàng từ hai hoặc nhiều bảng, dựa trên một cột có liên quan. Hãy xem xét hai thực thể:

import {Entity, PrimaryGeneratedColumn, Column, OneToMany} from "typeorm";
import {Project} from "./Project"; 

@Entity('student')
export class Student { 
@PrimaryGeneratedColumn()
id: number; 

@Column('varchar', {name: 'name', nullable: true, length: 50})
name: string | null;

@Column('varchar', {name: 'email', nullable: true, length: 30})
email: string | null;

@Column('varchar', {name: 'phone', nullable: true, length: 11})
phone: string | null;

@OneToMany(()=> Project, project => project.student) 
projects: project[]; 
} 
import {Entity, PrimaryGeneratedColumn, Column, ManyToOne}

from "typeorm"; import {Student} from "./Student";

@Entity('project') 
export class Project {

@PrimaryGeneratedColumn() 
id: number;

@Column('varchar', {name: 'name', nullable: true, length: 50})
name: string | null;

@Column('bigint', {name: 'student_id', nullable: true, unsigned: true})
studentId: number | null;

@ManyToOne(() => Student, student => student.projects)
@JoinColumn([{name: 'student_id', referencedColumnName: 'id'}])
student : Student;
}

leftJoinAndSelect

const student = await getRepository(Student).createQueryBuilder("student")
.leftJoinAndSelect("student.projects", "project")
.where("student.name = :name", { name: "Student1" })
.getOne();

Truy vấn này tương đương với:

SELECT student.*, project.* FROM student student LEFT JOIN projects project 
ON project.student = student.id WHERE student.name = 'Student1'

innerJoinAndSelect

const student = await getRepository(Student).createQueryBuilder("student")
.innerJoinAndSelect("student.projects", "project")
.where("student.name = :name", { name: "student1" })
.getOne();

Truy vấn trên tương đương với:

SELECT student.*, project.* FROM students student INNER JOIN projects project 
ON project.student = student.id WHERE student.name = 'Student1';

Insert

import {getConnection} from "typeorm"; 

await getRepository(Student)
.createQueryBuilder() 
.insert() 
.into(Student) 
.values([ { name: "test", email: "test@gmail.com", phone: "09011112222"}, 
{ name: "test2", email: "test2@gmail.com", phone: "09011112222"} ]) 
.execute();

Update

import {getConnection} from "typeorm"; 

await getRepository(Student)
.createQueryBuilder() 
.update(Student) 
.set({ name: "test3", email: "test3@gmail.com"}) 
.where("id = :id", { id: 1 }) .execute();

Delete

import {getConnection} from "typeorm"; 
await getRepository(Student).createQueryBuilder() 
.delete() 
.from(Student) 
.where("id = :id", { id: 1 }) 
.execute();

Kết luận

Nên sử dụng query builder bởi vì nó có thể build các câu sql từ dễ đến phức tạp

Syntax cũng tương tự gần giống với sql thuần nên rất dễ viết code và đọc hiểu

Cũng có thể sử dụng query builder một cách bình thường mà không cần dùng đến khai báo model

Về hiệu năng thì sử dụng query builder thời gian truy xuất sẽ nhanh hơn sử dụng repository api có sẵn của typeorm

Tài liệu tham khảo

https://typeorm.io/