typeorm 有什么用?typeorm 以操作對(duì)象方法的方式與數(shù)據(jù)庫交互,而不是像傳統(tǒng)庫(mysql)那樣需要寫 sql 語句。
本文主要說什么?Typeorm README.md 寫的很長,往下拉可以看到 "Step-by-Step Guide" 標(biāo)題,本文翻譯這部分內(nèi)容(非直譯),幫助新手一步步熟悉并使用 typeorm。(翻譯時(shí) typeorm 版本為 0.2.24。)
接上文.......
表與表之間的關(guān)系
創(chuàng)建一對(duì)一的關(guān)系
PhotoMetadata 類,描述照片的詳細(xì)信息,如長度、寬度、朝向、評(píng)論等信息,該類與 Photo 類的關(guān)系是一對(duì)一的關(guān)系:一張照片有且僅有一份詳細(xì)信息,一份詳細(xì)信息僅屬于一張照片。
創(chuàng)建 src/entity/PhotoMetadata.ts 文件,內(nèi)容如下:
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;
}
上面,我們使用了一個(gè)新的裝飾器 @OneToOne。它允許我們?yōu)閮蓚€(gè) Entity 設(shè)置一對(duì)一的關(guān)系,type => Photo 是一個(gè)函數(shù),指向該類要關(guān)聯(lián)的那個(gè)類。
我們強(qiáng)制使用一個(gè)函數(shù)來返回關(guān)聯(lián)類 @OneToOne(type => Photo),而不是直接使用該類的名字 @OneToOne(Photo),這樣做是由 js 語言特性決定的。(譯者問:具體啥特性?)
我們也可以將函數(shù)寫成 () => Photo,但是推薦使用 type => Photo,因?yàn)檫@樣更易讀,type 參數(shù)本身沒有任何意義,即它的值是 undefined。
我們還使用了 @JoinColumn 裝飾器,這個(gè)裝飾器可以指定一對(duì)一關(guān)系的擁有者。
Photo 擁有 PhotoMetadata,PhotoMetadata 屬于 Photo,在 PhotoMetadata 上加 @JoinColumn,在建表時(shí)體現(xiàn)在 photo_metadata 表多一個(gè) photoId 這個(gè)外鍵。
運(yùn)行應(yīng)用 $ node dist/testConnect.js ,可以看到數(shù)據(jù)庫里又新增了一張 photo_metadata 表,并且包含了關(guān)聯(lián) photo 表的外鍵。
+-------------+--------------+----------------------------+
| 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 |
+-------------+--------------+----------------------------+
保存一對(duì)一的關(guān)系
現(xiàn)在讓我們保存一張照片及其元數(shù)據(jù),并將它們彼此連接起來。
創(chuàng)建 src/testRelation.ts 文件,內(nèi)容如下:
import { createConnection } from "typeorm";
import { Photo } from "./entity/Photo";
import { PhotoMetadata } from "./entity/PhotoMetadata";
createConnection(/*...*/).then(async connection => {
// 創(chuàng)建 Photo 對(duì)象
let photo = new Photo();
photo.name = "Me and Bears";
photo.views = 2;
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.isPublished = true;
// 創(chuàng)建 PhotoMetadata 對(duì)象
let metadata = new PhotoMetadata();
metadata.height = 640;
metadata.width = 480;
metadata.compressed = true;
metadata.comment = "cybershoot";
metadata.orientation = "portait";
metadata.photo = photo; // <=== 將 Photo 和 PhotoMetadata 關(guān)聯(lián)起來
// 獲取 repository
let photoRepository = connection.getRepository(Photo);
let metadataRepository = connection.getRepository(PhotoMetadata);
// 首先保存照片
await photoRepository.save(photo);
// 照片保存之后,再保存元數(shù)據(jù),這是因?yàn)樵獢?shù)據(jù)中包含了照片數(shù)據(jù)的外鍵
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));
執(zhí)行程序 $ node dist/testRelation.js,查看數(shù)據(jù)庫新增了一條 photo 數(shù)據(jù),一條 photo_metadata 數(shù)據(jù),并且后者包含前者的外鍵值。
雙向關(guān)系
上面的關(guān)系是單向的,PhotoMetadata 中包含 Photo 的外鍵,因此查詢 PhotoMetadata 時(shí),可以順帶查出 Photo 的信息;
import { createConnection } from "typeorm";
import { PhotoMetadata } from "./entity/PhotoMetadata";
createConnection(/*...*/).then(async connection => {
let photometadataRepository = connection.getRepository(PhotoMetadata);
let photometadatas = await photometadataRepository.find({ relations: ["photo"] });
console.log('photometadatas ==================== \n', photometadatas)
}).catch(error => console.log(error));
查詢結(jié)果,可以看到 PhotoMetadata 中確實(shí)包含了 photo 的數(shù)據(jù)。
photometadatas ====================
[
PhotoMetadata {
id: 1,
height: 640,
width: 480,
orientation: 'portait',
compressed: true,
comment: 'cybershoot',
photo: Photo { # <====== 只是查詢 metadata 數(shù)據(jù),會(huì)順帶把 photo 的數(shù)據(jù)也查出來
id: 3,
name: 'Me and Bears',
description: 'I am near polar bears',
filename: 'photo-with-bears.jpg',
views: 2,
isPublished: true
}
},
// .....
]
上面的代碼,Photo 和 PhotoMetadata 的關(guān)系是單向的,關(guān)系的擁有者是 PhotoMetadata,但 Photo 類不知道任何 PhotoMetadata 的信息,這使得從 Photo 類訪問 PhotoMetadata 變得復(fù)雜。
為了解決這個(gè)問題,我們應(yīng)該為 Photo 也添加一個(gè)關(guān)系,使得 Photo 和 PhotoMetadata 變成雙向關(guān)系。
(改成雙向關(guān)系后,查詢 Photo 信息,也可以順帶查詢出 PhotoMetadata 的信息)
修改 PhotoMetadata 類:
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;
}
修改 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;
}
查詢關(guān)系對(duì)象數(shù)據(jù)
現(xiàn)在我們可以用一條查詢語句得到照片信息和它的元數(shù)據(jù)信息,有兩種方式可以做到這點(diǎn):
- 使用 find* 方法
- 使用 QueryBuilder 函數(shù)
使用 find* 方法,該方法允許您指定關(guān)聯(lián)關(guān)系的對(duì)象。(下面??代碼關(guān)聯(lián)關(guān)系的對(duì)象是 Photo 類中的 metadata 屬性)
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"] });
console.log('photos', photos)
}).catch(error => console.log(error));
返回的 photos 是從數(shù)據(jù)庫中查詢的照片數(shù)組,每張照片都包含元數(shù)據(jù)信息。
photos [
Photo {
id: 3,
name: 'Me and Bears',
description: 'I am near polar bears',
filename: 'photo-with-bears.jpg',
views: 2,
isPublished: true,
metadata: PhotoMetadata { # <=== 查 photo 也能順帶把 metadata 數(shù)據(jù)查出來
id: 1,
height: 640,
width: 480,
orientation: 'portait',
compressed: true,
comment: 'cybershoot'
}
},
]
使用 find() 是個(gè)簡單有效的方式,但有時(shí)您需要更加復(fù)雜的查詢,此時(shí)可以使用 QueryBuilder(),QueryBuilder() 允許用優(yōu)雅的方式 處理復(fù)雜的查詢。
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();
console.log('photos', photos);
}).catch(error => console.log(error));
當(dāng)你使用 QueryBuilder() 時(shí),看起來就像創(chuàng)建 sql 查詢語句一樣。在這個(gè)例子中,photo 和 metadata 都是別名,你可以使用別名訪問列。
使用 cascade 選項(xiàng)來自動(dòng)保存關(guān)系對(duì)象
上面 保存 Photo 和 PhotoMetadata,我們分別做了兩次保存操作。
// ......
// 首先保存照片
await photoRepository.save(photo);
// 照片保存之后,再保存元數(shù)據(jù),這是因?yàn)樵獢?shù)據(jù)中包含了照片數(shù)據(jù)的外鍵
await metadataRepository.save(metadata);
我們希望只做一次保存操作就可以完成保存 Photo 和 PhotoMetadata,可以使用 cascade 選項(xiàng)。
修改 Photo 類中 @OneToOne 裝飾器:
export class Photo {
/// ... other columns
@OneToOne(type => PhotoMetadata, metadata => metadata.photo, {
cascade: true, // <=== 加上 cascade 選項(xiàng)
})
metadata: PhotoMetadata;
}
因?yàn)樵O(shè)置了級(jí)聯(lián)選項(xiàng)(cascade: true),現(xiàn)在保存 photo 時(shí),會(huì)自動(dòng)保存 metadata 數(shù)據(jù)。
createConnection(options).then(async connection => {
// 創(chuàng)建 photo 對(duì)象
let photo = new Photo();
photo.name = "Me and Bears";
photo.views = 3;
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.isPublished = true;
// 創(chuàng)建 metadata 對(duì)象
let metadata = new PhotoMetadata();
metadata.height = 640;
metadata.width = 480;
metadata.compressed = true;
metadata.comment = "cybershoot";
metadata.orientation = "portait";
// 將 photo 和 metadata 聯(lián)系起來
photo.metadata = metadata;
// 獲取 repository
let photoRepository = connection.getRepository(Photo);
// 做一次保存操作,可以完成保存 photo 和保存 metadata 操作
await photoRepository.save(photo);
console.log("Photo is saved, photo metadata is saved too.")
}).catch(error => console.log(error));
注意這里我們?cè)O(shè)置了 photo 對(duì)象的 metadata 屬性,而不是像之前那樣設(shè)置 metadata 的 photo 屬性。因?yàn)槟闶窃?photo 類中設(shè)置的 cascade 配置,因此保存 photo,會(huì)自動(dòng)保存 metadata,但是保存 metadata,并不會(huì)自動(dòng)保存 photo。
創(chuàng)建(多對(duì)一)/(一對(duì)多)關(guān)系
讓我們創(chuàng)建一個(gè)(一對(duì)多)/(多對(duì)一)的關(guān)系,每張照片都有唯一一個(gè)作者,每個(gè)作者可以同時(shí)擁有很多張照片。
創(chuàng)建 src/entity/Author.ts 文件,內(nèi)容如下:
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from "typeorm";
import { Photo } from "./Photo";
@Entity()
export class Author {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(type => Photo, photo => photo.author) // note: 我們還需要在 Photo 類中添加另一種關(guān)系
photos: Photo[];
}
@OneToMany 不能單獨(dú)存在,需要在另一個(gè)類中添加 @ManyToOne。
修改 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;
}
@ManyToOne 裝飾器和 @JoinColumn 裝飾器類似,會(huì)為 photo 表添加一個(gè) authorId 的外鍵。
運(yùn)行應(yīng)用 $ node dist/testConnect.js,數(shù)據(jù)庫會(huì)自動(dòng)創(chuàng)建 Author 表:
+-------------+--------------+----------------------------+
| author |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
+-------------+--------------+----------------------------+
我們發(fā)現(xiàn) Photo 表也被修改了,新增了 authorId 列,作為 Author 表的外鍵:
+-------------+--------------+----------------------------+
| photo |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
| description | varchar(255) | |
| filename | varchar(255) | |
| isPublished | boolean | |
| authorId | int(11) | FOREIGN KEY |
+-------------+--------------+----------------------------+
創(chuàng)建多對(duì)多關(guān)系
讓我們創(chuàng)建多對(duì)多的關(guān)系:一張照片可以保存在多個(gè)相冊(cè)中(同一張照片可以打印很多張,但還是同一張照片),一個(gè)相冊(cè)中可以存很多張照片。
創(chuàng)建 src/entity/Album.ts 文件,內(nèi)容如下:
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm";
import { Photo } from "./Photo"
@Entity()
export class Album {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(type => Photo, photo => photo.albums)
@JoinTable()
photos: Photo[];
}
@JoinTable 必須被明確指定這是關(guān)系的所有者。
修改 Photo 類添加關(guān)系:
import { ..., ManyToMany } from "typeorm";
import { Album } from './Album'
export class Photo {
/// ... other columns
@ManyToMany(type => Album, album => album.photos)
albums: Album[];
}
運(yùn)行應(yīng)用 $ node dist/testConnect.js,數(shù)據(jù)庫會(huì)自動(dòng)創(chuàng)建 album 表和 album_photos_photo 表:
+-------------+--------------+----------------------------+
| album |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY FOREIGN KEY |
| name | varchar(255) | PRIMARY KEY FOREIGN KEY |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
| album_photos_photo |
+-------------+--------------+----------------------------+
| album_id | int(11) | PRIMARY KEY FOREIGN KEY |
| photo_id | int(11) | PRIMARY KEY FOREIGN KEY |
+-------------+--------------+----------------------------+
多對(duì)多的關(guān)系會(huì)創(chuàng)建一張中間表。
現(xiàn)在讓我們往數(shù)據(jù)庫中插入一些相冊(cè)和照片:
createConnection(options).then(async connection => {
// 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.views = 2;
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"] });
}).catch(error => console.log(error));
查詢結(jié)果:
loadedPhoto Photo {
id: 1,
name: 'Me and Bears',
description: 'I am near polar bears',
filename: 'photo-with-bears.jpg',
views: 1,
isPublished: true,
albums: []
}
總結(jié):
- 雙向關(guān)系的目的是做一次查詢操作,可以同時(shí)查出 Photo 以及它的 metadata 數(shù)據(jù);
- cascade 的目的是只要做一次保存操作,就可以完成 photo 和 metadata 的保存;
- 多對(duì)多的關(guān)系會(huì)創(chuàng)建一張中間表。