mysql
typeORM
TypeORM是一款可以運行在許多平台上的ORM套件,可以使用TS和JS進行操作,目標是永遠可以使用最新版的JS版本和提供其餘功能來操作資料庫。
TypeORM支援兩種模式Active Recod
以及Data Mapper
來操作,這不同於現今其餘的JS ORM,這代表你可以用最有效率的方式寫出高品質、低耦合、可擴充、易維護的程式。
使用Active Record方式的話,你會將query的method寫在model本身,可以投過model method來進行存擋、刪除、載入。 簡單來說,Active Record模式就是讓你透過model來與資料庫互動。
以下範例:
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,以下為互動的範例:
// 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。範例如下:
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:
const timber = await User.findByName("Timber", "Saw")
使用Data Mapper方式的話,你會將query的method寫在"repositories",可以投過repositories來進行存擋、刪除、載入。 在data mapper模式中,你的entities是非常不靈光的,他就只是定義其properties並擁有一些'dummy' methods. 簡單來說,Data Mapper模式就是讓你透過repositores來與資料庫互動而非models。 以下範例:
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
@Column()
isActive: boolean
}
與資料庫互動的方式:
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
兩者各有其優缺點,只要記住軟體開發關乎的事後續如何維護,Data Mapper方法比較貼近於後續維護,在大型app中會更有效率,而Active Record講求的是讓方式簡潔,所以更適合於小型的app中,And simplicity is always a key to better maintainability(?)
npm install typeorm --save
npm install relect-metadata --save
npm install @types/node --save-dev
npm install mysql --save
(you can install mysql2 instead as well)確保你使用的TS版本是在4.5以上,且在tsconfig.json
啟用下面兩項:
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
你對一款ORM有什麼期待?首先,你會期許它能為你建立資料庫並且查詢/寫入/更新/刪除過程中也無需寫讓你頭痛的又難以維護的原始SQL語言,這份指南將展示如何從最基礎開始建立設定TypeORM並且讓你做到你所期待的事。
與資料庫互動前需要先建立table,如何叫TypeORM建立tables? 答案就是透過模型(models),你app內的模型就是你的資料庫tables。
例如以下,你有個Photo model:
export class Photo {
id: number
name: string
description: string
filename: string
views: number
isPublished: boolean
}
你想要在資料庫內儲存photos,為儲存這些photos,首先你需要一個資料庫table,且這資料庫table要從你的model所建立。並非所有模型都是,只有那些定義為entities的才是。
Entity就是當一個模型(model)被@Entity
所注入。一個資料庫table將會依照此models被建立,你可以在TypeORM的任何地方使用entities,你可以查詢/寫入/更新/刪除並用其執行其他動作。
以下依照Photo 模型(model)建立一個entity
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。
為加入資料庫columns,你只需要將entity的屬性簡單地加上@Column
注入器。
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。
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
}
現在,假如你想要讓你的id欄位可以被自動產生(也被稱為 auto-increment/ sequence / serial / generated identity column),為達到此目的,你需要更改@PrimaryColumn
注入器為@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
}
接下來,來修正下我們的資料型別,預設上,string會映照為像varchar(255)的type(依據資料庫類型)。number會映照為像integer的type,我們不會想要我們的資料欄位被限制為普通的varchar或是integer,可以做以下設定來修正資料類型:
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
}
欄位型別是為資料庫特定的,你可以設定任何欄位型別只要你的資料庫有支援的話。你可以看這裡來獲得更多關於你資料庫支援的欄位型別資訊。
現在,entity已經被建立,讓我們建立index.ts檔案並且將DataSource
建立在那邊:
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在每次連線時都可以更新同步。
+-------------+--------------+----------------------------+
| photo |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(100) | |
| description | text | |
| filename | varchar(255) | |
| views | int(11) | |
| isPublished | boolean | |
+-------------+--------------+----------------------------+
現在讓我們建立一張新照片並且將其存到資料庫中:
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
並且回傳。
我們剛建立了新照片並將其存到資料庫。我們使用EntityManager
來儲存它。你可以在app內操控任何entity藉由使用entity manager,例如:
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物件的陣列
現在讓我們重構並使用repository的方式,每個entity都有屬於自己的repository是用來進行處理所有跟entity有關的動作。使用repositories會比EntityManager來得方便當你要處理許多與entities相關的事情。
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)
以下試著藉由repository來進行查詢的動作:
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)
以下先查詢後進行更新並且保存:
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將會被更新並存到資料庫。
以下進行刪除:
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將會被刪除。
讓我們與另外一個class建立一對一關係,建立一個新的class名為PhotoMetadata。PhotoMetada會涵蓋photo的額外資訊:
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。
現在儲存一photo還有其metadata並與其做連結:
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",
)
關係可以是單向或雙向的,現在Photo和PhotoMetadata的關係是雙向的。 主關係為PhotoMetadata,Photo並不知道關於任何PhotoMetada的資訊,如果要從Photo端取得PhotoMetada的資訊將會變得相當複雜,為解決此問題,我們要加入逆關係(inverse relation),讓Photo和PhotoMetadata為雙向關係,以下修改entities:
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
}
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。
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>
}
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>
}
現在讓我們來讀取photo以及photo metadata在單一個query中,有兩個方式能夠做到-使用find*
方法-或是使用QueryBuilder的方式,以下先看find*
的方式,find*
的方式讓你可以指定一個object是FindOneOptions / FindManyOptions 的介面。
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,你可以在這裡看到更多關於Find Options的內容。
使用find的方式很好又簡單,但如果你需要較複雜的query方式,你需要使用QueryBuilder
,QueryBuilder
讓你可以以更優雅的方式執行更複雜的quires:
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屬性別稱,你可以使用這些別稱來存取資料。
可以在選項中設定cascade
,當我們想要同時保存物件以及關聯物件時,在photo注入器中加上cascade
:
export class Photo {
/// ... other columns
@OneToOne(() => PhotoMetadata, (metadata) => metadata.photo, {
cascade: true,
})
metadata: PhotoMetadata
}
使用cascade
不用在分開保存photo和metadata,只要保存photo物件,metadata也會自動被保存因為cascade
選項的關係。
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那裡。
接下來建立一組一對多、多對一關係,photo會有一個author,而每個author會有許多photos:
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:
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:
+-------------+--------------+----------------------------+
| author |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
+-------------+--------------+----------------------------+
也會對photo table做修正,加入一個新的author column附帶外鍵:
+-------------+--------------+----------------------------+
| photo |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
| description | varchar(255) | |
| filename | varchar(255) | |
| isPublished | boolean | |
| authorId | int(11) | FOREIGN KEY |
+-------------+--------------+----------------------------+
來建立多對多的關係,photo可以在許多albums內,albums也可以有許多photos:
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
會被加入在主關係那方
現在在反向關係那方也加入:
export class Photo {
/// ... other columns
@ManyToMany(() => Album, (album) => album.photos)
albums: Album[]
}
運行app後,ORM會建立一個album_photos_photo_albums連結table:
+-------------+--------------+----------------------------+
| album_photos_photo_albums |
+-------------+--------------+----------------------------+
| album_id | int(11) | PRIMARY KEY FOREIGN KEY |
| photo_id | int(11) | PRIMARY KEY FOREIGN KEY |
+-------------+--------------+----------------------------+
也別忘了將Album class加入到ORM的entities中:
const options: DataSourceOptions = {
// ... other options
entities: [Photo, PhotoMetadata, Author, Album],
}
現在來將albums和photos加入到資料庫中:
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會是下面:
{
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"
}]
}
你可以使用QueryBuilde構建出任何複雜度的SQL.例如:
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 的自由度更高,因此在專案中可能會大量的使用它。這裡有更多關於 QueryBuilder 的信息
當你建立好DataSouce的設定後才能與資料庫進行互動,TypeORM的DataSource乘載了資料庫的連線設定並且提供針對你使用的資料庫的初始化。
為建立初始連接,你必須調用DataSource實例的intialize方法,destroy方法用來斷開連線。
一般來說,你會在app啟用時調用intialize方法,並且在結束使用資料庫時調用detroy,實務上,如果你建立的是一個後端應用程式通常都會保持運行,並不會detroy DataSource。
為建立一個新的DataSource實例你需要呼叫new DataSource
來初始這個建構子並將其放在全域變數中以便在整個app中使用:
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,例如:
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: [
// ....
],
})
當你設定好你的DataSource後,你可以在任何地方使用,例如:
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來對資料庫進行操作。
DataSource是一組設定配置當你建立新的DataSource實例時可以傳入,不同的資料庫也有他們自己的配置設定可以傳入。
entities: [Post, Category, "entity/*.js", "modules/**/entity/*.js"]
。subscribers: [PostSubscriber, AppSubscriber, "subscriber/*.js", "modules/**/subscriber/*.js"]
。 migrations: [FirstMigration, SecondMigration, "migration/*.js", "modules/**/migration/*.js"]
。["query", "error", "schema"]
"advanced-console", "simple-console" and "file"
,預設為"advanced-console".
const dataSourceOptions: DataSourceOptions = dataSource.options
const isInitialized: boolean = dataSource.isInitialized
const driver: Driver = dataSource.driver
const manager: EntityManager = dataSource.manager
// you can call manager methods, for example find:
const users = await manager.find()
const manager: MongoEntityManager = dataSource.mongoManager
// you can call manager or mongodb-manager specific methods, for example find:
const users = await manager.find()
await dataSource.initialize()
await dataSource.destroy()
await dataSource.synchronize()
await dataSource.dropDatabase()
await dataSource.runMigrations()
await dataSource.undoLastMigration()
if (dataSource.hasMetadata(User))
const userMetadata = dataSource.getMetadata(User)
const userMetadata = dataSource.getMetadata(User)
// now you can get any information about User entity
const repository = dataSource.getRepository(User)
// now you can call repository methods, for example find:
const users = await repository.find()
const repository = dataSource.getTreeRepository(Category)
// now you can call tree repository methods, for example findTrees:
const categories = await repository.findTrees()
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()
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
})
const rawData = await dataSource.query(
SELECT * FROM USERS)
const users = await dataSource
.createQueryBuilder()
.select()
.from(User, "user")
.where("user.name = :name", { name: "John" })
.getMany()
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就是一個class映照到資料庫的table(或是MongoDB的collection),你可以建立一個entity藉由定義一個新的class並用@Entity()注入器注入:
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
@Column()
isActive: boolean
}
會建立以下table:
+-------------+--------------+----------------------------+
| user |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| firstName | varchar(255) | |
| lastName | varchar(255) | |
| isActive | boolean | |
+-------------+--------------+----------------------------+
基本的entities包含了欄位以及關係。每個entity都要有primary欄位(或是MongoDB中的ObjectId)。
每個entity都要註冊到data source中:
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],
})
或你可以指定整個資料夾,所有裡面的檔案都會被載入:
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的實例,因此它並不會知道你的建構子參數為合。
因為資料庫表格包含欄位,所以你的entities一定要有欄位。在Entity class裡面用@Column標注的都會映照成資料庫的表格欄位
每個entity中至少都會有一個primary欄位,有好幾種表達primary欄位的形式:
@PrimaruColumn()
建立一個primary欄位可以為任何value以及任何type,你可以主動設定欄位的type,如果沒有特別設定則會就property來自動推斷type,以下範例建立id為primary欄位:import { Entity, PrimaryColumn } from "typeorm"
@Entity()
export class User {
@PrimaryColumn()
id: number
}
@PrimaryGeneratedColumn()
建立一個primary欄位而其值會藉由一個自動增加的值自動產生,它會藉由auto-increment/serial/sequence/identity產生int欄位(根據資料庫或是提供的設定檔)。您不需在儲存前手動新增。import { Entity, PrimaryGeneratedColumn } from "typeorm"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
}
import { Entity, PrimaryGeneratedColumn } from "typeorm"
@Entity()
export class User {
@PrimaryGeneratedColumn("uuid")
id: string
}
你可以使用複數的primary欄位:
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,範例:
// 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",
})
有一些特殊欄位型別帶有額外的功能:
TypeORM 支援所有最常用的資料庫支援的欄位類型。 欄位類型是資料庫別有的 - 這位資料庫架構提供了最大的彈性。 你可以把欄位類型放在@Column的第一個參數或是在@Column的欄位選項中,例如:
@Column("int")
or
@Column({ type: "int" })
如果你想要指定額外的參數類型你也可以加在欄位選項中,例如:
@Column("varchar", { length: 200 })
or
@Column({ type: "int", width: 200 })
關於 bigint 類型的注意事項:在 SQL 數據庫中使用的 bigint 列類型不適合常規數字類型,而是將屬性映射到字符串。
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
postgres和mysql有支援enum欄位類型,有多種不同的定義欄位的方式:
使用Typescript ENUM:
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型別:
export type UserRoleType = "admin" | "editor" | "ghost",
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
role: UserRoleType
}
mariadb和mysql有支援set欄位類型,有多種不同的定義欄位的方式:
使用Typescript enums:
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類型:
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[]
}
關聯幫助你與其他entities更簡單的互動,有許多不同的關聯類型:
範例:
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[]
}
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[]
}
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選項可以為boolean或是一組array包含這些選項 ("insert" | "update" | "remove" | "soft-remove" | "recover")[],預設為false,若設定為true則會開起全部的cascades,你也可以設定特定的選項藉由提供array。
例如:
@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不只定義了關係的某邊會帶有外鍵也讓你客製化join column的名稱或是referenced column name.
當我們設定了@JoinColumn,他會在資料庫自動建立一組欄位名稱為 propertyName + referencedColumnName。例如:
@ManyToOne(type => Category)
@JoinColumn() // this decorator is optional for @ManyToOne, but required for @OneToOne
category: Category;
這組code會在資料庫中建立欄位categoryId
如果你想換不同的名稱可以如下:
@ManyToOne(type => Category)
@JoinColumn({ name: "cat_id" })
category: Category;
Join column總是會參考到其他的欄位,預設上是參考到關聯entity的primary欄位,如果你想跟其他欄位做關聯你也可以在@JoinColumn中設定:
@ManyToOne(type => Category)
@JoinColumn({ referencedColumnName: "name" })
category: Category;
現在這關聯到的事Category entity的name欄位而不是id。此關係的欄位名稱將會是categoryName。
你也可以關聯多個欄位,請注意這裡並沒有關聯預設中的主欄位,你必須要提供關聯欄位名稱。
@ManyToOne(type => Category)
@JoinColumn([
{ name: "category_id", referencedColumnName: "id" },
{ name: "locale_id", referencedColumnName: "locale_id" }
])
category: Category;
@JoinTable適用在多對多關聯上來描述集合表的欄位,集合表是TypeORM建立的一個特殊的單獨表,其內容包含了關聯entities的欄位,你可以更改集合表內欄位的名稱以及他們要關聯的欄位藉由@JoinColumn:你也可以更改集合表的名稱
@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[];
一對一關係就是A只包含了一個B實例,B只包含了一個A實例,以下用User和Profile做示範,一個User只會有一個Profile,一個Profile也只會被一個User所擁有。
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number
@Column()
gender: string
@Column()
photo: string
}
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的欄位:
+-------------+--------------+----------------------------+
| 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必須只能放在關係中的一邊,且那邊在資料庫中必須會帶有一組外鍵。
以下示範如何在使關係中存入資料:
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:
const users = await dataSource.getRepository(User).find({
relations: {
profile: true,
},
})
或是使用QueryBuilder並用join
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.profile", "profile")
.getMany()
如果eager loading選項有被啟用,你不需要特別寫relations因為他永遠都會被自動載入,如果你使用的是QueryBuilder則eager loading就會被關係,則需要使用leftJoinAndSelect來取得關聯資料。
關係可以為單向或是雙向的,單向就是只有一邊有關係注入器,則雙向是兩邊都有注入器。以下範例:
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
}
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
const profiles = await dataSource
.getRepository(Profile)
.createQueryBuilder("profile")
.leftJoinAndSelect("profile.user", "user")
.getMany()
Many-to-one / one-to-many就是A包含了多個B的實例但是B只會包含一個A的實例,以下用User和Photo來做示範,User可以有好幾張photos,但每一張photo就只會被一個User所擁有。
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
}
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會產生如下的表格:
+-------------+--------------+----------------------------+
| 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) | |
+-------------+--------------+----------------------------+
範例展示如何儲存:
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)
或是另一種方式:
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你可以這樣寫
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
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來取得關聯資料。