Database
Deepkit bietet ein ORM, das es ermöglicht, auf Datenbanken auf moderne Art und Weise zuzugreifen. Entities werden dabei einfach über TypeScript Typen definiert:
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,
) {}
}
Dabei können beliebige TypeScript Typen und Validierung-Dekoratoren von Deepkit benutzt werden, um die Entity vollumfänglich zu definieren. Das Entity-Typensystem ist dabei so ausgelegt, dass diese Typen beziehungsweise Klassen ebenfalls in anderen Bereichen wie HTTP-Routen, RPC-Aktionen, oder Frontend benutzt werden können. Das verhindert, dass man zum Beispiel einen User mehrmals in der gesamten Applikation verteilt definiert hat.
Installation
Da Deepkit ORM auf den Runtime Types basiert, ist es notwendig @deepkit/type
bereits korrekt installiert zu haben. Siehe dazu Runtime Type Installation.
Falls dies erfolgreich geschehen ist, kann @deepkit/orm
selbst und ein Datenbank-Adapter installiert werden.
Falls Klassen als Entities verwendet werden sollen, muss experimentalDecorators
in der tsconfig.json aktiviert werden:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Sobald die Library installiert ist, kann ein Datenbank-Adapter installiert und die API davon direkt benutzt werden.
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]);
Benutzung
Es wird primär mit dem Database
Objekt gearbeitet. Einmal instantiiert, kann es innerhalb der ganzen Anwendung genutzt werde, um Daten abzufragen oder zu manipulieren. Die Verbindung zur Datenbank wird dabei lazy initialisiert.
Dem Database
Objekt wird ein Adapter übergeben, welcher aus den Datenbank-Adaptern Libraries kommt.
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();
Entity
Eine Entity ist entweder eine Klasse oder Object Literal (interface) und hat immer einen Primary Key.
Die Entity wird mittels Typen-Dekoratoren aus @deepkit/type
mit alle notwendigen Informationen dekoriert. Zum Beispiel wird ein Primary Key definiert sowie diverse Felder und ihre Validierungen-Einschränkungen. Dieser Felder spiegeln die Datenbank-Struktur ab, in der Regel eine Tabelle oder eine Collection.
Durch spezielle Typen-Dekoratoren wie Mapped<'name'>
kann ein Feldnamen auch auf einen anderen Namen in der Datenbank abgebildet werden.
Klasse
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);
Interface
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
Primitive Datentypen wie String, Number (bigint), und Boolean werden gängige Datenbank-Typen abgebildet. Es wird dabei lediglich der TypeScript Type genutzt.
interface User {
logins: number;
username: string;
pro: boolean;
}
Primary Key
Jede Entity braucht genau einen Primary Key. Mehrere Primary Keys werden nicht unterstützt.
Der Basis-Typ eines Primary Keys kann dabei beliebig sein. Oft wird eine Nummer oder UUID verwendet. Für MongoDB wird gerne die MongoId bzw ObjectID verwendet.
Bei Nummern bietet sich AutoIncrement
an.
import { PrimaryKey } from '@deepkit/type';
interface User {
id: number & PrimaryKey;
}
Auto Increment
Felder, die beim Einfügen automatisch inkrementiert werden sollen, werden mit dem AutoIncrement
Dekorator annotiert. Alle Adapter unterstützen auto-increment Werte. Der MongoDB Adapter verwendet eine zusätzliche Collection, um den Zähler zu verfolgen.
Ein Auto-Increment Feld ist ein automatischer Zähler und kann nur an einem Primary Key angewendet werden. Die Datenbank stellt automatisch sicher, dass eine ID nur einmal verwendet wird.
import { PrimaryKey, AutoIncrement } from '@deepkit/type';
interface User {
id: number & PrimaryKey & AutoIncrement;
}
UUID
Felder, die vom Typ UUID (v4) sein sollten, werden mit dem Dekorator UUID annotiert. Der Laufzeittyp ist string
und in der Datenbank selbst meist binär. Verwenden Sie die Funktion uuid()
, um eine neue UUID v4 zu erzeugen.
import { uuid, UUID, PrimaryKey } from '@deepkit/type';
class User {
id: UUID & PrimaryKey = uuid();
}
MongoDB ObjectID
Felder, die in MongoDB vom Typ ObjectID sein sollten, werden mit dem Dekorator MongoId
annotiert. Der Laufzeittyp ist string
und in der Datenbank selbst ObjectId
(binär).
MongoID-Felder erhalten beim Einfügen automatisch einen neuen Wert. Es ist nicht zwingend nötig, den Feldnamen _id
zu verwenden. Er kann einen beliebigen Namen haben.
import { PrimaryKey, MongoId } from '@deepkit/type';
class User {
id: MongoId & PrimaryKey = '';
}
Optional / Nullable
Optionale Felder werden mit title?: string
oder title: string | null
als TypeScript-Typ deklariert. Man sollte nur eine Variante davon verwenden, normalerweise die optionale ?
Syntax, die mit undefined
funktioniert.
Beide Varianten führen dazu, dass der Datenbank-Typ für alle SQL-Adapter NULLABLE
ist. Der einzige Unterschied zwischen diesen Dekoratoren ist also, dass sie unterschiedliche Werte zur Laufzeit darstellen.
Im folgenden Beispiel ist das geänderte Feld optional und kann daher zur Laufzeit undefiniert sein, obwohl es in der Datenbank immer als NULL dargestellt wird.
import { PrimaryKey } from '@deepkit/type';
class User {
id: number & PrimaryKey = 0;
modified?: Date;
}
Dieses Beispiel zeigt, wie der nullable Typ funktioniert. Sowohl in der Datenbank als auch in der Javascript-Laufzeit wird NULL verwendet. Dies ist ausführlicher als modified?: Date
und wird nicht häufig verwendet.
import { PrimaryKey } from '@deepkit/type';
class User {
id: number & PrimaryKey = 0;
modified: Date | null = null;
}
Database Type Mapping
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 |
Mit DatabaseField
ist es möglich, ein Feld auf einen beliebigen Datenbank-Typen zu mappen. Der Typ muss eine gültige SQL-Anweisung sein, die unverändert an das Migrationssystem übergeben wird.
import { DatabaseField } from '@deepkit/type';
interface User {
title: string & DatabaseField<{type: 'VARCHAR(244)'}>;
}
Um ein Feld für eine bestimmte Datenbank zu mappen, kann entweder SQLite
, MySQL
, oder Postgres
benutzt werden.
SQLite
import { SQLite } from '@deepkit/type';
interface User {
title: string & SQLite<{type: 'text'}>;
}
Session / Unit Of Work
Eine Session ist so etwas wie eine Arbeitseinheit. Sie verfolgt alles, was Sie tun, und hält die Änderungen automatisch fest, sobald commit()
aufgerufen wird. Es ist der bevorzugte Weg, um Änderungen in der Datenbank auszuführen, da es Anweisungen in einer Weise bündelt, die es sehr schnell macht. Eine Session ist sehr leichtgewichtig und kann zum Beispiel leicht in einem Request-Response-Lebenszyklus erstellt werden.
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();
Fügen Sie der Session mit session.add(T)
neue Instanz hinzu oder entfernen Sie bereits vorhandene Instanzen mit session.remove(T)
. Sobald Sie mit dem Session-Objekt fertig sind, dereferenzieren Sie es einfach überall, damit der Garbage-Collector es entfernen kann.
Änderungen werden bei Entity-Instanzen, die über das Session-Objekt geholt werden, automatisch erkannt.
const users = await session.query(User).find();
for (const user of users) {
user.name += ' changed';
}
await session.commit();//saves all users
Identity Map
Sessions bieten eine Identity-Map, die sicherstellt, dass es immer nur ein Javascript-Objekt pro Datenbank-Eintrag gibt. Wenn Sie zum Beispiel session.query(User).find()
zweimal innerhalb derselben Sitzung ausführen, erhalten Sie zwei verschiedene Arrays, aber mit denselben Entitätsinstanzen darin.
Wenn Sie mit session.add(entity1)
eine neue Entität hinzufügen und diese erneut abrufen, erhalten Sie genau dieselbe Entitätsinstanz entity1
.
Wichtig: Sobald Sie anfangen, Sessions zu verwenden, sollten Sie deren Methode Session.query
anstelle von Database.query
verwenden. Nur bei Session-Queries ist die Identitätszuordnungsfunktion aktiviert.
Query
Ein Query ist ein Objekt, das beschreibt, wie Daten aus der Datenbank abgerufen oder geändert werden sollen. Es hat mehrere Methoden um das Query zu beschreiben und Abbruchmethoden die diese ausführen. Der Datenbankadapter kann die Query-API auf viele Arten erweitern, um Datenbank spezifische Features zu unterstützen.
Sie können ein Query erstellen, indem Sie Database.query(T)
oder Session.query(T)
verwenden. Wir empfehlen Sessions da es die Leistung verbessert.
@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();
Filter
Ein Filter kann angewendet werden, um die Ergebnismenge einzuschränken.
//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
Um die Felder einzugrenzen, die von der Datenbank empfangen werden sollen, kann select('field1')
verwendet werden.
const user = await database.query(User).select('username').findOne();
const user = await database.query(User).select('id', 'username').findOne();
Wichtig dabei ist, dass sobald ein Eingrenzungen der Felder über select
stattfindet, die Ergebnisse keine Instanzen der Entity mehr sind, sondern lediglich Object-Literals.
const user = await database.query(User).select('username').findOne();
user instanceof User; //false
Order
Mit orderBy(field, order)
kann die Reihenfolge der Einträge geändert werden.
Es kann mehrere Male orderBy
ausgeführt werden, um die Reihenfolge immer weiter zu verfeinern.
const users = await session.query(User).orderBy('created', 'desc').find();
const users = await session.query(User).orderBy('created', 'asc').find();
Pagination
Mit den Methoden itemsPerPage()
und page()
können die Ergebnisse paginiert werden. Seite beginnt bei 1.
const users = await session.query(User).itemsPerPage(50).page(1).find();
Mit den alternativen Methoden limit
und skip
können Sie manuell paginieren.
const users = await session.query(User).limit(5).skip(10).find();
Join
Standardmäßig werden Referenzen aus der Entity weder in Abfragen einbezogen noch geladen. Um ein Join in die Abfrage aufzunehmen, ohne den Verweis zu laden, verwenden Sie join()
(left join) oder innerJoin()
. Um einen Join in die Abfrage aufzunehmen und die Referenz zu laden, verwenden Sie joinWith()
oder innerJoinWith()
.
Alle folgenden Beispiele gehen von diesen Modellschemata aus:
@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
}
Um Join-Abfragen zu ändern, verwenden Sie dieselben Methoden, jedoch mit dem use
-Präfix: useJoin
, useInnerJoin
, useJoinWith
oder useInnerJoinWith
. Um die Änderung der Join-Abfrage zu beenden, verwenden Sie end()
, um so die übergeordnete Abfrage zurückzubekommen.
//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
Mit Aggregationsmethoden können Sie Datensätze zählen und Felder aggregieren.
Die folgenden Beispiele gehen von diesem Modellschema aus:
@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
ermöglicht es, das Ergebnis nach dem angegebenen Feld zu gruppieren.
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();
Es gibt mehrere Aggregationsmethoden: withSum
, withAverage
, withCount
, withMin
, withMax
, withGroupConcat
. Jede erfordert einen Feldnamen als erstes Argument und ein optionales zweites Argument, um den Alias zu ändern.
// 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
Mit returning
können bei Änderungen via patch
und delete
zusätzliche Felder angefordert werden.
Vorsicht: Nicht in allen Datenbank-Adaptern sind die Felder atomar zurückgegeben. Verwenden Sie Transaktionen, um Datenkonsistenz sicherzustellen.
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}});
Find
Gibt ein Array an Einträgen zurück, die zu dem angegebenen Filter passen.
const users: User[] = await database.query(User).filter({username: 'Peter'}).find();
FindOne
Gibt ein Eintrag zurück, der zu dem angegebenen Filter passen.
Wird kein Eintrag gefunden, wird ein ItemNotFound
Fehler geworfen.
const users: User = await database.query(User).filter({username: 'Peter'}).findOne();
FindOneOrUndefined
Gibt ein Eintrag zurück, der zu dem angegebenen Filter passen. Wird kein Eintrag gefunden, wird undefined zurückgegeben.
const query = database.query(User).filter({username: 'Peter'});
const users: User|undefined = await query.findOneOrUndefined();
FindField
Gibt eine Liste eines Feldes zurück, der zu dem angegebenen Filter passen.
const usernames: string[] = await database.query(User).findField('username');
FindOneField
Gibt eine Liste eines Feldes zurück, der zu dem angegebenen Filter passen.
Wird kein Eintrag gefunden, wird ein ItemNotFound
Fehler geworfen.
const username: string = await database.query(User).filter({id: 3}).findOneField('username');
Patch
Patch ist eine Änderungsabfrage, die die in der Abfrage beschriebenen Datensätze patcht. Die Methoden
patchOne
und patchMany
beenden die Abfrage und führen den Patch aus.
patchMany
ändert alle Einträge in der Datenbank, die zu dem angegebenen Filter passen. Ist kein Filter gesetzt, wird die gesamte Tabelle geändert. Nutzen Sie patchOne
, um immer nur einen Eintrag zu verändern.
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
löscht alle Einträge in der Datenbank, die zu dem angegebenen Filter passen.
Ist kein Filter gesetzt, wird die gesamte Tabelle gelöscht. Nutzen Sie deleteOne
, um immer nur einene Eintrag zu löschen.
const result = await database.query(User)
.filter({visits: 0})
.deleteMany();
const result = await database.query(User).filter({id: 4}).deleteOne();
Has
Gibt zurück, ob mindestens ein Eintrag in der Datenbank existiert.
const userExists: boolean = await database.query(User).filter({username: 'Peter'}).has();
Count
Gibt die Anzahl der Einträge zurück.
const userCount: number = await database.query(User).count();
Lift
Das Lifting einer Abfrage bedeutet, dass ihr neue Funktionen hinzugefügt werden. Dies wird in der Regel entweder von Plugins oder komplexen Architekturen verwendet, um größere Abfrageklassen in mehrere praktische, wiederverwendbare Klassen aufzuteilen.
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();
Relations
Beziehungen ermöglichen es Ihnen, zwei Entitäten auf eine bestimmte Art und Weise zu verbinden. Dies geschieht in Datenbanken in der Regel über das Konzept der Fremdschlüssel. Deepkit ORM unterstützt Relationen für alle offiziellen Datenbankadapter.
Eine Relation wird mit dem Reference
-Dekorator annotiert. Normalerweise hat eine Relation auch eine umgekehrte Relation, die mit dem Typ BackReference
annotiert wird, aber nur benötigt wird, wenn die umgekehrte Relation in einer Datenbankabfrage verwendet werden soll. Rückreferenzen sind nur virtuell.
One To Many
Die Entität, die einen Verweis speichert, wird in der Regel als die "besitzende Seite" oder diejenige, die den Verweis "besitzt", bezeichnet. Der folgende Code zeigt zwei Entitäten mit einer One-To-Many-Beziehung zwischen User
und Post
. Das bedeutet, dass ein User
mehrere Post
haben kann. Die Entität Post
besitzt die Beziehung Post→User
. In der Datenbank selbst gibt es nun ein Feld Post."author"
, das den Primärschlüssel von User
enthält.
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();
Referenzen werden in Abfragen standardmäßig nicht ausgewählt. Siehe dazu Join.
Many To One
Ein Verweis hat in der Regel einen umgekehrten Verweis, der Many-to-One genannt wird. Es handelt sich nur um eine virtuelle Referenz, da sie nicht in der Datenbank selbst reflektiert wird. Eine Rückreferenz wird mit BackReference
annotiert und wird hauptsächlich für Reflection und Query Joins verwendet. Wenn Sie eine BackReference
von User
zu Post
hinzufügen, können Sie Post
direkt aus User
-Abfragen verbinden.
@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
Eine Many-to-many-Beziehung ermöglicht es Ihnen, viele Datensätze mit vielen anderen zu verbinden. Sie kann zum Beispiel für Benutzer in Gruppen verwendet werden. Ein Benutzer kann in keiner, einer oder vielen Gruppen sein. Folglich kann eine Gruppe 0, einen oder viele Benutzer enthalten.
Many-to-many-Beziehungen werden normalerweise über eine Pivot-Entität implementiert. Die Pivot-Entität enthält die eigentlichen eigenen Referenzen auf zwei andere Entitäten, und diese beiden Entitäten haben Rückreferenzen auf die Pivot-Entität.
@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,
) {
}
}
Mit diesen Entities können Sie nun Benutzer und Gruppen erstellen und sie mit der Pivot-Entität verbinden. Durch die Verwendung eines Rückverweises in User können wir die Gruppen direkt mit einer User-Abfrage abrufen.
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();
Um die Verknüpfung eines Benutzers mit einer Gruppe aufzuheben, wird der Datensatz der UserGroup gelöscht:
const users = await database.query(UserGroup)
.filter({user: user1, group: group1})
.deleteOne();
Events
Ereignisse sind eine Möglichkeit, sich in Deepkit ORM einzuklinken und ermöglichen es Ihnen, leistungsfähige Plugins zu schreiben. Es gibt zwei Kategorien von Ereignissen: Abfrage-Ereignisse und Unit-of-Work-Ereignisse. Plugin-Autoren verwenden in der Regel beide, um beide Möglichkeiten der Datenmanipulation zu unterstützen.
Events werden über Database.listen
un einem Event-Token registriert. Es kann auch kurzlebige Event-Listener auf Sessions registriert werden.
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
Abfrageereignisse werden ausgelöst, wenn eine Abfrage über Database.query()
oder Session.query()
ausgeführt wird.
Jedes Event hat seine eigenen zusätzlichen Eigenschaften wie den Typ der Entität, die Abfrage selbst und die Datenbanksitzung. Sie können die Abfrage überschreiben, indem Sie eine neue Abfrage auf Event.query
setzen.
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();
Query
hat dabei mehrere Event-Tokens:
Event-Token | Description |
---|---|
Query.onFetch |
|
Query.onDeletePre |
|
Query.onDeletePost |
|
Query.onPatchPre |
|
Query.onPatchPost |
Unit Of Work Events
Unit-of-Work-Ereignisse werden ausgelöst, wenn eine neue Session änderungen absetzt.
Event-Token | Description |
---|---|
DatabaseSession.onUpdatePre |
|
DatabaseSession.onUpdatePost |
|
DatabaseSession.onInsertPre |
|
DatabaseSession.onInsertPost |
|
DatabaseSession.onDeletePre |
|
DatabaseSession.onDeletePost |
|
DatabaseSession.onCommitPre |
Transactions
Eine Transaktion ist eine sequentielle Gruppe von Anweisungen, Abfragen oder Operationen wie Select, Insert, Update oder Delete, die als eine einzige Arbeitseinheit ausgeführt werden, die bestätigt oder rückgängig gemacht werden kann.
Deepkit unterstützt Transaktionen für alle offiziell unterstützten Datenbanken. Standardmäßig werden für jede Abfrage und Datenbanksitzung keine Transaktionen verwendet. Um Transaktionen zu aktivieren, gibt es zwei Hauptmethoden: Sessions und Callback.
Session Transactions
Sie können für jede erstellte Session eine neue Transaktion starten und zuweisen. Dies ist die bevorzugte Art der Interaktion mit der Datenbank, da Sie das Session-Objekt einfach weitergeben können und alle Abfragen, die von dieser Session instanziiert werden, automatisch seiner Transaktion zugewiesen werden.
Ein typisches Muster ist, alle Operationen in einen try-catch-Block zu verpacken und commit()
in der allerletzten Zeile auszuführen (das nur ausgeführt wird, wenn alle vorherigen Befehle erfolgreich waren) und rollback()
im catch-Block, um alle Änderungen zurückzunehmen sobald ein Fehler auftritt.
Obwohl es eine alternative API gibt (siehe unten), funktionieren alle Transaktionen nur mit Datenbanksitzungsobjekten. Um offene Änderungen aus der Unit-of-Work in einer Datenbanksitzung an die Datenbank zu übertragen, wird normalerweise commit()
aufgerufen. In einer transaktionalen Sitzung überträgt commit()
nicht nur alle ausstehenden Änderungen in die Datenbank, sondern schließt auch die Transaktion ab ("commits") und schließt damit die Transaktion. Alternativ können Sie session.flush()
aufrufen, um alle anstehenden Änderungen ohne Commit
und damit ohne Abschluss der Transaktion zu übertragen. Um eine Transaktion zu committen, ohne die Unit-of-Work zu leeren, verwenden Sie 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();
}
Sobald commit()
oder rollback()
in einer Session ausgeführt wird, wird die Transaktion freigegeben. Sie müssen dann useTransaction()
erneut aufrufen, wenn Sie in einer neuen Transaktion weiterarbeiten wollen.
Bitte beachten Sie, dass sobald die erste Datenbankoperation in einer transaktionalen Session ausgeführt wird, wird die zugewiesene Datenbankverbindung dem aktuellen Sitzungsobjekt fest und exklusiv zugewiesen (sticky). Somit werden alle nachfolgenden Operationen auf derselben Verbindung (und somit in den meisten Datenbanken auf demselben Datenbankserver) ausgeführt. Erst wenn entweder die transaktionale Session beendet wird (commit oder rollback), wird die Datenbankverbindung wieder freigegeben. Es ist daher zu empfehlen, eine Transaktion nur so kurz wie nötig zu halten.
Wenn eine Session bereits mit einer Transaktion verbunden ist, gibt ein Aufruf von session.useTransaction()
immer das gleiche Objekt zurück. Verwenden Sie session.isTransaction()
, um zu prüfen, ob der Sitzung eine Transaktion zugeordnet ist.
Verschachtelte Transaktionen werden nicht unterstützt.
Transaktion Callback
Eine Alternative zu transaktionalen Sessions ist 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);
});
Die Methode database.transaction(callback)
führt einen asynchronen Callback innerhalb einer neuen transaktionalen Session aus. Wenn der Callback erfolgreich ist (das heisst kein Fehler geworfen wird), wird die Session automatisch committed (und damit ihre Transaktion committed und alle Änderungen geleert). Wenn der Callback fehlschlägt, führt die Sitzung automatisch rollback()
aus, und der Fehler wird weitergeleitet.
Isolations
Viele Datenbanken unterstützen verschiedene Arten von Transaktionen. Um das Transaktionsverhalten zu ändern, können Sie verschiedene Methoden für das zurückgegebene Transaktionsobjekt von useTransaction()
aufrufen. Die Schnittstelle dieses Transaktionsobjekts hängt von dem verwendeten Datenbankadapter ab. Zum Beispiel hat das von einer MySQL-Datenbank zurückgegebene Transaktionsobjekt andere Optionen als das von einer MongoDB-Datenbank zurückgegebene. Verwenden Sie die Code-Vervollständigung oder sehen Sie sich die Schnittstelle des Datenbankadapters an, um eine Liste der möglichen Optionen zu erhalten.
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
});
Während Transaktionen für MySQL, PostgreSQL und SQLite standardmäßig funktionieren, müssen Sie MongoDB zunächst als "Replikatsatz" einrichten.
Um eine Standard-MongoDB-Instanz in ein Replikatset zu konvertieren, lesen Sie bitte die offizielle Dokumentation Convert a Standalone to a Replica Set.
Composite Primary Key
Composite Primary-Key bedeutet, eine Entität hat mehrere Primärschlüssel, die automatisch zu einem "zusammengesetzten Primärschlüssel" zusammengefasst werden. Diese Art der Modellierung der Datenbank hat Vor- und Nachteile. Wir sind der Meinung, dass zusammengesetzte Primärschlüssel enorme praktische Nachteile haben, die ihre Vorteile nicht rechtfertigen, sodass sie als schlechte Praxis betrachtet werden sollten und daher vermieden werden sollten. Deepkit ORM unterstützt keine zusammengesetzten Primärschlüssel. In diesem Kapitel erklären wir warum und zeigen (bessere) Alternativen auf.
Nachteile
Joins sind nicht trivial. Obwohl sie in RDBMS hochgradig optimiert sind, stellen sie in Anwendungen eine ständige Komplexität dar, die leicht aus dem Ruder laufen und zu Leistungsproblemen führen kann. Leistung nicht nur in Bezug auf die Ausführungszeit der Abfragen, sondern auch in Bezug auf die Entwicklungszeit.
Joins
Jeder einzelne Join wird komplizierter, je mehr Felder beteiligt sind. Während viele Datenbanken Optimierungen implementiert haben, um Joins mit mehreren Feldern nicht per se langsamer zu machen, erfordert es vom Entwickler, diese Joins ständig im Detail zu durchdenken, da z. B. das Vergessen von Schlüsseln zu subtilen Fehlern führen kann (da der Join auch ohne Angabe aller Schlüssel funktioniert) und der Entwickler daher die vollständige zusammengesetzte Primärschlüsselstruktur kennen muss.
Indizes
Indizes mit mehreren Feldern (die zusammengesetzte Primärschlüssel sind) leiden unter dem Problem der Feldreihenfolge in Abfragen. Während Datenbanksysteme bestimmte Abfragen optimieren können, ist es bei komplexen Strukturen schwierig, effiziente Operationen zu schreiben, die alle definierten Indizes korrekt nutzen. Bei einem Index mit mehreren Feldern (wie einem zusammengesetzten Primärschlüssel) ist es normalerweise erforderlich, die Felder in der richtigen Reihenfolge zu definieren, damit die Datenbank den Index tatsächlich verwenden kann. Wenn die Reihenfolge nicht korrekt angegeben ist (z. B. in einer WHERE-Klausel), kann dies leicht dazu führen, dass die Datenbank den Index überhaupt nicht verwendet und stattdessen eine vollständige Tabellendurchsuchung durchführt. Zu wissen, welche Datenbank-Abfrage auf welche Weise optimiert, ist ein fortgeschrittenes Wissen, über das neue Entwickler in der Regel nicht verfügen, das aber erforderlich ist, sobald Sie mit zusammengesetzten Primärschlüsseln arbeiten, damit Sie das Beste aus Ihrer Datenbank herausholen und keine Ressourcen verschwenden.
Migrationen
Sobald Sie entscheiden, dass eine bestimmte Entität ein zusätzliches Feld zur eindeutigen Identifizierung (und damit zum Composite Primary Key wird) benötigt, führt dies zur Anpassung aller Entitäten in Ihrer Datenbank, die Beziehungen zu dieser Entität haben.
Nehmen wir an, Sie haben z. B. eine Entität User
mit zusammengesetztem Primärschlüssel und beschließen, in verschiedenen Tabellen einen Fremdschlüssel zu diesem User
zu verwenden, z. B. in einer Pivot-Tabelle audit_log
, groups
und posts
. Sobald Sie den Primärschlüssel von User
ändern, müssen alle diese Tabellen in einer Migration ebenfalls angepasst werden.
Dies macht Migrationsdateien nicht nur viel komplexer, sondern kann auch zu größeren Ausfallzeiten bei der Ausführung von Migrationsdateien führen, da Schemaänderungen in der Regel entweder eine vollständige Datenbanksperre oder zumindest eine Tabellensperre erfordern. Je mehr Tabellen von einer großen Änderung wie einer Indexänderung betroffen sind, desto länger dauert die Migration. Und je größer eine Tabelle ist, desto länger dauert die Migration.
Denken Sie an die Tabelle audit_log
. Solche Tabellen haben in der Regel viele Datensätze (etwa Millionen), und Sie müssen sie bei einer Schemaänderung nur deshalb anfassen, weil Sie beschlossen haben, einen zusammengesetzten Primärschlüssel zu verwenden und dem Primärschlüssel von User
ein zusätzliches Feld hinzuzufügen. Je nach Größe all dieser Tabellen werden Migrationsänderungen dadurch entweder unnötig teurer oder in einigen Fällen sogar so teuer, dass eine Änderung des Primärschlüssels von User
finanziell nicht mehr vertretbar ist. Dies führt in der Regel zu Umgehungslösungen (z. B. Hinzufügen eines eindeutigen Indexes zur Benutzertabelle), die zu technischen Schulden führen und früher oder später auf der Liste der Altlasten landen.
Bei großen Projekten kann dies zu enormen Ausfallzeiten führen (von Minuten bis Stunden) und manchmal sogar zur Einführung eines völlig neuen Migrationsabstraktionssystems, das im Wesentlichen Tabellen kopiert, Datensätze in Geistertabellen einfügt und nach der Migration Tabellen hin und her verschiebt. Diese zusätzliche Komplexität wird wiederum jeder Entität aufgezwungen, die eine Beziehung zu einer anderen Entität mit einem zusammengesetzten Primärschlüssel hat, und wird umso größer, je größer Ihre Datenbankstruktur wird. Das Problem wird immer schlimmer, ohne dass es eine Möglichkeit gibt, es zu lösen (außer durch die vollständige Entfernung des zusammengesetzten Primärschlüssels).
Auffindbarkeit
Wenn Sie Datenbankadministrator oder Data Engineer/Scientist sind, arbeiten Sie in der Regel direkt an der Datenbank und erkunden die Daten, wenn Sie sie brauchen. Bei zusammengesetzten Primärschlüsseln muss jeder Benutzer, der SQL direkt schreibt, von allen beteiligten Tabellen den richtigen Primärschlüssel kennen (und die Spaltenreihenfolge, um korrekte Indexoptimierungen zu erhalten). Dieser zusätzliche Overhead erschwert nicht nur die Untersuchung von Daten, die Erstellung von Berichten usw., sondern kann auch zu Fehlern in älterem SQL führen, wenn ein zusammengesetzter Primärschlüssel plötzlich geändert wird. Das alte SQL ist wahrscheinlich immer noch gültig und läuft einwandfrei, liefert aber plötzlich falsche Ergebnisse, da das neue Feld im zusammengesetzten Primärschlüssel in der Verknüpfung fehlt. Es ist hierbei viel einfacher, lediglich einen Primärschlüssel zu haben. Dies erleichtert die Auffindbarkeit von Daten und stellt sicher, dass alte SQL-Abfragen auch dann noch korrekt funktionieren, wenn Sie sich entscheiden, die Art und Weise zu ändern, wie zum Beispiel ein Benutzerobjekt eindeutig identifiziert wird.
Überarbeitung
Sobald ein zusammengesetzter Primärschlüssel in einer Entität verwendet wird, kann ein Refactoring des Schlüssels zu einem erheblichen zusätzlichen Refactoring führen. Da eine Entität mit einem zusammengesetzten Primärschlüssel in der Regel kein einzelnes eindeutiges Feld hat, müssen alle Filter und Verknüpfungen alle Werte des zusammengesetzten Schlüssels enthalten. Das bedeutet in der Regel, dass der Code auf die Kenntnis des zusammengesetzten Primärschlüssels angewiesen ist, sodass alle Felder abgerufen werden müssen (z. B. für URLs wie /user/:key1/:key2). Sobald dieser Schlüssel geändert wird, müssen alle Stellen, an denen dieses Wissen explizit verwendet wird, wie URLs, benutzerdefinierte SQL-Abfragen und andere Stellen, umgeschrieben werden.
Während ORMs in der Regel Joins automatisch erstellen, ohne die Werte manuell zu spezifizieren, können sie nicht automatisch das Refactoring für alle anderen Anwendungsfälle wie URL-Strukturen oder benutzerdefinierte SQL-Abfragen abdecken, und vor allem nicht für Stellen, an denen das ORM gar nicht verwendet wird, wie in Berichtssystemen und allen externen Systemen.
ORM-Komplexität
Durch die Unterstützung von zusammengesetzten Primärschlüsseln steigt die Komplexität des Codes eines leistungsstarken ORM wie Deepkit ORM enorm an. Nicht nur, dass der Code und die Wartung komplexer und damit teurer werden, es werden auch mehr Edge-Cases von Benutzern auftreten, die behoben und gewartet werden müssen. Die Komplexität der Abfrageschicht, der Änderungserkennung, des Migrationssystems, der internen Verfolgung von Beziehungen usw. nimmt erheblich zu. Die Gesamtkosten, die mit dem Aufbau und der Unterstützung eines ORM mit zusammengesetzten Primärschlüsseln verbunden sind, sind alles in allem zu hoch und nicht zu rechtfertigen, weshalb Deepkit dies nicht unterstützt.
Vorteile
Abgesehen davon haben zusammengesetzte Primärschlüssel auch Vorteile, wenn auch nur sehr oberflächliche. Durch die Verwendung einer möglichst geringen Anzahl von Indizes für jede Tabelle wird das Schreiben (Einfügen/Aktualisieren) von Daten effizienter, da weniger Indizes gepflegt werden müssen. Außerdem wird die Struktur des Modells etwas sauberer (da es normalerweise eine Spalte weniger hat). Der Unterschied zwischen einem sequentiell geordneten, automatisch inkrementierenden Primärschlüssel und einem nicht inkrementierenden Primärschlüssel ist heutzutage jedoch völlig vernachlässigbar, da Festplattenplatz billig ist und der Vorgang in der Regel nur ein "Append-Only"-Vorgang ist, der sehr schnell ist.
Es mag sicherlich ein paar Randfälle geben (und für ein paar sehr spezifische Datenbanksysteme), in denen es zunächst besser ist, mit zusammengesetzten Primärschlüsseln zu arbeiten. Aber selbst in diesen Systemen könnte es insgesamt (unter Berücksichtigung aller Kosten) sinnvoller sein, sie nicht zu verwenden und zu einer anderen Strategie zu wechseln.
Alternative
Eine Alternative zu zusammengesetzten Primärschlüsseln ist die Verwendung eines einzigen automatisch inkrementierenden numerischen Primärschlüssels, in der Regel "id" genannt, und die Verlagerung des zusammengesetzten Primärschlüssels in einen eindeutigen Index mit mehreren Feldern. Je nach verwendetem Primärschlüssel (abhängig von der erwarteten Zeilenzahl) verwendet die "id" entweder 4 oder 8 Bytes pro Datensatz.
Durch den Einsatz dieser Strategie ist man nicht mehr gezwungen, über die oben beschriebenen Probleme nachzudenken und eine Lösung zu finden, was die Kosten für immer größer werdende Projekte enorm senkt.
Die Strategie bedeutet konkret, dass jede Entität ein "id"-Feld hat, normalerweise ganz am Anfang, und dieses Feld wird dann verwendet, um standardmäßig eindeutige Zeilen und in Joins zu identifizieren.
class User {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(public username: string) {}
}
Als Alternative zu einem zusammengesetzten Primärschlüssel würden Sie stattdessen einen eindeutigen Mehrfeldindex verwenden.
@entity.index(['tenancyId', 'username'], {unique: true})
class User {
id: number & PrimaryKey & AutoIncrement = 0;
constructor(
public tenancyId: number,
public username: string,
) {}
}
Deepkit ORM unterstützt automatisch inkrementelle Primärschlüssel, auch für MongoDB. Dies ist die bevorzugte Methode zur Identifizierung von Datensätzen in Ihrer Datenbank. Für MongoDB können Sie jedoch die ObjectId (_id: MongoId & PrimaryKey = ''
) als einfachen Primärschlüssel verwenden. Eine Alternative zum numerischen, automatisch inkrementierenden Primärschlüssel ist eine UUID, die ebenso gut funktioniert (jedoch etwas andere Leistungsmerkmale aufweist, da die Indexierung teurer ist).
Zusammenfassung
Zusammengesetzte Primärschlüssel bedeuten im Wesentlichen, dass nach ihrer Einführung alle künftigen Änderungen und die praktische Verwendung mit wesentlich höheren Kosten verbunden sind. Während es zu Beginn wie eine saubere Architektur aussieht (weil man eine Spalte weniger hat), führt es zu erheblichen praktischen Kosten, sobald das Projekt tatsächlich entwickelt wird, und die Kosten steigen weiter, je größer das Projekt wird.
Betrachtet man die Asymmetrien zwischen Vor- und Nachteilen, so wird deutlich, dass zusammengesetzte Primärschlüssel in den meisten Fällen nicht zu rechtfertigen sind. Die Kosten sind viel größer als der Nutzen. Nicht nur für Sie als Benutzer, sondern auch für uns als Autor und Betreuer des ORM-Codes. Aus diesem Grund unterstützt Deepkit ORM keine zusammengesetzten Primärschlüssel.
Plugins
Soft-Delete
Das Soft-Delete Plugin ermöglicht es, Datenbankeinträge versteckt zu halten, ohne sie tatsächlich zu löschen. Wenn ein Datensatz gelöscht wird, wird er nur als gelöscht markiert und nicht wirklich gelöscht. Alle Abfragen filtern automatisch nach dieser gelöschten Eigenschaft, sodass es sich für den Benutzer so anfühlt, als ob er tatsächlich gelöscht wäre.
Um das Plugin zu verwenden, müssen Sie die SoftDelete-Klasse instanziieren und sie für jede Entität aktivieren.
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);
Löschen
Um Datensätze sanft zu löschen, verwenden Sie die üblichen Methoden: deleteOne
oder deleteMany
in einer Abfrage, oder Sie verwenden die Session, um sie zu löschen. Das Soft-Delete Plugin erledigt den Rest automatisch im Hintergrund.
Wiederherstellen
Gelöschte Datensätze können mithilfe einer aufgehobenen Abfrage über SoftDeleteQuery
wiederhergestellt werden. Es hat restoreOne
und 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();
Die Session unterstützt auch die Wiederherstellung von Elementen.
import { SoftDeleteSession } from '@deepkit/orm';
const session = database.createSession();
const user1 = session.query(User).findOne();
session.from(SoftDeleteSession).restore(user1);
await session.commit();
Hard Delete
Um Datensätze hart zu löschen, verwenden Sie eine gehobene Abfrage über SoftDeleteQuery. Dies stellt im Wesentlichen das alte Verhalten ohne das Plugin für eine einzelne Abfrage wieder her.
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();
Query deleted.
Bei einem "lifted" Query über SoftDeleteQuery
können Sie auch gelöschte Datensätze einbeziehen.
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()
Deleted by
deletedBy
kann über Abfrage und Sessions festgelegt werden.
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();