Typeorm là gì? Hướng Dẫn Cài Đặt và sử dụng Typeorm cơ bản

typeorm là gì

TypeORM là gì?

TypeORM là một ORM có thể chạy trên các nền tảng NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo và Electron và còn dùng được với TypeScript và JavaScript (ES5, ES6, ES7, ES8). Công cụ ra đời nhằm 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 ứng dụng tích hợp cơ sở dữ liệu – từ các ứng dụng nhỏ với chỉ vài table đến các ứng dụng doanh nghiệp quy mô lớn có nhiều cơ sở dữ liệu.
TypeORM hỗ trợ cả pattern Active Record và Data Mapper, khác với tất cả các ORM JavaScript hiện có, với TypeORM bạn có thể viết các ứng dụng high quality, loosely coupled, scalable và maintainable thật hiệu quả nhất.
TypeORM kế thừa mạnh mẽ từ các ORMs khác, bao gồm Hibernate, DoctrineEntity Framework.

Các tính năng của TypeORM

  • hỗ trợ cả DataMapper và ActiveRecord (bạn tự chọn nhé)
  • entities và columns
  • nhiều dạng cột cho database cụ thể
  • entity manager
  • repositories và repositories tùy chỉnh
  • mô hình chủ thể quan hệ rõ ràng
  • associations (hay relations)
  • eager relations và lazy relations
  • uni-directional, bi-directional và self-referenced relations
  • hỗ trợ inheritance patterns
  • cascades
  • indices
  • transactions
  • migrations và automatic migrations generation
  • connection pooling
  • replication
  • dùng nhiều kết nối database
  • làm việc với nhiều kiểu databases
  • cross-database và cross-schema queries
  • QueryBuilder gọn gàng, linh hoạt và mạnh mẽ
  • left và inner joins
  • pagination tối ưu cho các queries bằng joins
  • query caching
  • streaming kết quả thô
  • logging
  • listeners và subscribers (hooks)
  • hỗ trợ closure table pattern
  • schema declaration trong models hoặc các file config tách biệt
  • connection configuration theo định dạng json / xml / yml / env
  • hỗ trợ MySQL / MariaDB / Postgres / CockroachDB / SQLite / Microsoft SQL Server / Oracle / sql.js
  • hỗ trợ MongoDB NoSQL database
  • làm việc với các nền tảng NodeJS / Browser / Ionic / Cordova / React Native / NativeScript / Expo / Electron
  • hỗ trợ TypeScript và JavaScript
  • code đầu ra sẽ làm việc hiệu quả, linh hoạt, sạch và dễ maintain
  • Tuân theo hầu hết các best practice
  • CLI
Với TypeORM, model của bạn sẽ trông như thế này:
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}
Và domain logic:
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.age = 25;
await repository.save(user);

const allUsers = await repository.find();
const firstUser = await repository.findOne(1); // find by id
const timber = await repository.findOne({ firstName: "Timber", lastName: "Saw" });

await repository.remove(timber);
Mặt khác, nếu muốn dùng ActiveRecord, bạn cũng có thể làm được như sau:
import {Entity, PrimaryGeneratedColumn, Column, BaseEntity} from "typeorm";

@Entity()
export class User extends BaseEntity {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}
Và your domain logic sẽ giống như sau:
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.age = 25;
await user.save();

const allUsers = await User.find();
const firstUser = await User.findOne(1);
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" });

await timber.remove();

Cài Đặt Typeorm

Để clone hay tải công cụ, hãy truy cập vào trang Github chính thức và thực hiện các bước sau đây:
1. Cài đặt npm package: npm install typeorm --save
2. Bạn cần cài đặt shim reflect-metadata: npm install reflect-metadata --save
và import vào phần global của ứng dụng (ví dụ như ở app.ts): import "reflect-metadata";
3. Bạn có thể sẽ cần cài thêm node typings: npm install @types/node --save
4. Cài đặt thêm database driver:

    • cho MySQL hoặc MariaDBnpm install mysql --save (Bạn cũng có thể cài đặt mysql2 để thay thế)
    • cho PostgreSQL hoặc CockroachDBnpm install pg --save
    • cho SQLitenpm install sqlite3 --save
    • cho Microsoft SQL Servernpm install mssql --save
    • cho sql.jsnpm install sql.js --save
    • cho Oracle npm install oracledb --saveChỉ nên cài một cái thôi, tùy thuộc vào database đang sử dụng. Để giúp Oracle driver hoạt động, bạn cần tuân theo các hướng dẫn cài đặt từ page chính thức.
    • cho MongoDB (vẫn ở giai đoạn thử nghiệm) npm install mongodb --save
    • cho NativeScript, react-nativeCordova. Xem thử documentation of supported platforms

Tùy chỉnh TypeScript

Đồng thời, hãy đảm bảo bạn đang sử dụng compiler TypeScript từ phiên bản 3.3 trở lên, và đã kích hoạt setting sau trong tsconfig.json:
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
es6 trong section lib của compliler option có thể cũng cần được kích hoạt, hay cay cài đặt thêm es6-shim từ @types.

Quick start

Sử dụng lệnh CLI để khởi tạo project là cách nhanh nhất đề bắt đầu với TypeORM. Quick start chỉ hoạt động khi bạn sử dụng TypeORM trong ứng dụng NodeJS. Nếu bạn sử dụng các nền tảng khác, hãy xem scroll xuống phần hướng dẫn chi tiết nhé.
Trước hết, cài đặt TypeORM globally:
npm install typeorm -g


Sau đó đến thư mục bạn muốn tạo project mới và chạy lệnh:
typeorm init --name MyProject --database mysql


Trong đó name là tên của project và database là database bạn sẽ dùng. Database có thể mang các giá trị sau: mysql, mariadb, postgres, cockroachdb, sqlite, mssql, oracle, mongodb, cordova, react-native, expo, nativescript.
This command will generate a new project in the MyProject directory with the following files:
Lệnh sau sẽ tạo project mới trong thư mục MyProject với các file sau:
MyProject
├── src              // place of your TypeScript code
│   ├── entity       // place where your entities (database models) are stored
│   │   └── User.ts  // sample entity
│   ├── migration    // place where your migrations are stored
│   └── index.ts     // start point of your application
├── .gitignore       // standard gitignore file
├── ormconfig.json   // ORM and database connection configuration
├── package.json     // node module dependencies
├── README.md        // simple readme file
└── tsconfig.json    // TypeScript compiler options


Bạn cũng có thể chạy typeorm init lên một project node sẵn có, nhưng cần chú ý, một số file có thể bị viết đè.
Tiếp theo, ta cần cài đặt thêm dependencies cho project mới:
cd MyProject
npm install


Trong quá trình cài đặt, hãy chỉnh sửa tệp ormconfig.json và nhập các tùy chọn liên kết cho database vào đây:
{
   "type": "mysql",
   "host": "localhost",
   "port": 3306,
   "username": "test",
   "password": "test",
   "database": "test",
   "synchronize": true,
   "logging": false,
   "entities": [
      "src/entity/**/*.ts"
   ],
   "migrations": [
      "src/migration/**/*.ts"
   ],
   "subscribers": [
      "src/subscriber/**/*.ts"
   ]
}
Hơn nữa, trong đa số trường hợp, bạn sẽ chỉ cần tùy chỉnh host, username, password, database port.
Khi đã xong phần tùy chỉnh và cặt tất cả node module, bạn có thể chạy ứng dụng ngay:
npm start


Xong rồi đấy, ta đã cùng tìm hiểu typeorm là gì cũng như cách thức cài đặt và sử dụng công cụ tuyệt vời này. Nếu không có trục trặc gì khác bạn đã có thể chạy ứng dụng vè thêm user vào data base rồi. Sau này, bạn hoàn toàn có thể thoải mái tích hợp module và khởi tạo thêm nhiều chủ thể nữa.
Kế tiếp sẽ là phần hướng dẫn chi tiết hơn cho những bạn sử dụng nền tảng khác node js.

Hướng dẫn chi tiết

Bạn mong đợi gì từ một ORM? Trước hết, nó phải tạo được bảng database và tìm/thêm/cập nhật/xóa data mà không phải tốn công viết một loạt query SQL mà không cách nào maintain nổi.

Tạo model

Làm việc với database bắt đầu từ việc tạo bảng. Để tạo bảng, ta phải thông qua model. Model trong ứng dụng sẽ là bảng database của bạn.
For example, you have a Photo model:
Ví dụ, bạn có model Photo:
export class Photo {
    id: number;
    name: string;
    description: string;
    filename: string;
    views: number;
    isPublished: boolean;
}
Và bạn muốn lưu trữ hình ảnh trong database của mình. Để lưu trữ dữ liệu trong database, bạn cần bảng database, và bảng database được tạo từ model. Không phải models nào cũng tạo bảng được, mà chỉ những bảng được chỉnh định là entities (chủ thể) mới có khả năng này.

Tạo entity

Entity là model được decorate bởi decorator @Entity . Một bảng database sẽ được tạo cho các model như thế này. Với TypeORM, bạn sẽ phải liên tục làm việc với entity. Tương tự, bạn có thể thoải mái thực hiện load/insert/update/remove/… cho entity.
Hãy thử biến model Photo thành một entity nhé:
import {Entity} from "typeorm";

@Entity()
export class Photo {
    id: number;
    name: string;
    description: string;
    filename: string;
    views: number;
    isPublished: boolean;
}
Đến đây, một bảng database sẽ được tạo cho entity Photo và ta sẽ có thể làm việc với nó mọi nơi trong ứng dụng. Ta đã tạo bảng database, tuy nhiên bảng mà không có cột còn gì là bảng? Vậy tiếp theo mình thử tạo bảng nhé.

Thêm cột cho bảng

Để thêm cột cho database, bạn chỉ việc dùng decorator @Column để decorate cho property nào mà bạn muốn chuyển thành cột của entity tương ứng.
import {Entity, Column} from "typeorm";

@Entity()
export class Photo {

    @Column()
    id: number;

    @Column()
    name: string;

    @Column()
    description: string;

    @Column()
    filename: string;

    @Column()
    views: number;

    @Column()
    isPublished: boolean;
}
Đến đây các cột id, name, description, filename, viewsisPublished sẽ được thêm vào bảng photo. Type của cột trong database sẽ được quyết định dựa trên property type bạn đã dùng, ví dụ như number sẽ được convert thành integer, string trở thành varchar, boolean trở thành bool,v.v… Bạn cũng có thể sử dụng bất cứ column type nào được database hỗ trợ sau khi chỉ định trong decorator @Column.
Như vậy ta đã tạo được bảng với cột, nhưng vẫn chưa xong đâu. Mỗi bảng database phải có một cột với key chính.

Tạo cột “tự biến”

Hiện nay, giả sử bạn muốn column id được auto-generated (hay có thể nói là column identity tự increment/sequence/serial/generate). Để thự hiện, bạn cần thay đổi decorator @PrimaryColumn thành decorator @PrimaryGeneratedColumn:
import {Entity, Column, PrimaryGeneratedColumn} from "typeorm";

@Entity()
export class Photo {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    description: string;

    @Column()
    filename: string;

    @Column()
    views: number;

    @Column()
    isPublished: boolean;
}

Column data types

Kế tiếp, ta cùng chỉ định data type. Theo mặc định, string sẽ được map đến một type dạng varchar(255) – tùy theo database type. Còn số sẽ được map đến một type dạng interger(số nguyên) – tùy theo database type. Hiển nhiên, tất cả các cột ta đang có không thể nào chỉ thuộc mỗi hai type được. Hãy cùng set up đúng dạng data type nhé:
import {Entity, Column, PrimaryGeneratedColumn} from "typeorm";

@Entity()
export class Photo {

    @PrimaryGeneratedColumn()
    id: number;

    @Column({
        length: 100
    })
    name: string;

    @Column("text")
    description: string;

    @Column()
    filename: string;

    @Column("double")
    views: number;

    @Column()
    isPublished: boolean;
}
Column types sẽ thay đổi tùy theo database. Bạn có thể thiết đặt bất kỳ data type nào được database hỗ trợ. Để tìm hiểu thêm các column tupe được hỗ trợ, đọc thêm tại đây

Tạo liên kết đến database

Ở đây, mỗi khi entity của chúng ta được tạo lập, hãy tạo thêm file index.ts (hay app.ts bạn muốn gọi thế nào cũng được) và thiết lập liên kết tại đây:
import "reflect-metadata";
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "test",
    entities: [
        Photo
    ],
    synchronize: true,
    logging: false
}).then(connection => {
    // here you can start to work with your entities
}).catch(error => console.log(error));
Trong ví dụ này chúng ta đang sử dụng MySQL, nhưng bạn cũng có thể database khác được hỗ trợ nhé. Với database khác, bạn chỉ việc đổi type trong phần option với database bạn đang sử dụng:  mysql, mariadb, postgres, cockroachdb, sqlite, mssql, oracle, cordova, nativescript, react-native, expo, hay mongodb. Còn nữa, hãy đảm bảo bạn đang dùng host, port, username, password và database setting của mình nhé.
Vậy là chúng ta đã thêm entity Photo vào list entity cho liên kết này. Mỗi entity bạn đang sử dụng trong liên kết phải được liệt kê ở đây.
Tùy chọn synchronize sẽ giúp cho entity đồng bộ hóa với database mỗi khi bạn chạy ứng dụng.

Load tất cả entities từ thư mục

Sau này, khi tạo càng nhiều entity hơn, ta cần add chúng thành entities trong tùy chọn của mình. Khá là bất tiện, thay vào đó ta có thể set up nguyên một tư mục giúp kết nối tất cả entity và lấy dùng chung cho liên kết của mình:
import {createConnection} from "typeorm";

createConnection({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "test",
    entities: [
        __dirname + "/entity/*.js"
    ],
    synchronize: true,
}).then(connection => {
    // here you can start to work with your entities
}).catch(error => console.log(error));
Nhưng bạn cần cẩn thận với cách làm này. Nếu đang dùng ts-node, thì thay vào đó bạn cần phải chỉ định đường dẫn tới file .ts. Nếu đang dùng outDir thì bạn sẽ phải chỉ định đường dẫn đến file .js trong thư mục outDir. Nếu dùng outDir và khi xóa hay đặt lại tên của entity, hãy dọn sạch thư mục outDir và compile project lại lần nữa, bới vì khi bạn xóa file .ts gốc, phiên bản .js đã compile lại không bị xóa khỏi thư mục đầu ra và vận được TypeORM load (vì chúng vẫn còn nằm trong thư mục outDir).

Chạy ứng dụng

Lúc này nếu bạn chạy index.ts, một liên kết với database sẽ được khởi động và tạo lập một bảng database cho hình ảnh của mình.
+-------------+--------------+----------------------------+
|                         photo                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(100) |                            |
| description | text         |                            |
| filename    | varchar(255) |                            |
| views       | int(11)      |                            |
| isPublished | boolean      |                            |
+-------------+--------------+----------------------------+

Tạo và thêm hình ảnh vào database

Hãy thử tạo hình ảnh mới và lưu trữ vào database nhé:
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(connection => {

    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.views = 1;
    photo.isPublished = true;

    return connection.manager
            .save(photo)
            .then(photo => {
                console.log("Photo has been saved. Photo id is", photo.id);
            });

}).catch(error => console.log(error));
Ngay khi được lưu lại, entity này sẽ được chỉnh một id mới. Phương thức save sẽ trả kết quả là một phiên bản của cùng một object mà bạn đã pass cho nó. Đây không phải là một bản sao mới y nguyên của object, mà sẽ có thay đổi “id”.

Sử dụng cú pháp async/await

Cùng vận dụng các tính năng ES8 (ES2017) mới nhất và dùng cú pháp async/await để thay thế:
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.views = 1;
    photo.isPublished = true;

    await connection.manager.save(photo);
    console.log("Photo has been saved");

}).catch(error => console.log(error));

Sử dụng Entity Manager

Chúng ta vừa tạo một hình ảnh mở và lưu trong database với EntityManager. Với entity manager bạn có thể thao túng bất kỳ entity nào trong ứng dụng. Ví dụ như, hãy thử load entity ta đã lưu:
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let savedPhotos = await connection.manager.find(Photo);
    console.log("All photos from the db: ", savedPhotos);

}).catch(error => console.log(error));
savedPhotos sẽ là một array các object Photo với dữ liệu được load từ database
Tìm hiểu thêm về EntityManager ở đây.

Sử dụng Repositories

Ở phần này, ta hãy thử refactor code và dùng Repository thay cho EntityManager. Mỗi entity đều có repository của riêng nó quản lý tất cả hoạt động của entity đó. Khi cần làm việc nhiều với entity, Repositories sẽ là phương thức tiện lợi hơn so với EntityManagers:
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.views = 1;
    photo.isPublished = true;

    let photoRepository = connection.getRepository(Photo);

    await photoRepository.save(photo);
    console.log("Photo has been saved");

    let savedPhotos = await photoRepository.find();
    console.log("All photos from the db: ", savedPhotos);

}).catch(error => console.log(error));
Tìm hiểu thêm về Repository tại đây.

Loading từ database

Với Repository, ta hãy thử load operation:
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let allPhotos = await photoRepository.find();
    console.log("All photos from the db: ", allPhotos);

    let firstPhoto = await photoRepository.findOne(1);
    console.log("First photo from the db: ", firstPhoto);

    let meAndBearsPhoto = await photoRepository.findOne({ name: "Me and Bears" });
    console.log("Me and Bears photo from the db: ", meAndBearsPhoto);

    let allViewedPhotos = await photoRepository.find({ views: 1 });
    console.log("All viewed photos: ", allViewedPhotos);

    let allPublishedPhotos = await photoRepository.find({ isPublished: true });
    console.log("All published photos: ", allPublishedPhotos);

    let [allPhotos, photosCount] = await photoRepository.findAndCount();
    console.log("All photos: ", allPhotos);
    console.log("Photos count: ", photosCount);

}).catch(error => console.log(error));

Updating trong database

Hãy thử load một hình ảnh từ database, update và lưu trữ hình ảnh này:
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoToUpdate = await photoRepository.findOne(1);
    photoToUpdate.name = "Me, my friends and polar bears";
    await photoRepository.save(photoToUpdate);

}).catch(error => console.log(error));
Giờ đây hình ảnh với id = 1 sẽ được update trong database.

Xóa khỏi database

Kế tiếp hãy thử xóa photo khỏi database:
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoToRemove = await photoRepository.findOne(1);
    await photoRepository.remove(photoToRemove);

}).catch(error => console.log(error));
Giờ đây photo có id = 1 sẽ được xỏa bỏ khỏi database.

Tạo quan hệ one-to-one

Hãy thử tạo một quan hệ one-to-one với class khác. Trước hết, tạo new class trong PhotoMetadata.ts. Class PhotoMetadata buộc phải chứa meta-information bổ sung cho hình ảnh:
import {Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn} from "typeorm";
import {Photo} from "./Photo";

@Entity()
export class PhotoMetadata {

    @PrimaryGeneratedColumn()
    id: number;

    @Column("int")
    height: number;

    @Column("int")
    width: number;

    @Column()
    orientation: string;

    @Column()
    compressed: boolean;

    @Column()
    comment: string;

    @OneToOne(type => Photo)
    @JoinColumn()
    photo: Photo;
}
Ở đây, chúng ta đang sử dụng một decorator mới là @OneToOne cho phép tạo quan hệ one-to-one giữa hai entity. Trong đó, type => Photo là hàm trả kết quả là class của entity ta cần tạo quan hệ. Vì đặc trưng của ngôn ngữ, thay vì dùng class trực tiếp, chúng ta buộc phải gián dùng hàm cho class. Ta cũng có thể viết thành () => Photo, nhưng chúng tôi vẫn nên dùng type => Photo để code dễ đọc hơn. Bản thân type variable không chứa bất kỳ thứ gì cả.
Decorator @JoinColumn cũng sẽ được thêm vào đây, decorator chỉ ra phía “làm chủ” mối quan hệ. Quan hệ có thể là đơn hướng hay song hướng. Nhưng chỉ một phía của quan hệ mới có thể làm chủ. Việc sử dụng decorator @JoinColumn cho phía làm chủ quan hệ là bắt buộc.
Nếu chạy ứng dụng, bạn sẽ thấy một bảng mới chứa cột key ngoại lại cho quan hệ hình ảnh:
+-------------+--------------+----------------------------+
|                     photo_metadata                      |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| height      | int(11)      |                            |
| width       | int(11)      |                            |
| comment     | varchar(255) |                            |
| compressed  | boolean      |                            |
| orientation | varchar(255) |                            |
| photoId     | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

Lưu quan hệ one-to-one

Chúng ta cùng lưu photo, metadata của ảnh và liên kết chúng với nhau.
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    // create a photo
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.isPublished = true;

    // create a photo metadata
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";
    metadata.photo = photo; // this way we connect them

    // get entity repositories
    let photoRepository = connection.getRepository(Photo);
    let metadataRepository = connection.getRepository(PhotoMetadata);

    // first we should save a photo
    await photoRepository.save(photo);

    // photo is saved. Now we need to save a photo metadata
    await metadataRepository.save(metadata);

    // done
    console.log("Metadata is saved, and relation between metadata and photo is created in the database too");

}).catch(error => console.log(error));

Quan hệ nghịch đảo

Ta có quan hệ đơn hướng và quan hệ song hướng. Hiện nay, quan hệ ta đang có giữa PhotoMetadata và Photo đang là quan hệ đơn hướng. Phía làm chủa quan hệ là PhotoMetadata, và Photo không biết gì về PhotoMetadata cả. Như vậy, việc truy cập PhotoMetadata từ phía Photo sẽ trở nên khó khăn hơn. Để xử lý vấn đề này chúng ta nên thêm một quan hệ nghịch đảo, biến mối quan hệ giữa PhotoMetadata và Photo thành song hướng. Hãy thử thực hiện trên entity:
import {Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn} from "typeorm";
import {Photo} from "./Photo";

@Entity()
export class PhotoMetadata {

    /* ... other columns */

    @OneToOne(type => Photo, photo => photo.metadata)
    @JoinColumn()
    photo: Photo;
}
import {Entity, Column, PrimaryGeneratedColumn, OneToOne} from "typeorm";
import {PhotoMetadata} from "./PhotoMetadata";

@Entity()
export class Photo {

    /* ... other columns */

    @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo)
    metadata: PhotoMetadata;
}
photo => photo.metadata là một hàm trả kết quả là têm của phía nghịch đảo của mối quan hệ. Ở đây ta cho thấy rằng tính chất matadata của class Photo là nơi lưu trữ PhotoMetadata trong class Photo. Thay vì pass một hàm trả property của hình ảnh, bạn cũng có thể chỉ việc pass một string cho decorator @OneToOne, như "metadata". Nhưng ở đây chúng ta hãy đi theo cách dùng hàm để giúp quá trình refactor dễ dàng hơn.
Cần chú ý, ta chỉ nên dùng decorator @JoinColumn ở một phía của mối quan hệ. Bên nào được gán decorator này sẽ trở thành phía làm chủ của mối quan hệ. Phí làm chủ của mối quan hệ có một cột chứa key ngoại lai trong database.

Loading objects và quan hệ của chúng

Giờ, ta hãy thử load photos và metadata trong cùng một query duy nhất. Sẽ có hai cách làm, dùng lệnh find* hoặc hàm QueryBuilder. Ta sẽ dùng lệnh find* trước. Lệnh find* cho phép bạn chỉ định object với giao diện FindOneOptions / FindManyOptions.
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoRepository = connection.getRepository(Photo);
    let photos = await photoRepository.find({ relations: ["metadata"] });

}).catch(error => console.log(error));
Ở đây, photos sẽ chứa một loạt ảnh từ database, và mỗi ảnh chứa photo metadata của riêng nó. Tìm hiểu thêm về Find Options trong tài liệu sau.
Find option là một phương pháp hữu hiệu và đơn giản, nhưng với một query phức tạp hơn, thay vào đó hãy dùng QueryBuilder. QueryBuilder cho phép bạn dễ dàng sử dụng các query phức tạp:
import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photos = await connection
            .getRepository(Photo)
            .createQueryBuilder("photo")
            .innerJoinAndSelect("photo.metadata", "metadata")
            .getMany();


}).catch(error => console.log(error));
QueryBuilder cho phép tạo và thực thi truy vấn SQL ở hầu hết mọi cấp độ phức tạp. Khi làm việc với QueryBuilder, hãy nghĩ như thể bạn đang tạo truy vấn SQL mới. Trong ví dụ này, “photo” và “metadata” là các bí danh áp dụng cho các hình ảnh được lựa chọn. Bạn dùng bí danh để truy cập và cột và các tính chất của data được chỉ định.

Dùng cascades để tự động save save objects liên quan

Chúng ta có thể thiết lập tùy chọn cascade (chuỗi thao tác), trong các trường hợp khi cần save các object liên quan ngay khi object khác được save. Hãy cùng thay đổi decorator @OneToOne của photo một chút xíu:
export class Photo {
    /// ... other columns

    @OneToOne(type => PhotoMetadata, metadata => metadata.photo, {
        cascade: true,
    })
    metadata: PhotoMetadata;
}
cascade cho phép chúng ta không chỉ save từng photo riêng rẻ mà cả metadata kèm theo chúng. Giờ đây chúng tả chỉ việc lưu một photo object, và metadata object cũng sẽ tự động lưu với tùy chọn cascade.
createConnection(options).then(async connection => {

    // create photo object
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.isPublished = true;

    // create photo metadata object
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";

    photo.metadata = metadata; // this way we connect them

    // get repository
    let photoRepository = connection.getRepository(Photo);

    // saving a photo also save the metadata
    await photoRepository.save(photo);

    console.log("Photo is saved, photo metadata is saved too.")

}).catch(error => console.log(error));

Tạo quan hệ many-to-one / one-to-many

Hãy cùng tạo quan hệ many-to-one / one-to-many. Giả sử mỗi photo có một author, và mỗi author có thể có nhiều photo. Trước hết, hãy tạo một class Author:
import {Entity, Column, PrimaryGeneratedColumn, OneToMany, JoinColumn} from "typeorm";
import {Photo} from "./Photo";

@Entity()
export class Author {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below
    photos: Photo[];
}
Author chứa một phía nghịch đảo của quan hệ. OneToMany luôn là một phía ngịch đảo của quan hệ, và không thể tồn tại thiếu ManyToOne ở phía bên kia quan hệ.
Kế tiếp hãy thêm phía làm chủ của quan hệ vào entity Photo:
import {Entity, Column, PrimaryGeneratedColumn, ManyToOne} from "typeorm";
import {PhotoMetadata} from "./PhotoMetadata";
import {Author} from "./Author";

@Entity()
export class Photo {

    /* ... other columns */

    @ManyToOne(type => Author, author => author.photos)
    author: Author;
}
Trong quan hệ many-to-one / one-to-many , phía làm chủ luôn là many-to-one. Có nghĩa là class nào dùng @ManyToOne sẽ lưu trữ id của object liên quan.
Sau khi chạy ứng dụng, ORM sẽ tạo bảng author:
+-------------+--------------+----------------------------+
|                          author                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
+-------------+--------------+----------------------------+
Bảng photo cũng sẽ thay đổi, thêm một cột author mới và tạo một key ngoại lai cho nó:
+-------------+--------------+----------------------------+
|                         photo                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| name        | varchar(255) |                            |
| description | varchar(255) |                            |
| filename    | varchar(255) |                            |
| isPublished | boolean      |                            |
| authorId    | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

Tạo quan hệ many-to-many

Hãy tạo quan hệ many-to-one / many-to-many. Giả sử một hình ảnh có thể có nhiêu album, và mỗi album có thể chứa nhiều hình ảnh. Hãy tạo class Album:
import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable} from "typeorm";

@Entity()
export class Album {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @ManyToMany(type => Photo, photo => photo.albums)
    @JoinTable()
    photos: Photo[];
}
@JoinTable bắt buộc để chỉ định rằng đây là phía làm chủ của quan hệ.
Hãy add phía nghịch đảo của quan hệ của cúng ta cho class Photo:
export class Photo {
    /// ... other columns

    @ManyToMany(type => Album, album => album.photos)
    albums: Album[];
}
Sau khi chạy ứng dụng, ORM sẽ tạo môt bảng nối album_photos_photo_albums:
+-------------+--------------+----------------------------+
|                album_photos_photo_albums                |
+-------------+--------------+----------------------------+
| album_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |
| photo_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+
Đừng quên đăng ký class Album với liên kết của bạn trong ORM:
const options: ConnectionOptions = {
    // ... other options
    entities: [Photo, PhotoMetadata, Author, Album]
};
Tiếp đến, hãy chèn thêm album và photo vào database của chúng ta:
let connection = await createConnection(options);

// create a few albums
let album1 = new Album();
album1.name = "Bears";
await connection.manager.save(album1);

let album2 = new Album();
album2.name = "Me";
await connection.manager.save(album2);

// create a few photos
let photo = new Photo();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.albums = [album1, album2];
await connection.manager.save(photo);

// now our photo is saved and albums are attached to it
// now lets load them:
const loadedPhoto = await connection
    .getRepository(Photo)
    .findOne(1, { relations: ["albums"] });
loadedPhoto sẽ tương đương với:
{
    id: 1,
    name: "Me and Bears",
    description: "I am near polar bears",
    filename: "photo-with-bears.jpg",
    albums: [{
        id: 1,
        name: "Bears"
    }, {
        id: 2,
        name: "Me"
    }]
}

Sử dụng QueryBuilder

Bạn có thể sử dụng QueryBuilder để xây dựng SQL query ở mọi mức độ phức tạp. Ví dụ, bạn có thể làm như sau:
let photos = await connection
    .getRepository(Photo)
    .createQueryBuilder("photo") // first argument is an alias. Alias is what you are selecting - photos. You must specify it.
    .innerJoinAndSelect("photo.metadata", "metadata")
    .leftJoinAndSelect("photo.albums", "album")
    .where("photo.isPublished = true")
    .andWhere("(photo.name = :photoName OR photo.name = :bearName)")
    .orderBy("photo.id", "DESC")
    .skip(5)
    .take(10)
    .setParameters({ photoName: "My", bearName: "Mishka" })
    .getMany();
Query này chỉ định tất cả photo đã publish với tên “My” hay “Mishka”. Query sẽ chỉ định kết quả từ vị trí 5 (pagination offset), và sẽ chỉ định chỉ 5 kết quả (pagination limit). Kết quả chỉ định sẽ được sắp xếp giảm dần theo id. Album của photo sẽ được left-join trong khi metadata thì được inner join.
Bạn sẽ sử dụng query builder rất nhiều cho ứng dụng của mình. Tìm hiểu thêm về QueryBuilder tại đây.
Như vậy ta đã tìm hiểu TypeORM là gì, cách cài đặt và sử dụng công cụ tuyệt vời này, đừng quên chia sẻ bài viết nếu bạn thấy bổ ích nhé!

Nhận xét

  1. Thanks you, very easy to understand, same Hibernate in Java

    Trả lờiXóa
  2. bạn giải thích rất chi tiết và có cả link đính kèm hữu dụng. Cảm ơn bạn rất nhiều <3

    Trả lờiXóa
  3. Rất chi tiết nhưng mình góp là bạn nên thêm phần Mục lục và tách thành nhiều bài viết để dễ theo dõi hơn

    Trả lờiXóa

Đăng nhận xét