###### tags: `mysql` `typeORM` # TypeORM ## Getting Started TypeORM是一款可以運行在許多平台上的ORM套件,可以使用TS和JS進行操作,目標是永遠可以使用最新版的JS版本和提供其餘功能來操作資料庫。 TypeORM支援兩種模式`Active Recod`以及`Data Mapper`來操作,這不同於現今其餘的JS ORM,這代表你可以用最有效率的方式寫出高品質、低耦合、可擴充、易維護的程式。 ### Active Record vs Record pattern #### Active Record 使用Active Record方式的話,你會將query的method寫在model本身,可以投過model method來進行存擋、刪除、載入。 簡單來說,Active Record模式就是讓你透過model來與資料庫互動。 以下範例: ```typescript! import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number @Column() firstName: string @Column() lastName: string @Column() isActive: boolean } ``` 所有的active-record都必須是`extends BaseEntity`,這提供你可以與entity互動的method,以下為互動的範例: ```typescript! // example how to save AR entity const user = new User() user.firstName = "Timber" user.lastName = "Saw" user.isActive = true await user.save() // example how to remove AR entity await user.remove() // example how to load AR entities const users = await User.find({ skip: 2, take: 5 }) const newUsers = await User.findBy({ isActive: true }) const timber = await User.findOneBy({ firstName: "Timber", lastName: "Saw" }) ``` `BaseEntity`有大部分的屬於標準`Repository`的method,所以多數時候你不需要使用`Respository` or `EntityManager`來搭配active record entities。 現在假如我們想建立一個function來回傳Users藉由查找first name 和 last name,可以在User clasee裡面寫個static method。範例如下: ```typescript! import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number @Column() firstName: string @Column() lastName: string @Column() isActive: boolean static findByName(firstName: string, lastName: string) { return this.createQueryBuilder("user") .where("user.firstName = :firstName", { firstName }) .andWhere("user.lastName = :lastName", { lastName }) .getMany() } } ``` 並如下使用這個method: ```typescript! const timber = await User.findByName("Timber", "Saw") ``` #### Data Mapper pattern 使用Data Mapper方式的話,你會將query的method寫在"repositories",可以投過repositories來進行存擋、刪除、載入。 在data mapper模式中,你的entities是非常不靈光的,他就只是定義其properties並擁有一些'dummy' methods. 簡單來說,Data Mapper模式就是讓你透過repositores來與資料庫互動而非models。 以下範例: ```typescript! import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column() firstName: string @Column() lastName: string @Column() isActive: boolean } ``` 與資料庫互動的方式: ```typescript! const userRepository = dataSource.getRepository(User) // example how to save DM entity const user = new User() user.firstName = "Timber" user.lastName = "Saw" user.isActive = true await userRepository.save(user) // example how to remove DM entity await userRepository.remove(user) // example how to load DM entities const users = await userRepository.find({ skip: 2, take: 5 }) const newUsers = await userRepository.findBy({ isActive: true }) const timber = await userRepository.findOneBy({ firstName: "Timber", lastName: "Saw", }) ``` 為了要延伸其method,可以參考這篇如何建立[custom methods](https://typeorm.io/custom-repository#) #### Which one should I choose? 兩者各有其優缺點,只要記住軟體開發關乎的事後續如何維護,Data Mapper方法比較貼近於後續維護,在大型app中會更有效率,而Active Record講求的是讓方式簡潔,所以更適合於小型的app中,And simplicity is always a key to better maintainability(?) ### Installation 1. Install npm package: `npm install typeorm --save` 2. You nedd to install reflect-metadata shim: `npm install relect-metadata --save` 並將其引入到你app內的全域環境下(for example in app.ts) 3. You may need to install node typings: `npm install @types/node --save-dev` 4. Install a database drive: `npm install mysql --save`(you can install mysql2 instead as well) ##### TypeScript configuration 確保你使用的TS版本是在4.5以上,且在`tsconfig.json`啟用下面兩項: ```json! "emitDecoratorMetadata": true, "experimentalDecorators": true, ``` ### Step-by-Step Guide 你對一款ORM有什麼期待?首先,你會期許它能為你建立資料庫並且查詢/寫入/更新/刪除過程中也無需寫讓你頭痛的又難以維護的原始SQL語言,這份指南將展示如何從最基礎開始建立設定TypeORM並且讓你做到你所期待的事。 #### Create a model 與資料庫互動前需要先建立table,如何叫TypeORM建立tables? 答案就是透過模型(models),你app內的模型就是你的資料庫tables。 例如以下,你有個Photo model: ```typescript! export class Photo { id: number name: string description: string filename: string views: number isPublished: boolean } ``` 你想要在資料庫內儲存photos,為儲存這些photos,首先你需要一個資料庫table,且這資料庫table要從你的model所建立。並非所有模型都是,只有那些定義為*entities*的才是。 #### Create a entity Entity就是當一個模型(model)被`@Entity`所注入。一個資料庫table將會依照此models被建立,你可以在TypeORM的任何地方使用entities,你可以查詢/寫入/更新/刪除並用其執行其他動作。 以下依照Photo 模型(model)建立一個entity ```typescript! import { Entity } from "typeorm" @Entity() export class Photo { id: number name: string description: string filename: string views: number isPublished: boolean } ``` 現在一個資料庫table已經被建立,且可以在我們的app內任何地方與其互動,然而沒有columns怎麼算是一個table呢?讓我們為資料庫table建立一些columns。 #### Adding table columns 為加入資料庫columns,你只需要將entity的屬性簡單地加上`@Column`注入器。 ```typescript! 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 } ``` 現在以上的欄位都會被加入photo table,欄位的型別將會由屬性的型別去做推斷,例如 `number`會被轉換成`integer`, `string`為`varchar`, `boolean`為`bool`,但你可以使用任何你資料庫有資源的型別,只要特別在`@Column`注入器特別標注即可。 我們建立了有欄位的資料庫表單,還剩下一個東西就是每個資料庫表單都需要一個欄位帶有主鍵primary key。 #### Creating a primary column ```typescript! import { Entity, Column, PrimaryColumn } from "typeorm" @Entity() export class Photo { @PrimaryColumn() id: number @Column() name: string @Column() description: string @Column() filename: string @Column() views: number @Column() isPublished: boolean } ``` #### Creating an auto-generated column 現在,假如你想要讓你的id欄位可以被自動產生(也被稱為 auto-increment/ sequence / serial / generated identity column),為達到此目的,你需要更改`@PrimaryColumn `注入器為`@PrimaryGeneratedColumn`: ```typescript! 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 接下來,來修正下我們的資料型別,預設上,string會映照為像varchar(255)的type(依據資料庫類型)。number會映照為像integer的type,我們不會想要我們的資料欄位被限制為普通的varchar或是integer,可以做以下設定來修正資料類型: ```typescript! 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 } ``` 欄位型別是為資料庫特定的,你可以設定任何欄位型別只要你的資料庫有支援的話。你可以看[這裡](https://typeorm.io/entities#column-types)來獲得更多關於你資料庫支援的欄位型別資訊。 #### Creating a new DataSource 現在,entity已經被建立,讓我們建立index.ts檔案並且將`DataSource`建立在那邊: ```typescript! import "reflect-metadata" import { DataSource } from "typeorm" import { Photo } from "./entity/Photo" const AppDataSource = new DataSource({ type: "postgres", host: "localhost", port: 5432, username: "root", password: "admin", database: "test", entities: [Photo], synchronize: true, logging: false, }) // to initialize initial connection with the database, register all entities // and "synchronize" database schema, call "initialize()" method of a newly created database // once in your application bootstrap AppDataSource.initialize() .then(() => { // here you can start to work with your database }) .catch((error) => console.log(error)) ``` 範例中使用的是Postgres,但你可以使用任一有支援的資料庫,只要更換`type`內的選項為你要使用的資料庫即可。 我們將Photo entit加入到此data source的entities陣列中,你必須將任每個你要使用的Entity都列在此處。 設定同步化(synchronize)為true確保你的entity在每次連線時都可以更新同步。 ```sql! +-------------+--------------+----------------------------+ | photo | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | name | varchar(100) | | | description | text | | | filename | varchar(255) | | | views | int(11) | | | isPublished | boolean | | +-------------+--------------+----------------------------+ ``` #### Creating and inserting a photo into the database 現在讓我們建立一張新照片並且將其存到資料庫中: ```typescript! import { Photo } from "./entity/Photo" import { AppDataSource } from "./index" const 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 AppDataSource.manager.save(photo) console.log("Photo has been saved. Photo id is", photo.id) ``` 當你的entity被儲存後你會得到一個新產生的id,`save`方法會回傳被傳入的同一個物件的實例,並非為此物件的copy,他會修改其`id`並且回傳。 #### Using Entity Manager 我們剛建立了新照片並將其存到資料庫。我們使用`EntityManager`來儲存它。你可以在app內操控任何entity藉由使用entity manager,例如: ```typescript! import { Photo } from "./entity/Photo" import { AppDataSource } from "./index" const savedPhotos = await AppDataSource.manager.find(Photo) console.log("All photos from the db: ", savedPhotos) ``` `savePhotos`會是一組由資料庫仔入的Photo物件的陣列 #### Using Repositories 現在讓我們重構並使用repository的方式,每個entity都有屬於自己的repository是用來進行處理所有跟entity有關的動作。使用repositories會比EntityManager來得方便當你要處理許多與entities相關的事情。 ```typescript! import { Photo } from "./entity/Photo" import { AppDataSource } from "./index" const 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 const photoRepository = AppDataSource.getRepository(Photo) await photoRepository.save(photo) console.log("Photo has been saved") const savedPhotos = await photoRepository.find() console.log("All photos from the db: ", savedPhotos) ``` #### Loading from the database 以下試著藉由repository來進行查詢的動作: ```typescript! import { Photo } from "./entity/Photo" import { AppDataSource } from "./index" const photoRepository = AppDataSource.getRepository(Photo) const allPhotos = await photoRepository.find() console.log("All photos from the db: ", allPhotos) const firstPhoto = await photoRepository.findOneBy({ id: 1, }) console.log("First photo from the db: ", firstPhoto) const meAndBearsPhoto = await photoRepository.findOneBy({ name: "Me and Bears", }) console.log("Me and Bears photo from the db: ", meAndBearsPhoto) const allViewedPhotos = await photoRepository.findBy({ views: 1 }) console.log("All viewed photos: ", allViewedPhotos) const allPublishedPhotos = await photoRepository.findBy({ isPublished: true }) console.log("All published photos: ", allPublishedPhotos) const [photos, photosCount] = await photoRepository.findAndCount() console.log("All photos: ", photos) console.log("Photos count: ", photosCount) ``` #### Updating in the database 以下先查詢後進行更新並且保存: ```typescript! import { Photo } from "./entity/Photo" import { AppDataSource } from "./index" const photoRepository = AppDataSource.getRepository(Photo) const photoToUpdate = await photoRepository.findOneBy({ id: 1, }) photoToUpdate.name = "Me, my friends and polar bears" await photoRepository.save(photoToUpdate) ``` 現在id為1的photo將會被更新並存到資料庫。 #### Removing from the database 以下進行刪除: ```typescript! import { Photo } from "./entity/Photo" import { AppDataSource } from "./index" const photoRepository = AppDataSource.getRepository(Photo) const photoToRemove = await photoRepository.findOneBy({ id: 1, }) await photoRepository.remove(photoToRemove) ``` 現在id為1的photo將會被刪除。 #### Creating a one-to-one relation 讓我們與另外一個class建立一對一關係,建立一個新的class名為PhotoMetadata。PhotoMetada會涵蓋photo的額外資訊: ```typescript! 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(() => Photo) @JoinColumn() photo: Photo } ``` 這裡我們使用了一個新的注入器名為`@OneToOne`,這讓我們建立兩個entities之間一對一的關係。 `type => Photo`會回傳欲與其建立關係的entity的class。因為語言的關係我們使用fucntion來回傳class而並非直接使用class。另外此處寫成`type => Photo`而非是`() => Photo`目的為增加易讀性,這個type變數本身不涵蓋任何東西。 這裡也使用到了`@JoinColumn`注入器,意思就是此處的class將會'擁有'此關係,關係可以是單向或是雙向的,只有單向的關係才能'擁有',在主關係處必需要使用`@JoinColumn`。 現在運行app,你會看到一個新被建立的table,也會包含帶有外鍵的photo column。 #### Save a one-to-one relation 現在儲存一photo還有其metadata並與其做連結: ```typescript! import { Photo } from "./entity/Photo" import { PhotoMetadata } from "./entity/PhotoMetadata" // create a photo const 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 // create a photo metadata const metadata = new PhotoMetadata() metadata.height = 640 metadata.width = 480 metadata.compressed = true metadata.comment = "cybershoot" metadata.orientation = "portrait" metadata.photo = photo // this way we connect them // get entity repositories const photoRepository = AppDataSource.getRepository(Photo) const metadataRepository = AppDataSource.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 the relation between metadata and photo is created in the database too", ) ``` #### Inverse side of the relationship 關係可以是單向或雙向的,現在Photo和PhotoMetadata的關係是雙向的。 主關係為PhotoMetadata,Photo並不知道關於任何PhotoMetada的資訊,如果要從Photo端取得PhotoMetada的資訊將會變得相當複雜,為解決此問題,我們要加入逆關係(inverse relation),讓Photo和PhotoMetadata為雙向關係,以下修改entities: ```typescript! import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, } from "typeorm" import { Photo } from "./Photo" @Entity() export class PhotoMetadata { /* ... other columns */ @OneToOne(() => Photo, (photo) => photo.metadata) @JoinColumn() photo: Photo } ``` ```typescript! import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from "typeorm" import { PhotoMetadata } from "./PhotoMetadata" @Entity() export class Photo { /* ... other columns */ @OneToOne(() => PhotoMetadata, (photoMetadata) => photoMetadata.photo) metadata: PhotoMetadata } ``` `photo => photo.metadata`會回傳另外一方關係的名稱,這裡展示了在Photo class內的metadata屬性就是我們儲存PhotoMetadata的地方,你可以相對地簡單傳入一個string到`@OneToOne`注入器,像是`metadata`,而非傳入一個會回傳photo屬性的function,但我們使用這種function的方式來讓我們的重構相對簡易。 請特別注意到我們只應該在主關係這端使用`@JoinColumn`注入器,將此注入器放在任何一端都會使其成為主關係,主關係這端會在資料庫中涵蓋一個帶有外鍵的column。 ####v Relations in ESM projects 如果你的TypeScript專案中使用的是ESM,你應該要在關係的屬性中使用`Relation` wrapper形式來避免circular dependency issues。 ```typescript! import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, Relation, } from "typeorm" import { Photo } from "./Photo" @Entity() export class PhotoMetadata { /* ... other columns */ @OneToOne(() => Photo, (photo) => photo.metadata) @JoinColumn() photo: Relation<Photo> } ``` ```typescript! import { Entity, Column, PrimaryGeneratedColumn, OneToOne, Relation, } from "typeorm" import { PhotoMetadata } from "./PhotoMetadata" @Entity() export class Photo { /* ... other columns */ @OneToOne(() => PhotoMetadata, (photoMetadata) => photoMetadata.photo) metadata: Relation<PhotoMetadata> } ``` #### Loading objects with their relations 現在讓我們來讀取photo以及photo metadata在單一個query中,有兩個方式能夠做到-使用`find*`方法-或是使用QueryBuilder的方式,以下先看`find*`的方式,`find*`的方式讓你可以指定一個object是FindOneOptions / FindManyOptions 的介面。 ```typescript! import { Photo } from "./entity/Photo" import { PhotoMetadata } from "./entity/PhotoMetadata" import { AppDataSource } from "./index" const photoRepository = AppDataSource.getRepository(Photo) const photos = await photoRepository.find({ relations: { metadata: true, }, }) ``` 這裡photos會是一組從資料庫傳來的photos陣列,且每個photo都會包含其photo metadata,你可以在[這裡](https://typeorm.io/find-options#)看到更多關於Find Options的內容。 使用find的方式很好又簡單,但如果你需要較複雜的query方式,你需要使用`QueryBuilder`,`QueryBuilder`讓你可以以更優雅的方式執行更複雜的quires: ```typescript! import { Photo } from "./entity/Photo" import { PhotoMetadata } from "./entity/PhotoMetadata" import { AppDataSource } from "./index" const photos = await AppDataSource.getRepository(Photo) .createQueryBuilder("photo") .innerJoinAndSelect("photo.metadata", "metadata") .getMany() ``` `QueryBuilder`可以讓你建立及執行幾乎任何複雜程度的SQL語法,當你在用`QueryBuilder`時就當作你在寫SQL query,在範例中,'photo'和'metadata'會是被選中photos屬性別稱,你可以使用這些別稱來存取資料。 #### Using cascades to automatically save related objects 可以在選項中設定`cascade`,當我們想要同時保存物件以及關聯物件時,在photo注入器中加上`cascade`: ```typescript! export class Photo { /// ... other columns @OneToOne(() => PhotoMetadata, (metadata) => metadata.photo, { cascade: true, }) metadata: PhotoMetadata } ``` 使用`cascade`不用在分開保存photo和metadata,只要保存photo物件,metadata也會自動被保存因為`cascade`選項的關係。 ```typescript! import { AppDataSource } from "./index" // create photo object const 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 const metadata = new PhotoMetadata() metadata.height = 640 metadata.width = 480 metadata.compressed = true metadata.comment = "cybershoot" metadata.orientation = "portrait" photo.metadata = metadata // this way we connect them // get repository const photoRepository = AppDataSource.getRepository(Photo) // saving a photo also save the metadata await photoRepository.save(photo) console.log("Photo is saved, photo metadata is saved too.") ``` 請特別注意到這裡寫的是`photo.metadata = metadata`而非`metadata.photo = photo`,`cascade`只會在你從photo這邊將metadata做連結時才會有作用,因為`cascade`寫在photo class那裡。 #### Creating a many-to-one / one-to-many relation 接下來建立一組一對多、多對一關係,photo會有一個author,而每個author會有許多photos: ```typescript! import { Entity, Column, PrimaryGeneratedColumn, OneToMany, JoinColumn, } from "typeorm" import { Photo } from "./Photo" @Entity() export class Author { @PrimaryGeneratedColumn() id: number @Column() name: string @OneToMany(() => Photo, (photo) => photo.author) // note: we will create author property in the Photo class below photos: Photo[] } ``` Author是反向關係,`OneToMany`關係都是反向關係且一定要有`ManyToOne`搭配。 現在來將主關係加入到Photo entity: ```typescript! import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from "typeorm" import { PhotoMetadata } from "./PhotoMetadata" import { Author } from "./Author" @Entity() export class Photo { /* ... other columns */ @ManyToOne(() => Author, (author) => author.photos) author: Author } ``` 在一對多/多對一關係中,多對一的那方都會是主關係,這代表使用多對一的那方會將相關對象的id儲存起來。 運行app後,ORM會建立以下的table: ```sql! +-------------+--------------+----------------------------+ | author | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | name | varchar(255) | | +-------------+--------------+----------------------------+ ``` 也會對photo table做修正,加入一個新的author column附帶外鍵: ```sql! +-------------+--------------+----------------------------+ | photo | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | name | varchar(255) | | | description | varchar(255) | | | filename | varchar(255) | | | isPublished | boolean | | | authorId | int(11) | FOREIGN KEY | +-------------+--------------+----------------------------+ ``` #### Creating a many-to-many relation 來建立多對多的關係,photo可以在許多albums內,albums也可以有許多photos: ```typescript! import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, } from "typeorm" @Entity() export class Album { @PrimaryGeneratedColumn() id: number @Column() name: string @ManyToMany(() => Photo, (photo) => photo.albums) @JoinTable() photos: Photo[] } ``` `@JoinTable`會被加入在主關係那方 現在在反向關係那方也加入: ```typescript! export class Photo { /// ... other columns @ManyToMany(() => Album, (album) => album.photos) albums: Album[] } ``` 運行app後,ORM會建立一個album_photos_photo_albums連結table: ```typescript! +-------------+--------------+----------------------------+ | album_photos_photo_albums | +-------------+--------------+----------------------------+ | album_id | int(11) | PRIMARY KEY FOREIGN KEY | | photo_id | int(11) | PRIMARY KEY FOREIGN KEY | +-------------+--------------+----------------------------+ ``` 也別忘了將Album class加入到ORM的entities中: ```typescript! const options: DataSourceOptions = { // ... other options entities: [Photo, PhotoMetadata, Author, Album], } ``` 現在來將albums和photos加入到資料庫中: ```typescript! import { AppDataSource } from "./index" // create a few albums const album1 = new Album() album1.name = "Bears" await AppDataSource.manager.save(album1) const album2 = new Album() album2.name = "Me" await AppDataSource.manager.save(album2) // create a few photos const 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 photo.albums = [album1, album2] await AppDataSource.manager.save(photo) // now our photo is saved and albums are attached to it // now lets load them: const loadedPhoto = await AppDataSource.getRepository(Photo).findOne({ where: { id: 1, }, relations: { albums: true, }, }) ``` loadedPhoto會是下面: ```typescript! { 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" }] } ``` #### Using QueryBuilder 你可以使用QueryBuilde構建出任何複雜度的SQL.例如: ```typescript! const photos = await AppDataSource.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() ``` 此查詢選擇所有 published 的 name 等於"My"或"Mishka"的 photos。它將從結果中的第 5 個(分頁偏移)開始,並且僅選擇 10 個結果(分頁限制)。得到的結果將按 ID 降序排序。photo 的 albums 將被 left-joined,其元數據將被 inner joined。 由於 QueryBuilder 的自由度更高,因此在專案中可能會大量的使用它。[這裡](https://typeorm.io/select-query-builder#)有更多關於 QueryBuilder 的信息 ## DataSource ### Working with DataSource #### Waht is DataSource 當你建立好DataSouce的設定後才能與資料庫進行互動,TypeORM的DataSource乘載了資料庫的連線設定並且提供針對你使用的資料庫的初始化。 為建立初始連接,你必須調用DataSource實例的intialize方法,destroy方法用來斷開連線。 一般來說,你會在app啟用時調用intialize方法,並且在結束使用資料庫時調用detroy,實務上,如果你建立的是一個後端應用程式通常都會保持運行,並不會detroy DataSource。 #### Creating a new DataSource 為建立一個新的DataSource實例你需要呼叫`new DataSource`來初始這個建構子並將其放在全域變數中以便在整個app中使用: ```typescript! import { DataSource } from "typeorm" const AppDataSource = new DataSource({ type: "mysql", host: "localhost", port: 3306, username: "test", password: "test", database: "test", }) AppDataSource.initialize() .then(() => { console.log("Data Source has been initialized!") }) .catch((err) => { console.error("Error during Data Source initialization", err) }) ``` 將AppDataSource藉由export方式能在全域使用是個好方式因為你通常會在app中使用實例的方式。 DataSource可以有DataSourceOptions以及那些根據不同資料庫類別有不同的option。 你可以根據你需要的定義多個不同的data resource,例如: ```typescript! import { DataSource } from "typeorm" const MysqlDataSource = new DataSource({ type: "mysql", host: "localhost", port: 3306, username: "test", password: "test", database: "test", entities: [ // .... ], }) const PostgresDataSource = new DataSource({ type: "postgres", host: "localhost", port: 5432, username: "test", password: "test", database: "test", entities: [ // .... ], }) ``` #### How to use DataSource 當你設定好你的DataSource後,你可以在任何地方使用,例如: ```typescript! import { AppDataSource } from "./app-data-source" import { User } from "../entity/User" export class UserController { @Get("/users") getAll() { return AppDataSource.manager.find(User) } } ``` 使用DataSource實例你就可以用`.manager`和`.getRepository()`方式來對entities來對資料庫進行操作。 ### Data Source Options #### What is DataSourceOptions DataSource是一組設定配置當你建立新的DataSource實例時可以傳入,不同的資料庫也有他們自己的配置設定可以傳入。 #### Common data source options * type - 資料庫類別,你必須要填入你是使用哪種資料庫,這項是必填的。 Possible values are: "mysql", "postgres", "cockroachdb", "sap", "spanner", "mariadb", "sqlite", "cordova", "react-native", "nativescript", "sqljs", "oracle", "mssql", "mongodb", "aurora-mysql", "aurora-postgres", "expo", "better-sqlite3", "capacitor". * extra - 要傳遞給底層驅動程序的額外連接選項。如果要將額外設置傳遞給基礎數據庫驅動程序,請使用此配置。 * entities - 用以載入以及使用entitis以及Entity Schema,接受entity class以及entity schema classes和目錄路徑,例如` entities: [Post, Category, "entity/*.js", "modules/**/entity/*.js"]`。 * subscribers - 加載並用於此連接的訂閱者。接受要加載的實體類和目錄,例如:`subscribers: [PostSubscriber, AppSubscriber, "subscriber/*.js", "modules/**/subscriber/*.js"]`。 * migrations - 加載並用於此連接的migration,接受要加載的實體類和目錄,例如:` migrations: [FirstMigration, SecondMigration, "migration/*.js", "modules/**/migration/*.js"]`。 * logging - 指定是否指定是否啟用日誌記錄,如果為true則query和錯誤紀錄會被啟用,你可以特別指定要記錄的種類,例如:`["query", "error", "schema"]` * logger - 記錄器用以記錄為目的。可能的值為`"advanced-console", "simple-console" and "file"`,預設為`"advanced-console".` * maxQueryExecutionTime - 如果執行的query時間大於此值,logger就會將此query log出來。 * poolSize - 設定連線資料庫的最大數量 * namingStrategy - 用來設定設定tabled以及columns的命名策略。 * entityPrefix - 將所有table加上前綴 * entitySkipConstructor - 指示TypeORM是否要跳過建構子當從資料庫反序列化entities時,請特別注意當你不呼叫建構子時私有屬性(private properties)以及預設屬性(default properties)可能會不如預期般作業。 * dropSchema - 當每次data source被初始化時會將schema卸除,請注意別在正式環境中使用此選項,但這選項在debug以及開發環境中很有用。 * syzhronize - 指示是否每次app啟用時自動建立資料庫架構,請注意這個選項以及不要把這個用在正式產品上,否則你有可能會遺失資料,這選項在debug以及開發階段很有用,替代選項為你可以使用CLI指令跑schema:sync。請註記MongoDB資料庫並不會建立架構因為MongoDB是屬於無架構的資料庫,取而代之的是會建立索引。 * migrationRun - 指示是否每次app啟用時自動跑migration,替代方式為使用CLI指令跑migration: run。 * migrationsTableName - 包含有關執行migrations資訊的table名稱,預設為"migrations"。 * migrationsTransactionMode - 控制migrations的交易模式。可以為all | none | each * metadataTableName - 包含有關table metadata的table名稱,預設為"typeorm_metadata" * cache - 啟用entity結果的快取,可以參考[這裡](https://typeorm.io/caching#)有更多關於cache的資訊。 * cli.entitiesDir - 在CLI預設狀況下建立entities的路徑 * cli.subscribersDir - 在CLI預設狀況下建立subscribers的路徑 ### DataSource API * options - Options用來建立這個dataSource `const dataSourceOptions: DataSourceOptions = dataSource.options ` * isInitialized - 指示DataSource是否已經被初始化以及與資料庫的連線是否被建立。 `const isInitialized: boolean = dataSource.isInitialized` * driver - 此dataSource中使用的資料庫驅動 `const driver: Driver = dataSource.driver` * manager - 用來跟entities互動的EntityManager ```typescript! const manager: EntityManager = dataSource.manager // you can call manager methods, for example find: const users = await manager.find() ``` * mongoManager - 用來跟mongodb entities互動的MongoEntityManager ```typescript! const manager: MongoEntityManager = dataSource.mongoManager // you can call manager or mongodb-manager specific methods, for example find: const users = await manager.find() ``` * initialize - 初始化data source以及打開資料庫的連接池 `await dataSource.initialize()` * destroy - 摧毀DataSource並關閉所有資料庫的連線。通常用在整個app要關閉時。 `await dataSource.destroy()` * synchorize - 同步資料庫的架構。當在data source內有設定synchronize: true時就等同於呼叫此method。通常是在app開始運作時呼叫此method。 `await dataSource.synchronize()` * dropDatabase - 丟棄資料庫以及其資料。 `await dataSource.dropDatabase()` * runMigrations - 運行所有等待中的migrations `await dataSource.runMigrations()` * undoLastMigrations - 回復上一個執行的migration `await dataSource.undoLastMigration()` * hasMetadata - 確認此Entity的metadata已經被註冊。 ```typescript! if (dataSource.hasMetadata(User)) const userMetadata = dataSource.getMetadata(User) ``` * getMetadata - 獲得此Entity的metadata。 ```typescript! const userMetadata = dataSource.getMetadata(User) // now you can get any information about User entity ``` * getRepository - 獲得此entity的repository。 ```typescript! const repository = dataSource.getRepository(User) // now you can call repository methods, for example find: const users = await repository.find() ``` * getTreeRepository - 獲得此entity的TreeRepository ```typescript! const repository = dataSource.getTreeRepository(Category) // now you can call tree repository methods, for example findTrees: const categories = await repository.findTrees() ``` * getMongoRepository - 獲得此entity的MongoRepository ```typescript! const repository = dataSource.getMongoRepository(User) // now you can call mongodb-specific repository methods, for example createEntityCursor: const categoryCursor = repository.createEntityCursor() const category1 = await categoryCursor.next() const category2 = await categoryCursor.next() ``` * transaction - 在一個資料庫機要中執行多個資料庫請求下提供一個單一交易環境。 ```typescript! await dataSource.transaction(async (manager) => { // NOTE: you must perform all database operations using given manager instance // its a special instance of EntityManager working with this transaction // and don't forget to await things here }) ``` * query - 執行原生SQL query `const rawData = await dataSource.query(`SELECT * FROM USERS`)` * createQueryBuilder - 建立一個query builder ```typescript! const users = await dataSource .createQueryBuilder() .select() .from(User, "user") .where("user.name = :name", { name: "John" }) .getMany() ``` * createQueryRunner - 建立一個query runner用以管理及互動單一個真實的資料庫dataSoruce。 ```typescript! const queryRunner = dataSource.createQueryRunner() // you can use its methods only after you call connect // which performs real database connection await queryRunner.connect() // .. now you can work with query runner and call its methods // very important - don't forget to release query runner once you finished working with it await queryRunner.release() ``` ## Entity ### What is Entity Entity就是一個class映照到資料庫的table(或是MongoDB的collection),你可以建立一個entity藉由定義一個新的class並用@Entity()注入器注入: ```typescript! import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column() firstName: string @Column() lastName: string @Column() isActive: boolean } ``` 會建立以下table: ```sql! +-------------+--------------+----------------------------+ | user | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | firstName | varchar(255) | | | lastName | varchar(255) | | | isActive | boolean | | +-------------+--------------+----------------------------+ ``` 基本的entities包含了欄位以及關係。每個entity都要有primary欄位(或是MongoDB中的ObjectId)。 每個entity都要註冊到data source中: ```typescript! import { DataSource } from "typeorm" import { User } from "./entity/User" const myDataSource = new DataSource({ type: "mysql", host: "localhost", port: 3306, username: "test", password: "test", database: "test", entities: [User], }) ``` 或你可以指定整個資料夾,所有裡面的檔案都會被載入: ```typescript! import { DataSource } from "typeorm" const dataSource = new DataSource({ type: "mysql", host: "localhost", port: 3306, username: "test", password: "test", database: "test", entities: ["entity/*.js"], }) ``` 如果你想使用替代的table name你可以特別註明`@Entity: @Entity("my_users")`,如果你想為每個table都設定前綴名,你可以在data soruce中的選項中設定`entityPrefix`。 當使用entity建構子時其中的參數必須為optional,因為ORM會在從資料庫載入時建立entity class的實例,因此它並不會知道你的建構子參數為合。 #### Entity columns 因為資料庫表格包含欄位,所以你的entities一定要有欄位。在Entity class裡面用@Column標注的都會映照成資料庫的表格欄位 #### Primary columns 每個entity中至少都會有一個primary欄位,有好幾種表達primary欄位的形式: * `@PrimaruColumn()` 建立一個primary欄位可以為任何value以及任何type,你可以主動設定欄位的type,如果沒有特別設定則會就property來自動推斷type,以下範例建立id為primary欄位: ```typescript! import { Entity, PrimaryColumn } from "typeorm" @Entity() export class User { @PrimaryColumn() id: number } ``` * `@PrimaryGeneratedColumn()` 建立一個primary欄位而其值會藉由一個自動增加的值自動產生,它會藉由auto-increment/serial/sequence/identity產生int欄位(根據資料庫或是提供的設定檔)。您不需在儲存前手動新增。 ```typescript! import { Entity, PrimaryGeneratedColumn } from "typeorm" @Entity() export class User { @PrimaryGeneratedColumn() id: number } ``` * @PrimaryGeneratedColumn("uuid") 建立一個primary欄位而其值會藉由uuid自動產生,uuid是一組獨特的字串id,您不需在儲存前手動新增。 ```typescript! import { Entity, PrimaryGeneratedColumn } from "typeorm" @Entity() export class User { @PrimaryGeneratedColumn("uuid") id: string } ``` 你可以使用複數的primary欄位: ```typescript! import { Entity, PrimaryColumn } from "typeorm" @Entity() export class User { @PrimaryColumn() firstName: string @PrimaryColumn() lastName: string } ``` 當你使用save儲存entities時,它總是會先到資料庫藉由給予的id查找,如果有找到就會更新其列,如果沒有一筆新資料就會被插入。 為藉由id來找到entity你可以使用manager.findOneBy or repository.findOneBy,範例: ```typescript! // find one by id with single primary key const person = await dataSource.manager.findOneBy(Person, { id: 1 }) const person = await dataSource.getRepository(Person).findOneBy({ id: 1 }) // find one by id with composite primary keys const user = await dataSource.manager.findOneBy(User, { firstName: "Timber", lastName: "Saw", }) const user = await dataSource.getRepository(User).findOneBy({ firstName: "Timber", lastName: "Saw", }) ``` #### Special columns 有一些特殊欄位型別帶有額外的功能: * @CreateDateColumn 一特殊欄位會自動設定插入entity資料的日期。 * @UpdateDateColumn 一特殊欄位會自動設定每次更新儲存到entity&repository資料的日期。 * @DeleteDateColumn 一特殊欄位會自動設定執行軟刪除entity&repository資料的日期。如果設定了@DeleteDateColumn,預設將為不刪除(non-deleted)。 * @VersionColumn 一特殊欄位會自動設定版本號碼(自動增加的數字)當每次對entity&repository進行儲存時。 #### Column Types TypeORM 支援所有最常用的資料庫支援的欄位類型。 欄位類型是資料庫別有的 - 這位資料庫架構提供了最大的彈性。 你可以把欄位類型放在@Column的第一個參數或是在@Column的欄位選項中,例如: ```typescript! @Column("int") ``` or ```typescript! @Column({ type: "int" }) ``` 如果你想要指定額外的參數類型你也可以加在欄位選項中,例如: ```typescript! @Column("varchar", { length: 200 }) ``` or ```typescript! @Column({ type: "int", width: 200 }) ``` 關於 bigint 類型的注意事項:在 SQL 數據庫中使用的 bigint 列類型不適合常規數字類型,而是將屬性映射到字符串。 #### Column types for mysql / mariadb `bit, int, integer, tinyint, smallint, mediumint, bigint, float, double, double precision, dec, decimal, numeric, fixed, bool, boolean, date, datetime, timestamp, time, year, char, nchar, national char, varchar, nvarchar, national varchar, text, tinytext, mediumtext, blob, longtext, tinyblob, mediumblob, longblob, enum, set, json, binary, varbinary, geometry, point, linestring, polygon, multipoint, multilinestring, multipolygon, geometrycollection` #### enum column type postgres和mysql有支援enum欄位類型,有多種不同的定義欄位的方式: 使用Typescript ENUM: ```typescript! export enum UserRole { ADMIN = "admin", EDITOR = "editor", GHOST = "ghost", } @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column({ type: "enum", enum: UserRole, default: UserRole.GHOST, }) role: UserRole } ``` Note: String, numeric and heterogeneous enums are supported. 使用array的enum型別: ```typescript! export type UserRoleType = "admin" | "editor" | "ghost", @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column({ type: "enum", enum: ["admin", "editor", "ghost"], default: "ghost" }) role: UserRoleType } ``` #### set column type mariadb和mysql有支援set欄位類型,有多種不同的定義欄位的方式: 使用Typescript enums: ```typescript! export enum UserRole { ADMIN = "admin", EDITOR = "editor", GHOST = "ghost", } @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column({ type: "set", enum: UserRole, default: [UserRole.GHOST, UserRole.EDITOR], }) roles: UserRole[] } ``` 使用array搭配set類型: ```typescript! export type UserRoleType = "admin" | "editor" | "ghost", @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column({ type: "set", enum: ["admin", "editor", "ghost"], default: ["ghost", "editor"] }) roles: UserRoleType[] } ``` ## Relations ### What are Relations 關聯幫助你與其他entities更簡單的互動,有許多不同的關聯類型: * one-to-one using @OneToOne * many-to-one using @ManyToOne * one-to-many using @OneToMany * many-to-many using @ManyToMany #### Relation options * eager: boolean - 如果設為true,關係中的主關係永遠都會在載入在使用find* method或是使用在此entity上使用QueryBuilder。 * cascade: boolean | ("insert" | "update")[] - 如果設為true,關聯的物件會被插入以及更新,你也可以特別指令一組cascade選項的array * onDelete: "RESTRICT"|"CASCADE"|"SET NULL" - 指示外鍵該如何動作當關聯物件被刪除時。 * nullable: boolean - 指示這個關聯欄位是否可以為null值,預設為可以。 * orphanedRowAction: "nullify" | "delete" | "soft-delete" | disable - 當主關係儲存時從關係並不存在時,這選項會控制會如何影響他們,delete將會刪除資料庫的從關係資料,soft-delete將會被註記,nullify會移除關係,disable會保持關係,要執行刪除就必須使用他們自己的repository #### Cascades 範例: ```typescript! import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm" import { Question } from "./Question" @Entity() export class Category { @PrimaryGeneratedColumn() id: number @Column() name: string @ManyToMany((type) => Question, (question) => question.categories) questions: Question[] } ``` ```typescript! import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, } from "typeorm" import { Category } from "./Category" @Entity() export class Question { @PrimaryGeneratedColumn() id: number @Column() title: string @Column() text: string @ManyToMany((type) => Category, (category) => category.questions, { cascade: true, }) @JoinTable() categories: Category[] } ``` ```typescript! const category1 = new Category() category1.name = "ORMs" const category2 = new Category() category2.name = "Programming" const question = new Question() question.title = "How to ask questions?" question.text = "Where can I ask TypeORM-related questions?" question.categories = [category1, category2] await dataSource.manager.save(question) ``` 如這範例中我們並沒有幫category2和category2呼叫save method,他們會被自動存進資料庫因為cascade選項為true。 請注意,cascades看起來是個與關係互動的好方式,但也同時帶來bug和安全疑慮當有可能會有不想被存到資料庫的東西被存入。並且這方式也降低了一點操作上的辨識度。 #### Cascade Options cascade選項可以為boolean或是一組array包含這些選項 ("insert" | "update" | "remove" | "soft-remove" | "recover")[],預設為false,若設定為true則會開起全部的cascades,你也可以設定特定的選項藉由提供array。 例如: ```typescript! @Entity(Post) export class Post { @PrimaryGeneratedColumn() id: number @Column() title: string @Column() text: string // Full cascades on categories. @ManyToMany((type) => PostCategory, { cascade: true, }) @JoinTable() categories: PostCategory[] // Cascade insert here means if there is a new PostDetails instance set // on this relation, it will be inserted automatically to the db when you save this Post entity @ManyToMany((type) => PostDetails, (details) => details.posts, { cascade: ["insert"], }) @JoinTable() details: PostDetails[] // Cascade update here means if there are changes to an existing PostImage, it // will be updated automatically to the db when you save this Post entity @ManyToMany((type) => PostImage, (image) => image.posts, { cascade: ["update"], }) @JoinTable() images: PostImage[] // Cascade insert & update here means if there are new PostInformation instances // or an update to an existing one, they will be automatically inserted or updated // when you save this Post entity @ManyToMany((type) => PostInformation, (information) => information.posts, { cascade: ["insert", "update"], }) @JoinTable() informations: PostInformation[] } ``` #### @JoinColumn options @JoinColumn不只定義了關係的某邊會帶有外鍵也讓你客製化join column的名稱或是referenced column name. 當我們設定了@JoinColumn,他會在資料庫自動建立一組欄位名稱為 propertyName + referencedColumnName。例如: ```typescript! @ManyToOne(type => Category) @JoinColumn() // this decorator is optional for @ManyToOne, but required for @OneToOne category: Category; ``` 這組code會在資料庫中建立欄位`categoryId`如果你想換不同的名稱可以如下: ```typescript! @ManyToOne(type => Category) @JoinColumn({ name: "cat_id" }) category: Category; ``` Join column總是會參考到其他的欄位,預設上是參考到關聯entity的primary欄位,如果你想跟其他欄位做關聯你也可以在@JoinColumn中設定: ```typescript! @ManyToOne(type => Category) @JoinColumn({ referencedColumnName: "name" }) category: Category; ``` 現在這關聯到的事Category entity的name欄位而不是id。此關係的欄位名稱將會是categoryName。 你也可以關聯多個欄位,請注意這裡並沒有關聯預設中的主欄位,你必須要提供關聯欄位名稱。 ```typescript! @ManyToOne(type => Category) @JoinColumn([ { name: "category_id", referencedColumnName: "id" }, { name: "locale_id", referencedColumnName: "locale_id" } ]) category: Category; ``` #### @JoinTable options @JoinTable適用在多對多關聯上來描述集合表的欄位,集合表是TypeORM建立的一個特殊的單獨表,其內容包含了關聯entities的欄位,你可以更改集合表內欄位的名稱以及他們要關聯的欄位藉由@JoinColumn:你也可以更改集合表的名稱 ```typescript! @ManyToMany(type => Category) @JoinTable({ name: "question_categories", // table name for the junction table of this relation joinColumn: { name: "question", referencedColumnName: "id" }, inverseJoinColumn: { name: "category", referencedColumnName: "id" } }) categories: Category[]; ``` ### One-to-one relations 一對一關係就是A只包含了一個B實例,B只包含了一個A實例,以下用User和Profile做示範,一個User只會有一個Profile,一個Profile也只會被一個User所擁有。 ```typescript! import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class Profile { @PrimaryGeneratedColumn() id: number @Column() gender: string @Column() photo: string } ``` ```typescript! import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, } from "typeorm" import { Profile } from "./Profile" @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column() name: string @OneToOne(() => Profile) @JoinColumn() profile: Profile } ``` 這裡加入@OneToOne指定到profile並且關聯型別為Profile,也把@JoinColumn加入這項是必加入且只能放在關聯中的一邊,你放@JoinColumn的那一邊,那一天的表格會新增一個關聯id的欄位: ```sql! +-------------+--------------+----------------------------+ | profile | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | gender | varchar(255) | | | photo | varchar(255) | | +-------------+--------------+----------------------------+ +-------------+--------------+----------------------------+ | user | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | name | varchar(255) | | | profileId | int(11) | FOREIGN KEY | +-------------+--------------+----------------------------+ ``` 再一次,@JoinColumn必須只能放在關係中的一邊,且那邊在資料庫中必須會帶有一組外鍵。 以下示範如何在使關係中存入資料: ```typescript! const profile = new Profile() profile.gender = "male" profile.photo = "me.jpg" await dataSource.manager.save(profile) const user = new User() user.name = "Joe Smith" user.profile = profile await dataSource.manager.save(user) ``` 如果cascades選項有設為true,則只需要呼叫一次save就好 為載入User帶有profile的資料,你需要在FindOptions中寫relations: ```typescript! const users = await dataSource.getRepository(User).find({ relations: { profile: true, }, }) ``` 或是使用QueryBuilder並用join ```typescript! const users = await dataSource .getRepository(User) .createQueryBuilder("user") .leftJoinAndSelect("user.profile", "profile") .getMany() ``` 如果eager loading選項有被啟用,你不需要特別寫relations因為他永遠都會被自動載入,如果你使用的是QueryBuilder則eager loading就會被關係,則需要使用leftJoinAndSelect來取得關聯資料。 關係可以為單向或是雙向的,單向就是只有一邊有關係注入器,則雙向是兩邊都有注入器。以下範例: ```typescript! import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from "typeorm" import { User } from "./User" @Entity() export class Profile { @PrimaryGeneratedColumn() id: number @Column() gender: string @Column() photo: string @OneToOne(() => User, (user) => user.profile) // specify inverse side as a second parameter user: User } ``` ```typescript! import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, } from "typeorm" import { Profile } from "./Profile" @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column() name: string @OneToOne(() => Profile, (profile) => profile.user) // specify inverse side as a second parameter @JoinColumn() profile: Profile } ``` 我們就建立的雙向的關係,請注意從關係並不會有@JoinColumn,@JoinColumn只會在關係中的一邊帶有從關係外鍵的那邊。 雙向關係也讓你可以在兩邊都使用QueryBuilder ```typescript! const profiles = await dataSource .getRepository(Profile) .createQueryBuilder("profile") .leftJoinAndSelect("profile.user", "user") .getMany() ``` ### Many-to-one / one-to-many relations Many-to-one / one-to-many就是A包含了多個B的實例但是B只會包含一個A的實例,以下用User和Photo來做示範,User可以有好幾張photos,但每一張photo就只會被一個User所擁有。 ```typescript! import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm" import { User } from "./User" @Entity() export class Photo { @PrimaryGeneratedColumn() id: number @Column() url: string @ManyToOne(() => User, (user) => user.photos) user: User } ``` ```typescript! import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm" import { Photo } from "./Photo" @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column() name: string @OneToMany(() => Photo, (photo) => photo.user) photos: Photo[] } ``` 這裡在photos屬性上加入了@OneToMany注入器並註記關聯類別為Photo,在@ManyToOne / @OneToMany 關係中你可以忽略@JoinColumn,@OneToMany一定要搭配@ManyToOne,然而從關係並不是必須的,如果你只在乎@ManyToOne關係,你可以不用在關聯的entity上定義@OneToMany 範例的code會產生如下的表格: ```sql! +-------------+--------------+----------------------------+ | photo | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | url | varchar(255) | | | userId | int(11) | FOREIGN KEY | +-------------+--------------+----------------------------+ +-------------+--------------+----------------------------+ | user | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | name | varchar(255) | | +-------------+--------------+----------------------------+ ``` 範例展示如何儲存: ```typescript! const photo1 = new Photo() photo1.url = "me.jpg" await dataSource.manager.save(photo1) const photo2 = new Photo() photo2.url = "me-and-bears.jpg" await dataSource.manager.save(photo2) const user = new User() user.name = "John" user.photos = [photo1, photo2] await dataSource.manager.save(user) ``` 或是另一種方式: ```typescript! const user = new User() user.name = "Leo" await dataSource.manager.save(user) const photo1 = new Photo() photo1.url = "me.jpg" photo1.user = user await dataSource.manager.save(photo1) const photo2 = new Photo() photo2.url = "me-and-bears.jpg" photo2.user = user await dataSource.manager.save(photo2) ``` 啟用cascade後,你只需要save一次即可。 為載入一個User帶有photos你可以這樣寫 ```typescript! const userRepository = dataSource.getRepository(User) const users = await userRepository.find({ relations: { photos: true, }, }) // or from inverse side const photoRepository = dataSource.getRepository(Photo) const photos = await photoRepository.find({ relations: { user: true, }, }) ``` 或是使用QueryBuilder並join ```typescript! const users = await dataSource .getRepository(User) .createQueryBuilder("user") .leftJoinAndSelect("user.photos", "photo") .getMany() // or from inverse side const photos = await dataSource .getRepository(Photo) .createQueryBuilder("photo") .leftJoinAndSelect("photo.user", "user") .getMany() ``` 如果eager loading選項有被啟用,你不需要特別寫relations因為他永遠都會被自動載入,如果你使用的是QueryBuilder則eager loading就會被關係,則需要使用leftJoinAndSelect來取得關聯資料。