Database
Deepkit提供了一个ORM,允许以现代方式访问数据库。 实体是通过TypeScript类型简单定义的。
import { entity, PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type';
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
firstName?: string;
lastName?: string;
constructor(
public username: string & Unique & MinLength<2> & MaxLength<16>,
public email: string & Unique,
) {}
}
可以使用Deepkit的任何TypeScript类型和验证装饰器来完全定义实体。 实体类型系统的设计方式是,这些类型或类也可用于其他领域,如HTTP路由、RPC动作或前端。这可以防止,例如,一个用户在整个应用程序中被多次定义。
Installation
由于Deepkit ORM是基于Runtime Types的,所以有必要正确安装`@deepkit/type`。参见运行时类型安装。
如果这样做成功了,就可以安装`@deepkit/orm`本身和一个数据库适配器。
如果要将类作为实体使用,必须在tsconfig.json中激活`experimentalDecorators`。
{
"compilerOptions": {
"experimentalDecorators": true
}
}
一旦安装了这个库,就可以安装一个数据库适配器,可以直接使用它的API。
SQLite
npm install @deepkit/orm @deepkit/sqlite
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]);
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]);
MySQL
npm install @deepkit/orm @deepkit/mysql
import { MySQLDatabaseAdapter } from '@deepkit/mysql';
const database = new Database(new MySQLDatabaseAdapter({
host: 'localhost',
port: 3306
}), [User]);
使用
数据库 "对象是使用的主要对象。一旦被实例化,它就可以在整个应用程序中被用来查询或操作数据。与数据库的连接被初始化为懒惰。
`Database`对象被传递给一个适配器,它来自数据库适配器库。
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type';
import { Database } from '@deepkit/orm';
async function main() {
@entity.name('user')
class User {
public id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(public name: string) {
}
}
const database = new Database(new SQLiteDatabaseAdapter('./example.sqlite'), [User]);
await database.migrate(); //create tables
await database.persist(new User('Peter'));
const allUsers = await database.query(User).find();
console.log('all users', allUsers);
}
main();
实体
一个实体要么是一个类,要么是一个对象字面(接口),并且总是有一个主键。 使用`@deepkit/type`的类型装饰器对实体进行装饰,并提供所有必要的信息。例如,定义了一个主键,以及各种字段和它们的验证限制。这些字段反映了数据库结构,通常是一个表或一个集合。
通过特殊的类型装饰器,如 "Mapped<'name'>",一个字段名也可以被映射到数据库中的另一个名字。
级别
import { entity, PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type';
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
firstName?: string;
lastName?: string;
constructor(
public username: string & Unique & MinLength<2> & MaxLength<16>,
public email: string & Unique,
) {}
}
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]);
await database.migrate();
await database.persist(new User('Peter'));
const allUsers = await database.query(User).find();
console.log('all users', allUsers);
介面
import { PrimaryKey, AutoIncrement, Unique, MinLength, MaxLength } from '@deepkit/type';
interface User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
firstName?: string;
lastName?: string;
username: string & Unique & MinLength<2> & MaxLength<16>;
}
const database = new Database(new SQLiteDatabaseAdapter(':memory:'));
database.register<User>({name: 'user'});
await database.migrate();
const user: User = {id: 0, created: new Date, username: 'Peter'};
await database.persist(user);
const allUsers = await database.query<User>().find();
console.log('all users', allUsers);
Primitives
原始数据类型,如String、Number(bigint)和Boolean,被映射到常见的数据库类型。只有TypeScript类型被使用。
interface User {
logins: number;
username: string;
pro: boolean;
}
Primary Key
每个实体正好需要一个主键。不支持多主键。
主键的基本类型可以是任何类型。通常使用一个数字或UUID。 对于MongoDB,通常使用MongoId或ObjectID。
对于数字来说,"自动增量 "是一个不错的选择。
import { PrimaryKey } from '@deepkit/type';
interface User {
id: number & PrimaryKey;
}
自动增量
在插入过程中要自动递增的字段用`AutoIncrement`装饰器来注释。所有适配器都支持自动增量值。MongoDB适配器使用一个额外的集合来跟踪计数器。
自动增量字段是一个自动计数器,只能应用于一个主键。数据库自动确保一个ID只使用一次。
import { PrimaryKey, AutoIncrement } from '@deepkit/type';
interface User {
id: number & PrimaryKey & AutoIncrement;
}
UID
应该是UUID(v4)类型的字段用装饰器UUID来注释。运行时类型是 "string",在数据库本身中多为二进制。使用函数`uuid()`来创建一个新的UUID v4。
import { uuid, UUID, PrimaryKey } from '@deepkit/type';
class User {
id: UUID & PrimaryKey = uuid();
}
MongoDB ObjectID
在MongoDB中应该是ObjectID类型的字段用装饰器`MongoId`来注释。运行时的类型是 "string",而在数据库本身是 "ObjectId"(二进制)。
MongoID字段在插入时自动接收一个新值。使用字段名`_id`并不是强制性的。它可以有任何名字。
import { PrimaryKey, MongoId } from '@deepkit/type';
class User {
id: MongoId & PrimaryKey = '';
}
Optional / Nullable
可选字段被声明为TypeScript类型,有`title?: string`或`title: string | null`。你应该只使用一种变体,通常是可选的`?语法,与`undefined`一起使用。
这两种变体都会导致所有SQL适配器的数据库类型为`NULLABLE
。因此,这些装饰器之间的唯一区别是,它们在运行时代表不同的值。
在下面的例子中,改变的字段是可选的,因此在运行时可以不定义,尽管它在数据库中总是表示为NULL。
import { PrimaryKey } from '@deepkit/type';
class User {
id: number & PrimaryKey = 0;
modified?: Date;
}
这个例子显示了nullable类型是如何工作的。NULL在数据库和javascript运行时都被使用。这比 "modified?: Date "更粗略,不经常使用。
import { PrimaryKey } from '@deepkit/type';
class User {
id: number & PrimaryKey = 0;
modified: Date | null = null;
}
数据库类型映射
Runtime type | SQLite | MySQL | Postgres | Mongo |
---|---|---|---|---|
string |
text |
longtext |
text |
string |
number |
float |
double |
double precision |
int/number |
boolean |
integer(1) |
boolean |
boolean |
boolean |
date |
text |
datetime |
timestamp |
datetime |
array |
text |
json |
jsonb |
array |
map |
text |
json |
jsonb |
object |
map |
text |
json |
jsonb |
object |
union |
text |
json |
jsonb |
T |
uuid |
blob |
binary(16) |
uuid |
binary |
ArrayBuffer/Uint8Array/… |
blob |
longblob |
bytea |
binary |
通过`DatabaseField`,可以将一个字段映射到任何数据库类型。该类型必须是一个有效的SQL语句,该语句将不变地传递给迁移系统。
import { DatabaseField } from '@deepkit/type';
interface User {
title: string & DatabaseField<{type: 'VARCHAR(244)'}>;
}
要为一个特定的数据库映射一个字段,可以使用`SQLite`、MySQL`或`Postgres
。
SQLite
import { SQLite } from '@deepkit/type';
interface User {
title: string & SQLite<{type: 'text'}>;
}
会话/工作单位
一节课是类似于一个工作单位的东西。它记录了你所做的一切,并在调用`commit()`时自动记录了这些变化。它是在数据库中执行变化的首选方式,因为它以一种使其非常快的方式捆绑语句。一个会话是非常轻量级的,可以很容易地在请求-响应生命周期中创建,例如。
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type';
import { Database } from '@deepkit/orm';
async function main() {
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(public name: string) {
}
}
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User]);
await database.migrate();
const session = database.createSession();
session.add(new User('User1'), new User('User2'), new User('User3'));
await session.commit();
const users = await session.query(User).find();
console.log(users);
}
main();
用`session.add(T)`向会话添加新实例,或用`session.remove(T)`删除现有实例。一旦你完成了对会话对象的处理,只需在各处取消对它的引用,这样垃圾收集器就能将其删除。
对于通过会话对象获取的实体实例,会自动识别变化。
const users = await session.query(User).find();
for (const user of users) {
user.name += ' changed';
}
await session.commit();//saves all users
查询
查询是一个描述如何从数据库检索或改变数据的对象。它有几种方法来描述查询和执行查询的终止方法。数据库适配器可以以多种方式扩展查询API,以支持数据库的特定功能。
你可以通过使用`Database.query(T)`或`Session.query(T)`创建一个查询。我们推荐Sessions,因为它可以提高性能。
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
birthdate?: Date;
visits: number = 0;
constructor(public username: string) {
}
}
const database = new Database(...);
//[ { username: 'User1' }, { username: 'User2' }, { username: 'User2' } ]
const users = await database.query(User).select('username').find();
过滤
可以应用一个过滤器来限制结果集。
//simple filters
const users = await database.query(User).filter({name: 'User1'}).find();
//multiple filters, all AND
const users = await database.query(User).filter({name: 'User1', id: 2}).find();
//range filter: $gt, $lt, $gte, $lte (greater than, lower than, ...)
//equivalent to WHERE created < NOW()
const users = await database.query(User).filter({created: {$lt: new Date}}).find();
//equivalent to WHERE id > 500
const users = await database.query(User).filter({id: {$gt: 500}}).find();
//equivalent to WHERE id >= 500
const users = await database.query(User).filter({id: {$gte: 500}}).find();
//set filter: $in, $nin (in, not in)
//equivalent to WHERE id IN (1, 2, 3)
const users = await database.query(User).filter({id: {$in: [1, 2, 3]}}).find();
//regex filter
const users = await database.query(User).filter({username: {$regex: /User[0-9]+/}}).find();
//grouping: $and, $nor, $or
//equivalent to WHERE (username = 'User1') OR (username = 'User2')
const users = await database.query(User).filter({
$or: [{username: 'User1'}, {username: 'User2'}]
}).find();
//nested grouping
//equivalent to WHERE username = 'User1' OR (username = 'User2' and id > 0)
const users = await database.query(User).filter({
$or: [{username: 'User1'}, {username: 'User2', id: {$gt: 0}}]
}).find();
//nested grouping
//equivalent to WHERE username = 'User1' AND (created < NOW() OR id > 0)
const users = await database.query(User).filter({
$and: [{username: 'User1'}, {$or: [{created: {$lt: new Date}, id: {$gt: 0}}]}]
}).find();
选择
为了缩小从数据库接收的字段,可以使用`select('field1')`。
const user = await database.query(User).select('username').findOne();
const user = await database.query(User).select('id', 'username').findOne();
需要注意的是,一旦通过 "选择 "缩小了字段的范围,结果就不再是实体的实例,而只是对象字面。
const user = await database.query(User).select('username').findOne();
user instanceof User; //false
Order
通过`orderBy(field, order)`,可以改变条目的顺序。 可以多次执行`orderBy',使订单越来越细化。
const users = await session.query(User).orderBy('created', 'desc').find();
const users = await session.query(User).orderBy('created', 'asc').find();
Pagination
通过`itemsPerPage()`和`page()`方法,可以对结果进行分页。页面从1开始。
const users = await session.query(User).itemsPerPage(50).page(1).find();
使用替代方法`limit`和`skip`,你可以手动分页。
const users = await session.query(User).limit(5).skip(10).find();
Join
默认情况下,来自实体的引用既不包括也不在查询中加载。要在查询中包含一个连接而不加载引用,请使用`join()(左连接)或`innerJoin()
。要在查询中包含一个连接并加载引用,请使用`joinWith()或`innerJoinWith()
。
以下所有的例子都是基于这些模型方案。
@entity.name('group')
class Group {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(public username: string) {
}
}
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
group?: Group & Reference;
constructor(public username: string) {
}
}
//select only users with a group assigned (INNER JOIN)
const users = await session.query(User).innerJoin('group').find();
for (const user of users) {
user.group; //error, since reference was not loaded
}
//select only users with a group assigned (INNER JOIN) and load the relation
const users = await session.query(User).innerJoinWith('group').find();
for (const user of users) {
user.group.name; //works
}
要修改连接查询,使用相同的方法,但要使用`use`前缀:useJoin
、useInnerJoin
、useJoinWith`或`useInnerJoinWith
。要结束对连接查询的修改,请使用`end()`来取回父查询。
//select only users with a group with name 'admins' assigned (INNER JOIN)
const users = await session.query(User)
.useInnerJoinWith('group')
.filter({name: 'admins'})
.end() // returns to the parent query
.find();
for (const user of users) {
user.group.name; //always admin
}
Aggregation
聚合方法允许你计算记录和聚合字段。
下面的例子是基于这个模型方案的。
@entity.name('file')
class File {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
downloads: number = 0;
category: string = 'none';
constructor(public path: string & Index) {
}
}
`groupBy`允许将结果按指定字段分组。
await database.persist(
cast<File>({path: 'file1', category: 'images'}),
cast<File>({path: 'file2', category: 'images'}),
cast<File>({path: 'file3', category: 'pdfs'})
);
//[ { category: 'images' }, { category: 'pdfs' } ]
await session.query(File).groupBy('category').find();
有几种聚合方法:withSum', `withAverage', `withCount', `withMin', `withMax', `withGroupConcat
。每个都需要一个字段名作为第一个参数,以及一个可选的第二个参数来改变别名。
// first let's update some of the records:
await database.query(File).filter({path: 'images/file1'}).patchOne({$inc: {downloads: 15}});
await database.query(File).filter({path: 'images/file2'}).patchOne({$inc: {downloads: 5}});
//[{ category: 'images', downloads: 20 },{ category: 'pdfs', downloads: 0 }]
await session.query(File).groupBy('category').withSum('downloads').find();
//[{ category: 'images', downloads: 10 },{ category: 'pdfs', downloads: 0 }]
await session.query(File).groupBy('category').withAverage('downloads').find();
//[ { category: 'images', amount: 2 }, { category: 'pdfs', amount: 1 } ]
await session.query(File).groupBy('category').withCount('id', 'amount').find();
Returning
有了`returning`,就可以通过`patch`和`delete`在发生变化时请求额外的字段。
注意:不是所有的数据库适配器都会原子化地返回字段。使用交易来确保数据的一致性。
await database.query(User).patchMany({visits: 0});
//{ modified: 1, returning: { visits: [ 5 ] }, primaryKeys: [ 1 ] }
const result = await database.query(User)
.filter({username: 'User1'})
.returning('username', 'visits')
.patchOne({$inc: {visits: 5}});
查找
返回一个符合指定过滤器的条目数组。
const users: User[] = await database.query(User).filter({username: 'Peter'}).find();
找到一个
返回一个符合指定过滤器的条目。 如果没有找到项目,就会抛出 "没有找到项目 "的错误。
const users: User = await database.query(User).filter({username: 'Peter'}).findOne();
查找一个或未定义
返回一个符合指定过滤器的条目。 如果没有找到条目,则返回未定义。
const query = database.query(User).filter({username: 'Peter'});
const users: User|undefined = await query.findOneOrUndefined();
查找字段
返回一个符合指定过滤器的字段的列表。
const usernames: string[] = await database.query(User).findField('username');
查找一个字段
返回一个符合指定过滤器的字段的列表。 如果没有找到项目,就会抛出 "没有找到项目 "的错误。
const username: string = await database.query(User).filter({id: 3}).findOneField('username');
Patch
补丁是一个变更查询,对查询中描述的记录进行补丁。方法 `patchOne`和`patchMany`完成查询并执行补丁。
`patchMany`改变数据库中所有符合指定过滤器的条目。如果没有设置过滤器,整个表就会被改变。使用`patchOne`一次只改变一个条目。
await database.query(User).filter({username: 'Peter'}).patch({username: 'Peter2'});
await database.query(User).filter({username: 'User1'}).patchOne({birthdate: new Date});
await database.query(User).filter({username: 'User1'}).patchOne({$inc: {visits: 1}});
await database.query(User).patchMany({visits: 0});
Delete
deleteMany`删除数据库中所有符合指定过滤器的条目。
如果没有设置过滤器,整个表将被删除。使用`deleteOne
,一次只删除一个条目。
const result = await database.query(User)
.filter({visits: 0})
.deleteMany();
const result = await database.query(User).filter({id: 4}).deleteOne();
有
返回数据库中是否存在至少一个条目。
const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has();
Lift
提升一个查询意味着向它添加新的功能。这通常由插件或复杂的架构来使用,将较大的查询类分割成几个方便、可重复使用的类。
import { FilterQuery, Query } from '@deepkit/orm';
class UserQuery<T extends {birthdate?: Date}> extends Query<T> {
hasBirthday() {
const start = new Date();
start.setHours(0,0,0,0);
const end = new Date();
end.setHours(23,59,59,999);
return this.filter({$and: [{birthdate: {$gte: start}}, {birthdate: {$lte: end}}]} as FilterQuery<T>);
}
}
await session.query(User).lift(UserQuery).hasBirthday().find();
关系
关系允许你以某种方式连接两个实体。这通常是在数据库中使用外键的概念进行的。Deepkit ORM支持所有官方数据库适配器的关系。
一个关系被注释为 "参考 "装饰器。通常,一个关系也有一个反向关系,它被注释为 "反向参考 "类型,但只有在反向关系被用于数据库查询时才需要。背面的参考只是虚拟的。
One To Many
存储引用的实体通常被称为 "拥有页 "或 "拥有引用 "的实体。下面的代码显示了两个实体,在`User`和`Post`之间有一对多的关系。这意味着,一个 "用户 "可以有多个 "帖子"。实体`post`具有`post→user`的关系。在数据库本身,现在有一个字段`Post. "author"`,包含`User`的主键。
import { SQLiteDatabaseAdapter } from '@deepkit/sqlite';
import { entity, PrimaryKey, AutoIncrement, Reference } from '@deepkit/type';
import { Database } from '@deepkit/orm';
async function main() {
@entity.name('user').collectionName('users')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(public username: string) {
}
}
@entity.name('post')
class Post {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
constructor(
public author: User & Reference,
public title: string
) {
}
}
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User, Post]);
await database.migrate();
const user1 = new User('User1');
const post1 = new Post(user1, 'My first blog post');
const post2 = new Post(user1, 'My second blog post');
await database.persist(user1, post1, post2);
}
main();
默认情况下,查询中不选择引用。参见[数据库连接]中的内容。
多对一
一个引用通常有一个反向引用,称为多对一。它只是一个虚拟参考,因为它没有反映在数据库本身。背面引用被注解为 "BackReference",主要用于反射和查询连接。如果你从 "用户 "向 "帖子 "添加了一个 "返回参考",你可以直接从 "用户 "查询中加入 "帖子"。
@entity.name('user').collectionName('users')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
posts?: Post[] & BackReference;
constructor(public username: string) {
}
}
//[ { username: 'User1', posts: [ [Post], [Post] ] } ]
const users = await database.query(User)
.select('username', 'posts')
.joinWith('posts')
.find();
Many To Many
多对多的关系允许你将许多记录与其他许多记录联系起来。例如,它可以用于群组中的用户。一个用户可以不在任何组,也可以在一个或多个组。因此,一个组可以包含0、1或许多用户。
多对多的关系通常通过一个枢轴实体来实现。支点实体包含对其他两个实体的实际自己的引用,而这两个实体对支点实体有反向引用。
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
groups?: Group[] & BackReference<{via: typeof UserGroup}>;
constructor(public username: string) {
}
}
@entity.name('group')
class Group {
id: number & PrimaryKey & AutoIncrement = 0;
users?: User[] & BackReference<{via: typeof UserGroup}>;
constructor(public name: string) {
}
}
//the pivot entity
@entity.name('userGroup')
class UserGroup {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(
public user: User & Reference,
public group: Group & Reference,
) {
}
}
有了这些实体,你现在可以创建用户和组,并将它们连接到透视实体。通过使用User中的反向引用,我们可以用User查询直接检索到组。
const database = new Database(new SQLiteDatabaseAdapter(':memory:'), [User, Group, UserGroup]);
await database.migrate();
const user1 = new User('User1');
const user2 = new User('User2');
const group1 = new Group('Group1');
await database.persist(user1, user2, group1, new UserGroup(user1, group1), new UserGroup(user2, group1));
//[
// { id: 1, username: 'User1', groups: [ [Group] ] },
// { id: 2, username: 'User2', groups: [ [Group] ] }
// ]
const users = await database.query(User)
.select('username', 'groups')
.joinWith('groups')
.find();
要解除一个用户与一个组的联系,需要删除UserGroup的记录。
const users = await database.query(UserGroup)
.filter({user: user1, group: group1})
.deleteOne();
Events
事件是插入Deepkit ORM的一种方式,允许你编写强大的插件。有两类事件。查询事件和工作单位事件。插件作者通常使用这两种方式来支持这两种操作数据的方式。
事件是通过`Database.listen`和一个事件标记注册的。短暂的事件监听器也可以被注册在会话上。
import { Query, Database } from '@deepkit/orm';
const database = new Database(...);
database.listen(Query.onFetch, async (event) => {
});
const session = database.createSession();
//will only be executed for this particular session
session.eventDispatcher.listen(Query.onFetch, async (event) => {
});
Query Events
当通过`Database.query()`或`Session.query()`执行查询时,查询事件被触发。
每个事件都有自己的附加属性,如实体的类型、查询本身和数据库会话。你可以通过给`Event.query`设置一个新的查询来覆盖这个查询。
import { Query, Database } from '@deepkit/orm';
const database = new Database(...);
const unsubscribe = database.listen(Query.onFetch, async event => {
//overwrite the query of the user, so something else is executed.
event.query = event.query.filterField('fieldName', 123);
});
//to delete the hook call unsubscribe
unsubscribe();
查询 "有几个事件标记。
Event-Token | Description |
---|---|
Query.onFetch |
|
Query.onDeletePre |
|
Query.onDeletePost |
|
Query.onPatchPre |
|
Query.onPatchPost |
Transactions
事务是一组连续的语句、查询或操作,如选择、插入、更新或删除,作为一个工作单元执行,可以确认或撤销。
Deepkit支持所有官方支持的数据库的交易。默认情况下,任何查询或数据库会话都不使用事务。为了实现交易,有两种主要方法:会话和回调。
会议交易
你可以为每个创建的会话启动和分配一个新的交易。这是与数据库交互的首选方式,因为你可以简单地传递会话对象,这个会话实例化的所有查询都会自动分配给其事务。
一个典型的模式是将所有的操作包在一个try-catch块中,并在最后一行执行`commit()(只有在所有先前的命令都成功的情况下才执行),并在catch块中执行`rollback()
,一旦发生错误就回滚所有的修改。
虽然有一个替代的API(见下文),但所有的事务都只对数据库会话对象起作用。为了将数据库会话中的工作单元中未完成的修改提交给数据库,通常会调用`commit()。在一个事务性会话中,`commit()`不仅向数据库提交所有未完成的修改,而且还完成了事务("提交"),从而关闭事务。另外,你可以调用`session.flush()`来提交所有未完成的修改,而不用`commit
,因此也不用关闭事务。要提交一个事务而不清空工作单元,请使用`session.commitTransaction()`。
const session = database.createSession();
//this assigns a new transaction, and starts it with the very next database operation.
session.useTransaction();
try {
//this query is executed in the transaction
const users = await session.query(User).find();
await moreDatabaseOperations(session);
await session.commit();
} catch (error) {
await session.rollback();
}
一旦`commit()或`rollback()`在会话中被执行,事务就被释放。如果你想在一个新的交易中继续,你必须再次调用`useTransaction()
。
请注意,一旦在一个事务性会话中执行了第一个数据库操作,分配的数据库连接就会永久地、专门地分配给当前会话对象(粘性)。因此,所有后续操作都是在同一个连接上执行的(因此在同一个数据库服务器上的大多数数据库中)。只有当事务性会话被终止(提交或回滚)时,数据库连接才会被再次释放。因此,建议只在必要时保持交易的简短。
如果一个会话已经与一个事务相关联,对`session.useTransaction()`的调用总是返回同一个对象。使用`session.isTransaction()`来检查交易是否与会话相关。
不支持嵌套交易。
交易回调
事务性会话的一个替代方法是`database.transaction(callback)`。
await database.transaction(async (session) => {
//this query is executed in the transaction
const users = await session.query(User).find();
await moreDatabaseOperations(session);
});
database.transaction(callback)`方法在一个新的事务性会话中执行一个异步回调。如果回调成功(即没有抛出错误),会话就会自动提交(从而提交其事务并刷新所有更改)。如果回调失败,会话自动执行`rollback()
,并且错误被传播。
Isolations
许多数据库支持不同类型的交易。为了改变交易行为,你可以为`useTransaction()`返回的交易对象调用不同的方法。这个事务对象的接口取决于所使用的数据库适配器。例如,从MySQL数据库返回的交易对象与从MongoDB数据库返回的交易对象有不同的选项。使用代码补全或查看数据库适配器的界面以获得可能的选项列表。
const database = new Database(new MySQLDatabaseAdapter());
const session = database.createSession();
session.useTransaction().readUncommitted();
try {
//...operations
await session.commit();
} catch () {
await session.rollback();
}
//or
await database.transaction(async (session) => {
//this works as long as no database operation has been exuected.
session.useTransaction().readUncommitted();
//...operations
});
虽然MySQL、PostgreSQL和SQLite的交易默认是有效的,但你必须首先将MongoDB设置为一个 "复制集"。
要将一个标准的MongoDB实例转换为一个副本集,请参考官方文档链接:https://docs.mongodb.com/manual/tutorial/convert-standalone-to-replica-set/[将单机转换为一个副本集]。
复合主键
复合主键是指一个实体有几个主键,这些主键被自动组合成一个 "复合主键"。这种建立数据库模型的方式有优点也有缺点。我们认为复合主键有巨大的实际缺点,不能证明它们的优点,所以它们应该被认为是不好的做法,因此要避免。Deepkit ORM不支持复合主键。在本章中,我们将解释原因并展示(更好的)替代方案。
加入
涉及的字段越多,每个单独的连接就变得越复杂。虽然许多数据库已经实现了优化,使多字段的连接本身并不慢,但它要求开发人员不断地详细思考这些连接,因为,例如,忘记键可能会导致微妙的错误(因为即使没有指定所有的键,连接也会工作),因此开发人员需要知道完整的复合主键结构。
指数
有多个字段的指数(属于复合主键)在查询中存在字段顺序的问题。虽然数据库系统可以优化某些查询,但对于复杂的结构,很难编写出正确使用所有定义索引的有效操作。对于一个有多个字段的索引(比如复合主键),通常需要以正确的顺序定义字段,以便数据库能够实际使用该索引。如果顺序没有被正确指定(例如在WHERE子句中),这很容易导致数据库根本不使用索引,而是执行全表扫描。知道哪种数据库查询以哪种方式进行优化是新的开发人员通常不具备的高级知识,但在你开始使用复合主键时就必须知道,这样你才能从数据库中获得最大的收益,不浪费资源。
迁移
一旦你决定一个特定的实体需要一个额外的字段来唯一地识别它(从而成为复合主键),这将导致你数据库中所有与该实体有关系的实体的调整。
例如,假设你有一个带有复合主键的实体 "用户",你决定在不同的表中使用这个 "用户 "的外键,例如在透视表 "审计_日志"、"组 "和 "帖子 "中。只要你改变了`user’的主键,所有这些表也必须在迁移中进行调整。
这不仅使迁移文件变得更加复杂,而且在运行迁移文件时也会导致更大的停机时间,因为模式的改变通常需要一个完整的数据库锁或至少一个表锁。受大型变化(如索引变化)影响的表越多,迁移的时间就越长。而桌子越大,迁移的时间就越长。 考虑一下`audit_log`表。这样的表通常有很多记录(几百万条左右),你只需要在改变模式时接触它们,因为你已经决定使用复合主键,并在`用户’的主键上增加一个额外的字段。根据所有这些表的大小,这要么使迁移的费用不必要地增加,要么在某些情况下,费用高到改变 "用户 "的主键在经济上不再是合理的。这通常会导致变通的方法(例如给用户表添加唯一索引),导致技术债务,并迟早会被列入遗留问题清单。
对于大型项目来说,这可能会导致巨大的停机时间(从几分钟到几小时不等),有时甚至会引入一个全新的迁移抽象系统,该系统基本上是复制表,将记录插入幽灵表,在迁移后来回移动表。这种增加的复杂性反过来又强加在任何与另一个实体有复合主键关系的实体身上,而且你的数据库结构越大,这种复杂性就越大。这个问题越来越严重,没有办法解决(除了完全删除复合主键)。
可查找性
如果你是一个数据库管理员或数据工程师/科学家,你通常直接在数据库上工作,并在需要时探索数据。有了复合主键,任何直接编写SQL的用户都需要知道所有涉及的表的正确主键(以及列的顺序以获得正确的索引优化)。这种额外的开销不仅使检查数据、创建报告等变得困难,而且如果一个复合主键突然被改变,还会导致旧的SQL出错。旧的SQL可能仍然有效并且运行良好,但是突然返回不正确的结果,因为复合主键中的新字段在连接中丢失了。在这里,只有一个主键要容易得多。这使得查找数据更加容易,并确保在你决定改变用户对象的唯一识别方式时,旧的SQL查询仍能正常工作,例如。
修订版
一旦在一个实体中使用了复合主键,重构该键就会导致大量的额外重构。由于具有复合主键的实体通常没有一个唯一的字段,所有的过滤器和链接必须包含复合主键的所有值。这通常意味着代码依赖于对复合主键的了解,因此必须检索所有字段(例如对于像/user/:key1/:key2这样的URL)。一旦这个键被改变,所有明确使用这个知识的地方,如URL、自定义SQL查询和其他地方,都必须被重写。
虽然ORM通常会自动创建连接,而不需要手动指定值,但它们不能自动涵盖所有其他用例的重构,如URL结构或自定义SQL查询,尤其是在完全不使用ORM的地方,如报告系统和所有外部系统中。
ORM的复杂性
支持复合主键会极大地增加像Deepkit ORM这样的强大ORM的代码的复杂性。不仅代码和维护会变得更加复杂,因此更加昂贵,而且会有更多来自用户的边缘案例需要被修复和维护。查询层、变化检测、迁移系统、内部跟踪关系等的复杂性大大增加。总的来说,建立和支持具有复合主键的ORM的整体成本太高,无法证明其合理性,这就是Deepkit不支持它的原因。
优势
除此之外,复合主键也有优势,尽管是非常表面的优势。通过对每个表使用尽可能少的索引,写入(插入/更新)数据变得更加高效,因为需要维护的索引更少。这也使模型的结构更简洁一些(因为它通常少了一列)。然而,按顺序排列、自动递增的主键和不递增的主键之间的差别现在完全可以忽略不计,因为磁盘空间很便宜,而且这个过程通常只是一个追加操作,非常快。
当然,在一些边缘情况下(以及一些非常特殊的数据库系统),最初使用复合主键会更好。但即使在这些系统中,不使用它们而改用另一种策略,总体上可能更合理(考虑到所有成本)。
替代方案
复合主键的一个替代方法是使用一个自动递增的数字主键,通常称为 "id",并将复合主键移到一个具有多个字段的唯一索引中。根据所使用的主键(取决于预期的行数),"id "每条记录使用4或8个字节。
通过使用这种策略,人们不再被迫思考上述问题并找到解决方案,这极大地降低了越来越大的项目的成本。
该策略的具体含义是,每个实体都有一个 "id "字段,通常在最开始,然后这个字段在默认情况下和连接中被用来识别唯一的行。
class User {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(public username: string) {}
}
作为复合主键的替代品,你可以使用唯一的多字段索引来代替。
@entity.index(['tenancyId', 'username'], {unique: true})
class User {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(
public tenancyId: number,
public username: string,
) {}
}
Deepkit ORM自动支持增量主键,包括对MongoDB。这是在数据库中识别记录的首选方法。然而,对于MongoDB,你可以使用ObjectId(_id: MongoId & PrimaryKey = ''
)作为一个简单的主键。数字化的、自动递增的主键的一个替代品是UUID,它的效果同样好(但性能特征略有不同,因为索引的成本更高)。
插件
Soft-Delete
软删除插件使隐藏数据库记录而不实际删除它们成为可能。当一条记录被删除时,它只被标记为删除,而不是实际删除。所有的查询都会自动过滤这个被删除的属性,所以对用户来说,感觉就像它真的被删除了一样。
要使用该插件,你必须实例化SoftDelete类,并为每个实体激活它。
import { entity, PrimaryKey, AutoIncrement } from '@deepkit/type';
import { SoftDelete } from '@deepkit/orm';
@entity.name('user')
class User {
id: number & PrimaryKey & AutoIncrement = 0;
created: Date = new Date;
// this field is used as indicator whether the record is deleted.
deletedAt?: Date;
// this field is optional and can be used to track who/what deleted the record.
deletedBy?: string;
constructor(
public name: string
) {
}
}
const softDelete = new SoftDelete(database);
softDelete.enable(User);
//or disable again
softDelete.disable(User);
恢复
被删除的记录可以通过`SoftDeleteQuery`使用取消的查询来恢复。它有`restoreOne`和`restoreMany`。
import { SoftDeleteQuery } from '@deepkit/orm';
await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreOne();
await database.query(User).lift(SoftDeleteQuery).filter({ id: 1 }).restoreMany();
该会议还支持元素的恢复。
import { SoftDeleteSession } from '@deepkit/orm';
const session = database.createSession();
const user1 = session.query(User).findOne();
session.from(SoftDeleteSession).restore(user1);
await session.commit();
硬删除
要硬删除记录,请通过SoftDeleteQuery使用解除的查询。这基本上恢复了没有单一查询插件的旧行为。
import { SoftDeleteQuery } from '@deepkit/orm';
await database.query(User).lift(SoftDeleteQuery).hardDeleteOne();
await database.query(User).lift(SoftDeleteQuery).hardDeleteMany();
//those are equal
await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteOne();
await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().deleteMany();
查询删除。
通过`SoftDeleteQuery`的 "解除 "查询,你也可以包括已删除的记录。
import { SoftDeleteQuery } from '@deepkit/orm';
// find all, soft deleted and not deleted
await database.query(User).lift(SoftDeleteQuery).withSoftDeleted().find();
// find only soft deleted
await database.query(s).lift(SoftDeleteQuery).isSoftDeleted().count()
被删除的
`deletedBy`可以通过查询和会话设置。
import { SoftDeleteSession } from '@deepkit/orm';
const session = database.createSession();
const user1 = session.query(User).findOne();
session.from(SoftDeleteSession).setDeletedBy('Peter');
session.remove(user1);
await session.commit();
import { SoftDeleteQuery } from '@deepkit/orm';
database.query(User).lift(SoftDeleteQuery)
.deletedBy('Peter')
.deleteMany();