當我們的應用面對數據庫連接時,選擇一個好用的orm框架是非常重要的,他可以爲你解決sql注入,數據庫切換,數據模型遷移等等問題,也會爲你提供易讀的優雅的語法,讓你告別拼接sql語句
typeorm 作爲對typescript支持度最好的orm框架除了擁有這些優勢外,還提供了緩存
,關係
,日誌
等等開箱即用的功能,使用typescript
的querybuilder
可以模擬出任何複雜的sql語句並且不會丟失返回數據類型,並提供query
方法直接執行sql語句,來滿足對舊有sql語句的遷移,
typescript
可以運行在 NodeJS、Browser、Cordova、PhoneGap、Ionic、React Native、Expo 和 Electron
平臺上,並且官方支持MySQL / MariaDB / Postgres / SQLite / Microsoft SQL Server / Oracle / sql.js / mongodb
本文對typeorm
源碼的解析主要從幾個疑問上切入
-
synchronize:true
時數據模型同步原理,以及爲什麼在生產環境時不能使用 - reposity.save()的執行過程,在
cascade: true
時是如何自動將關係一起保存的,有數據時更新,無數據時插入是如何實現的 - 在查詢
relations
時是否會有性能問題,關係是如何維持的, -
queryBuilder
是如何構造出各種複雜的sql語句的
準備階段
我們可以直接clone typeorm官方倉庫代碼,官方倉庫包含sample
目錄,裏面是每個功能的示例代碼,我們直接對這裏面的代碼進行debug,來分析每個功能的源碼實現
git clone https://github.com/typeorm/typeorm
cd typeorm
npm install
npm run compile
vscode debug功能 launch.json
{
// 使用 IntelliSense 瞭解相關屬性。
// 懸停以查看現有屬性的描述。
// 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**",
"${workspaceRoot}/node_modules/**/*.js"
],
"program": "${workspaceFolder}/sample/sample3-many-to-one/app.ts",
//"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/build/compiled/**/*.js"]
}
]
}
此例是對sample
下的 sample3-many-to-one/app.ts
進行調試,我們先看下主要代碼
createConnection(options).then(connection => {
let details = new PostDetails();
details.authorName = "Umed";
details.comment = "about post";
details.metadata = "post,details,one-to-one";
let post = new Post();
post.text = "Hello how are you?";
post.title = "hello";
post.details = details;
let postRepository = connection.getRepository(Post);
postRepository
.save(post)
.then(post => console.log("Post has been saved"))
.catch(error => console.log("Cannot save. Error: ", error));
}).catch(error => console.log("Error: ", error));
Post
實體模型中通過many-to-one
保存對PostDetail
的引用,非常經典的多對一關係,我們以此示例代碼來分析本文中所要完成的四個問題
synchronize原理
數據庫模型的同步發生在我們剛剛連接到數據庫時,我們將debug 斷點斷到createConnection(options)
處,主要執行的是connect
代碼
async connect(): Promise<this> {
if (this.isConnected)
throw new CannotConnectAlreadyConnectedError(this.name);
// connect to the database via its driver
await this.driver.connect();
// connect to the cache-specific database if cache is enabled
if (this.queryResultCache)
await this.queryResultCache.connect();
// set connected status for the current connection
ObjectUtils.assign(this, { isConnected: true });
try {
// build all metadatas registered in the current connection
this.buildMetadatas();
await this.driver.afterConnect();
// if option is set - drop schema once connection is done
if (this.options.dropSchema)
await this.dropDatabase();
// if option is set - automatically synchronize a schema
if (this.options.synchronize)
await this.synchronize();
// if option is set - automatically synchronize a schema
if (this.options.migrationsRun)
await this.runMigrations({ transaction: this.options.migrationsTransactionMode });
} catch (error) {
// if for some reason build metadata fail (for example validation error during entity metadata check)
// connection needs to be closed
await this.close();
throw error;
}
return this;
}
this.driver
是connect的對象初始化時獲得的,不同的數據庫使用不同的driver。比如mysql
使用的是mysql
nodejs包,然後分別連接到配置文件中配置的數據庫和存儲查詢緩存的緩存數據庫,接下來有一個很關鍵的邏輯buildMetadatas
,爲所有我們通過@entity
,@colomn
等裝飾器定義的表和字段初始化元數據,函數裏主要執行
connectionMetadataBuilder.buildEntityMetadatas(this.options.entities || [])
,接下來是new EntityMetadataBuilder(this.connection,getMetadataArgsStorage()).build(allEntityClasses)
,getMetadataArgsStorage()
是獲取到我們通過裝飾器@Entity() 和 @Column()
等定義的實體和屬性,然後重點是build的執行,我們進入build
build(entityClasses?: Function[]): EntityMetadata[] {
// if entity classes to filter entities by are given then do filtering, otherwise use all
const allTables = entityClasses ? this.metadataArgsStorage.filterTables(entityClasses) : this.metadataArgsStorage.tables;
// filter out table metadata args for those we really create entity metadatas and tables in the db
const realTables = allTables.filter(table => table.type === "regular" || table.type === "closure" || table.type === "entity-child" || table.type === "view");
// create entity metadatas for a user defined entities (marked with @Entity decorator or loaded from entity schemas)
const entityMetadatas = realTables.map(tableArgs => this.createEntityMetadata(tableArgs));
// compute parent entity metadatas for table inheritance
entityMetadatas.forEach(entityMetadata => this.computeParentEntityMetadata(entityMetadatas, entityMetadata));
// after all metadatas created we set child entity metadatas for table inheritance
entityMetadatas.forEach(metadata => {
metadata.childEntityMetadatas = entityMetadatas.filter(childMetadata => {
return metadata.target instanceof Function
&& childMetadata.target instanceof Function
&& MetadataUtils.isInherited(childMetadata.target, metadata.target);
});
});
// build entity metadata (step0), first for non-single-table-inherited entity metadatas (dependant)
entityMetadatas
.filter(entityMetadata => entityMetadata.tableType !== "entity-child")
.forEach(entityMetadata => entityMetadata.build());
// build entity metadata (step0), now for single-table-inherited entity metadatas (dependant)
entityMetadatas
.filter(entityMetadata => entityMetadata.tableType === "entity-child")
.forEach(entityMetadata => entityMetadata.build());
// compute entity metadata columns, relations, etc. first for the regular, non-single-table-inherited entity metadatas
entityMetadatas
.filter(entityMetadata => entityMetadata.tableType !== "entity-child")
.forEach(entityMetadata => this.computeEntityMetadataStep1(entityMetadatas, entityMetadata));
// then do it for single table inheritance children (since they are depend on their parents to be built)
entityMetadatas
.filter(entityMetadata => entityMetadata.tableType === "entity-child")
.forEach(entityMetadata => this.computeEntityMetadataStep1(entityMetadatas, entityMetadata));
// calculate entity metadata computed properties and all its sub-metadatas
entityMetadatas.forEach(entityMetadata => this.computeEntityMetadataStep2(entityMetadata));
// calculate entity metadata's inverse properties
entityMetadatas.forEach(entityMetadata => this.computeInverseProperties(entityMetadata, entityMetadatas));
// go through all entity metadatas and create foreign keys / junction entity metadatas for their relations
entityMetadatas
.filter(entityMetadata => entityMetadata.tableType !== "entity-child")
.forEach(entityMetadata => {
// create entity's relations join columns (for many-to-one and one-to-one owner)
entityMetadata.relations.filter(relation => relation.isOneToOne || relation.isManyToOne).forEach(relation => {
const joinColumns = this.metadataArgsStorage.filterJoinColumns(relation.target, relation.propertyName);
const { foreignKey, columns, uniqueConstraint } = this.relationJoinColumnBuilder.build(joinColumns, relation); // create a foreign key based on its metadata args
if (foreignKey) {
relation.registerForeignKeys(foreignKey); // push it to the relation and thus register there a join column
entityMetadata.foreignKeys.push(foreignKey);
}
if (columns) {
relation.registerJoinColumns(columns);
}
if (uniqueConstraint) {
if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof AuroraDataApiDriver
|| this.connection.driver instanceof SqlServerDriver || this.connection.driver instanceof SapDriver) {
const index = new IndexMetadata({
entityMetadata: uniqueConstraint.entityMetadata,
columns: uniqueConstraint.columns,
args: {
target: uniqueConstraint.target!,
name: uniqueConstraint.name,
unique: true,
synchronize: true
}
});
if (this.connection.driver instanceof SqlServerDriver) {
index.where = index.columns.map(column => {
return `${this.connection.driver.escape(column.databaseName)} IS NOT NULL`;
}).join(" AND ");
}
if (relation.embeddedMetadata) {
relation.embeddedMetadata.indices.push(index);
} else {
relation.entityMetadata.ownIndices.push(index);
}
this.computeEntityMetadataStep2(entityMetadata);
} else {
if (relation.embeddedMetadata) {
relation.embeddedMetadata.uniques.push(uniqueConstraint);
} else {
relation.entityMetadata.ownUniques.push(uniqueConstraint);
}
this.computeEntityMetadataStep2(entityMetadata);
}
}
if (foreignKey && this.connection.driver instanceof CockroachDriver) {
const index = new IndexMetadata({
entityMetadata: relation.entityMetadata,
columns: foreignKey.columns,
args: {
target: relation.entityMetadata.target!,
synchronize: true
}
});
if (relation.embeddedMetadata) {
relation.embeddedMetadata.indices.push(index);
} else {
relation.entityMetadata.ownIndices.push(index);
}
this.computeEntityMetadataStep2(entityMetadata);
}
});
// create junction entity metadatas for entity many-to-many relations
entityMetadata.relations.filter(relation => relation.isManyToMany).forEach(relation => {
const joinTable = this.metadataArgsStorage.findJoinTable(relation.target, relation.propertyName)!;
if (!joinTable) return; // no join table set - no need to do anything (it means this is many-to-many inverse side)
// here we create a junction entity metadata for a new junction table of many-to-many relation
const junctionEntityMetadata = this.junctionEntityMetadataBuilder.build(relation, joinTable);
relation.registerForeignKeys(...junctionEntityMetadata.foreignKeys);
relation.registerJoinColumns(
junctionEntityMetadata.ownIndices[0].columns,
junctionEntityMetadata.ownIndices[1].columns
);
relation.registerJunctionEntityMetadata(junctionEntityMetadata);
// compute new entity metadata properties and push it to entity metadatas pool
this.computeEntityMetadataStep2(junctionEntityMetadata);
this.computeInverseProperties(junctionEntityMetadata, entityMetadatas);
entityMetadatas.push(junctionEntityMetadata);
});
});
// update entity metadata depend properties
entityMetadatas
.forEach(entityMetadata => {
entityMetadata.relationsWithJoinColumns = entityMetadata.relations.filter(relation => relation.isWithJoinColumn);
entityMetadata.hasNonNullableRelations = entityMetadata.relationsWithJoinColumns.some(relation => !relation.isNullable || relation.isPrimary);
});
// generate closure junction tables for all closure tables
entityMetadatas
.filter(metadata => metadata.treeType === "closure-table")
.forEach(entityMetadata => {
const closureJunctionEntityMetadata = this.closureJunctionEntityMetadataBuilder.build(entityMetadata);
entityMetadata.closureJunctionTable = closureJunctionEntityMetadata;
this.computeEntityMetadataStep2(closureJunctionEntityMetadata);
this.computeInverseProperties(closureJunctionEntityMetadata, entityMetadatas);
entityMetadatas.push(closureJunctionEntityMetadata);
});
// generate keys for tables with single-table inheritance
entityMetadatas
.filter(metadata => metadata.inheritancePattern === "STI" && metadata.discriminatorColumn)
.forEach(entityMetadata => this.createKeysForTableInheritance(entityMetadata));
// build all indices (need to do it after relations and their join columns are built)
entityMetadatas.forEach(entityMetadata => {
entityMetadata.indices.forEach(index => index.build(this.connection.namingStrategy));
});
// build all unique constraints (need to do it after relations and their join columns are built)
entityMetadatas.forEach(entityMetadata => {
entityMetadata.uniques.forEach(unique => unique.build(this.connection.namingStrategy));
});
// build all check constraints
entityMetadatas.forEach(entityMetadata => {
entityMetadata.checks.forEach(check => check.build(this.connection.namingStrategy));
});
// build all exclusion constraints
entityMetadatas.forEach(entityMetadata => {
entityMetadata.exclusions.forEach(exclusion => exclusion.build(this.connection.namingStrategy));
});
// add lazy initializer for entity relations
entityMetadatas
.filter(metadata => metadata.target instanceof Function)
.forEach(entityMetadata => {
entityMetadata.relations
.filter(relation => relation.isLazy)
.forEach(relation => {
this.connection.relationLoader.enableLazyLoad(relation, (entityMetadata.target as Function).prototype);
});
});
entityMetadatas.forEach(entityMetadata => {
entityMetadata.columns.forEach(column => {
// const target = column.embeddedMetadata ? column.embeddedMetadata.type : column.target;
const generated = this.metadataArgsStorage.findGenerated(column.target, column.propertyName);
if (generated) {
column.isGenerated = true;
column.generationStrategy = generated.strategy;
if (generated.strategy === "uuid") {
column.type = "uuid";
} else if (generated.strategy === "rowid") {
column.type = "int";
} else {
column.type = column.type || Number;
}
column.build(this.connection);
this.computeEntityMetadataStep2(entityMetadata);
}
});
});
return entityMetadatas;
}
這裏的邏輯很多,每個forEach
都有自己的邏輯,例如獲得表格的名字,獲取他的關係等等,最終構造出來一個EntityMetadata
類,,他包含巨多的屬性,我們debug到這裏時可以查看一下每個字段,也可以直接看他的class
定義,每個屬性的值都來源於我們通過typeorm
提供的各種裝飾器定義,最終構造出來的metadatas將存在於全局,並在各個邏輯中被頻繁使用,構造完metadatas
後,我們可以看到有對synchronize
的判斷
if (this.options.synchronize)
await this.synchronize();
當我們啓用synchronize
,會直接執行this.synchronize()
,
async synchronize(dropBeforeSync: boolean = false): Promise<void> {
if (!this.isConnected)
throw new CannotExecuteNotConnectedError(this.name);
if (dropBeforeSync)
await this.dropDatabase();
const schemaBuilder = this.driver.createSchemaBuilder();
await schemaBuilder.build();
}
主要執行了await schemaBuilder.build()
,mongodb 和 其他關係型有不一樣的構建邏輯,我們關注一下關係型數據庫
async build(): Promise<void> {
this.queryRunner = this.connection.createQueryRunner();
// CockroachDB implements asynchronous schema sync operations which can not been executed in transaction.
// E.g. if you try to DROP column and ADD it again in the same transaction, crdb throws error.
const isUsingTransactions = (
!(this.connection.driver instanceof CockroachDriver) &&
this.connection.options.migrationsTransactionMode !== "none"
);
if (isUsingTransactions) {
await this.queryRunner.startTransaction();
}
try {
const tablePaths = this.entityToSyncMetadatas.map(metadata => metadata.tablePath);
// TODO: typeorm_metadata table needs only for Views for now.
// Remove condition or add new conditions if necessary (for CHECK constraints for example).
if (this.viewEntityToSyncMetadatas.length > 0)
await this.createTypeormMetadataTable();
await this.queryRunner.getTables(tablePaths);
await this.queryRunner.getViews([]);
await this.executeSchemaSyncOperationsInProperOrder();
// if cache is enabled then perform cache-synchronization as well
if (this.connection.queryResultCache)
await this.connection.queryResultCache.synchronize(this.queryRunner);
if (isUsingTransactions) {
await this.queryRunner.commitTransaction();
}
} catch (error) {
try { // we throw original error even if rollback thrown an error
if (isUsingTransactions) {
await this.queryRunner.rollbackTransaction();
}
} catch (rollbackError) { }
throw error;
} finally {
await this.queryRunner.release();
}
}
重點執行了await this.queryRunner.getTables(tablePaths)
,裏面重點執行了loadTables
,通過查詢關係型數據庫INFORMATION_SCHEMA
表,來獲取到所有的表的信息,包括名稱,主外鍵,字段類型,字段大小等等等。。保存在loadedTables
中,然後getTables
執行完畢,接下來則是真正的數據庫結構同步邏輯,executeSchemaSyncOperationsInProperOrder()
await this.dropOldViews();
await this.dropOldForeignKeys();
await this.dropOldIndices();
await this.dropOldChecks();
await this.dropOldExclusions();
await this.dropCompositeUniqueConstraints();
// await this.renameTables();
await this.renameColumns();
await this.createNewTables();
await this.dropRemovedColumns();
await this.addNewColumns();
await this.updatePrimaryKeys();
await this.updateExistColumns();
await this.createNewIndices();
await this.createNewChecks();
await this.createNewExclusions();
await this.createCompositeUniqueConstraints();
await this.createForeignKeys();
await this.createViews();
根據名字就可以看到,刪除舊錶,刪除舊外鍵,刪除舊索引,添加字段,添加表等等等,我們挑選createNewTables()
來看一下,
protected async createNewTables(): Promise<void> {
const currentSchema = await this.queryRunner.getCurrentSchema();
for (const metadata of this.entityToSyncMetadatas) {
// check if table does not exist yet
const existTable = this.queryRunner.loadedTables.find(table => {
const database = metadata.database && metadata.database !== this.connection.driver.database ? metadata.database : undefined;
let schema = metadata.schema || (<SqlServerDriver|PostgresDriver|SapDriver>this.connection.driver).options.schema;
// if schema is default db schema (e.g. "public" in PostgreSQL), skip it.
schema = schema === currentSchema ? undefined : schema;
const fullTableName = this.connection.driver.buildTableName(metadata.tableName, schema, database);
return table.name === fullTableName;
});
if (existTable)
continue;
this.connection.logger.logSchemaBuild(`creating a new table: ${metadata.tablePath}`);
// create a new table and sync it in the database
const table = Table.create(metadata, this.connection.driver);
await this.queryRunner.createTable(table, false, false);
this.queryRunner.loadedTables.push(table);
}
}
循環遍歷this.entityToSyncMetadatas
,即我們上文提到的構建的通過各種裝飾器定義的所有表的元屬性,接下來在我們剛剛得到的loadedTables
中find
每一個metadata
的table
,如果找到了,繼續循環,未找到說明數據庫中還沒有此表格,那麼接下來執行新建表格的sql語句。
通過一個createNewTables()
邏輯的分析,可以看到,就是通過將數據庫中真正表格的狀況和我們通過裝飾器定義的各種表格的元屬性進行對比,來判斷是插入還是刪除還是更新。然後直接執行對應的sql語句,所以,如果我們修改了一個字段的名稱,可能會執行的語句是刪除掉舊的字段,增加新的字段,而不是通過alter
修改字段的名稱,所以會導致舊的字段的所有數據全部丟失,所以生產環境要慎用數據庫模型同步synchronize:true
,如果真正的要修改字段名, typeorm
爲我們提供了數據遷移的功能,通過編寫數據遷移腳本,可以安全的進行數據遷移,並且可以按照版本回滾,非常人性化。
save執行過程
斷點斷到postRepository.save(post)
處
save<Entity, T extends DeepPartial<Entity>>(targetOrEntity: (T|T[])|EntityTarget<Entity>, maybeEntityOrOptions?: T|T[], maybeOptions?: SaveOptions): Promise<T|T[]> {
// normalize mixed parameters
let target = (arguments.length > 1 && (targetOrEntity instanceof Function || targetOrEntity instanceof EntitySchema || typeof targetOrEntity === "string")) ? targetOrEntity as Function|string : undefined;
const entity: T|T[] = target ? maybeEntityOrOptions as T|T[] : targetOrEntity as T|T[];
const options = target ? maybeOptions : maybeEntityOrOptions as SaveOptions;
if (target instanceof EntitySchema)
target = target.options.name;
// if user passed empty array of entities then we don't need to do anything
if (Array.isArray(entity) && entity.length === 0)
return Promise.resolve(entity);
// execute save operation
return new EntityPersistExecutor(this.connection, this.queryRunner, "save", target, entity, options)
.execute()
.then(() => entity);
}
主要執行了EntityPersistExecutor().execute()
方法,主要內容是
const executors = await Promise.all(entitiesInChunks.map(async entities => {
const subjects: Subject[] = [];
// create subjects for all entities we received for the persistence
entities.forEach(entity => {
const entityTarget = this.target ? this.target : entity.constructor;
if (entityTarget === Object)
throw new CannotDetermineEntityError(this.mode);
subjects.push(new Subject({
metadata: this.connection.getMetadata(entityTarget),
entity: entity,
canBeInserted: this.mode === "save",
canBeUpdated: this.mode === "save",
mustBeRemoved: this.mode === "remove",
canBeSoftRemoved: this.mode === "soft-remove",
canBeRecovered: this.mode === "recover"
}));
});
// console.time("building cascades...");
// go through each entity with metadata and create subjects and subjects by cascades for them
const cascadesSubjectBuilder = new CascadesSubjectBuilder(subjects);
subjects.forEach(subject => {
// next step we build list of subjects we will operate with
// these subjects are subjects that we need to insert or update alongside with main persisted entity
cascadesSubjectBuilder.build(subject, this.mode);
});
// console.timeEnd("building cascades...");
// load database entities for all subjects we have
// next step is to load database entities for all operate subjects
// console.time("loading...");
await new SubjectDatabaseEntityLoader(queryRunner, subjects).load(this.mode);
// console.timeEnd("loading...");
// console.time("other subjects...");
// build all related subjects and change maps
if (this.mode === "save" || this.mode === "soft-remove" || this.mode === "recover") {
new OneToManySubjectBuilder(subjects).build();
new OneToOneInverseSideSubjectBuilder(subjects).build();
new ManyToManySubjectBuilder(subjects).build();
} else {
subjects.forEach(subject => {
if (subject.mustBeRemoved) {
new ManyToManySubjectBuilder(subjects).buildForAllRemoval(subject);
}
});
}
// console.timeEnd("other subjects...");
// console.timeEnd("building subjects...");
// console.log("subjects", subjects);
// create a subject executor
return new SubjectExecutor(queryRunner, subjects, this.options);
}));
// console.timeEnd("building subject executors...");
// make sure we have at least one executable operation before we create a transaction and proceed
// if we don't have operations it means we don't really need to update or remove something
const executorsWithExecutableOperations = executors.filter(executor => executor.hasExecutableOperations);
if (executorsWithExecutableOperations.length === 0)
return;
// start execute queries in a transaction
// if transaction is already opened in this query runner then we don't touch it
// if its not opened yet then we open it here, and once we finish - we close it
let isTransactionStartedByUs = false;
try {
// open transaction if its not opened yet
if (!queryRunner.isTransactionActive) {
if (!this.options || this.options.transaction !== false) { // start transaction until it was not explicitly disabled
isTransactionStartedByUs = true;
await queryRunner.startTransaction();
}
}
// execute all persistence operations for all entities we have
// console.time("executing subject executors...");
for (const executor of executorsWithExecutableOperations) {
await executor.execute();
}
// console.timeEnd("executing subject executors...");
// commit transaction if it was started by us
// console.time("commit");
if (isTransactionStartedByUs === true)
await queryRunner.commitTransaction();
// console.timeEnd("commit");
entities
則是我們save
的那個對象,本例中就是post
,爲每一個entity
建立一個subject
對象,subject
對象包含了我們保存需要的一切相關內容,此例中只有post
一個,但是接下來的cascadesSubjectBuilder.build(subject, this.mode)
則是爲我們post
的所有關係,繼續執行subject
的建立,
build(subject: Subject, operationType: "save"|"remove"|"soft-remove"|"recover") {
subject.metadata
.extractRelationValuesFromEntity(subject.entity!, subject.metadata.relations) // todo: we can create EntityMetadata.cascadeRelations
.forEach(([relation, relationEntity, relationEntityMetadata]) => {
// we need only defined values and insert, update, soft-remove or recover cascades of the relation should be set
if (relationEntity === undefined ||
relationEntity === null ||
(!relation.isCascadeInsert && !relation.isCascadeUpdate && !relation.isCascadeSoftRemove && !relation.isCascadeRecover))
return;
// if relation entity is just a relation id set (for example post.tag = 1)
// then we don't really need to check cascades since there is no object to insert or update
if (!(relationEntity instanceof Object))
return;
// if we already has this entity in list of operated subjects then skip it to avoid recursion
const alreadyExistRelationEntitySubject = this.findByPersistEntityLike(relationEntityMetadata.target, relationEntity);
if (alreadyExistRelationEntitySubject) {
if (alreadyExistRelationEntitySubject.canBeInserted === false) // if its not marked for insertion yet
alreadyExistRelationEntitySubject.canBeInserted = relation.isCascadeInsert === true && operationType === "save";
if (alreadyExistRelationEntitySubject.canBeUpdated === false) // if its not marked for update yet
alreadyExistRelationEntitySubject.canBeUpdated = relation.isCascadeUpdate === true && operationType === "save";
if (alreadyExistRelationEntitySubject.canBeSoftRemoved === false) // if its not marked for removal yet
alreadyExistRelationEntitySubject.canBeSoftRemoved = relation.isCascadeSoftRemove === true && operationType === "soft-remove";
if (alreadyExistRelationEntitySubject.canBeRecovered === false) // if its not marked for recovery yet
alreadyExistRelationEntitySubject.canBeRecovered = relation.isCascadeRecover === true && operationType === "recover";
return;
}
// mark subject with what we can do with it
// and add to the array of subjects to load only if there is no same entity there already
const relationEntitySubject = new Subject({
metadata: relationEntityMetadata,
parentSubject: subject,
entity: relationEntity,
canBeInserted: relation.isCascadeInsert === true && operationType === "save",
canBeUpdated: relation.isCascadeUpdate === true && operationType === "save",
canBeSoftRemoved: relation.isCascadeSoftRemove === true && operationType === "soft-remove",
canBeRecovered: relation.isCascadeRecover === true && operationType === "recover"
});
this.allSubjects.push(relationEntitySubject);
// go recursively and find other entities we need to insert/update
this.build(relationEntitySubject, operationType);
});
}
通過遍歷我們數據模型初始化時存儲的relations
,如果哪個關係的字段名在此次保存的數據中存在,則爲那一條關係也創建一個subject
並且插入到所有subject
的數組中,此方法是遞歸的,也就是如果關係數據中還有關係,則繼續深度執行,我們注意一下在subject
的canBeInserted
等屬性值進行判斷時,判斷條件中含有isCascadeInsert
,此值則來源於我們的cascade
屬性的設定。此例PostDetails
關係存在,則被插入到數組中,接下來我們待同步到數據庫的subject
有兩個,Post
和postDetail
接下來
await new SubjectDatabaseEntityLoader(queryRunner, subjects).load(this.mode);
這個函數通過判斷我們保存的數據中含有不含有主鍵字段如id,如果沒有id則是新的數據,直接執行插入等,如果有id說明是舊的數據,則通過主鍵id從數據庫裏查詢舊數據,接下來準備執行更新操作,
/**
* Loads database entities for all subjects.
*
* loadAllRelations flag is used to load all relation ids of the object, no matter if they present in subject entity or not.
* This option is used for deletion.
*/
async load(operationType: "save"|"remove"|"soft-remove"|"recover"): Promise<void> {
// we are grouping subjects by target to perform more optimized queries using WHERE IN operator
// go through the groups and perform loading of database entities of each subject in the group
const promises = this.groupByEntityTargets().map(async subjectGroup => {
// prepare entity ids of the subjects we need to load
const allIds: ObjectLiteral[] = [];
const allSubjects: Subject[] = [];
subjectGroup.subjects.forEach(subject => {
// we don't load if subject already has a database entity loaded
if (subject.databaseEntity || !subject.identifier)
return;
allIds.push(subject.identifier);
allSubjects.push(subject);
});
// if there no ids found (means all entities are new and have generated ids) - then nothing to load there
if (!allIds.length)
return;
const loadRelationPropertyPaths: string[] = [];
// for the save, soft-remove and recover operation
// extract all property paths of the relations we need to load relation ids for
// this is for optimization purpose - this way we don't load relation ids for entities
// whose relations are undefined, and since they are undefined its really pointless to
// load something for them, since undefined properties are skipped by the orm
if (operationType === "save" || operationType === "soft-remove" || operationType === "recover") {
subjectGroup.subjects.forEach(subject => {
// gets all relation property paths that exist in the persisted entity.
subject.metadata.relations.forEach(relation => {
const value = relation.getEntityValue(subject.entityWithFulfilledIds!);
if (value === undefined)
return;
if (loadRelationPropertyPaths.indexOf(relation.propertyPath) === -1)
loadRelationPropertyPaths.push(relation.propertyPath);
});
});
} else { // remove
// for remove operation
// we only need to load junction relation ids since only they are removed by cascades
loadRelationPropertyPaths.push(...subjectGroup.subjects[0].metadata.manyToManyRelations.map(relation => relation.propertyPath));
}
const findOptions: FindManyOptions<any> = {
loadEagerRelations: false,
loadRelationIds: {
relations: loadRelationPropertyPaths,
disableMixedMap: true
},
// the soft-deleted entities should be included in the loaded entities for recover operation
withDeleted: true
};
// load database entities for all given ids
const entities = await this.queryRunner.manager
.getRepository<ObjectLiteral>(subjectGroup.target)
.findByIds(allIds, findOptions);
// now when we have entities we need to find subject of each entity
// and insert that entity into database entity of the found subjects
entities.forEach(entity => {
const subjects = this.findByPersistEntityLike(subjectGroup.target, entity);
subjects.forEach(subject => {
subject.databaseEntity = entity;
if (!subject.identifier)
subject.identifier = subject.metadata.hasAllPrimaryKeys(entity) ? subject.metadata.getEntityIdMap(entity) : undefined;
});
});
// this way we tell what subjects we tried to load database entities of
for (let subject of allSubjects) {
subject.databaseEntityLoaded = true;
}
});
await Promise.all(promises);
}
可以看到,如果是準備更新的數據,則將subject
的databaseEntity
屬性設置爲要保存的值entity
,並將subject
的identifier
屬性設置爲此數據的主鍵,
接下來是一個比較關鍵的邏輯,也是typeorm爲我們提供的非常方便的功能
if (this.mode === "save" || this.mode === "soft-remove" || this.mode === "recover") {
new OneToManySubjectBuilder(subjects).build();
new OneToOneInverseSideSubjectBuilder(subjects).build();
new ManyToManySubjectBuilder(subjects).build();
} else {
subjects.forEach(subject => {
if (subject.mustBeRemoved) {
new ManyToManySubjectBuilder(subjects).buildForAllRemoval(subject);
}
});
}
當我們在保存一個不包含關係字段但含有關係數據的entity時,例如此例中,PostDetail
對Post
的關係是one-to-many
,Post
中包含一個PostDetail
的外鍵postDetailId
,而PostDetail
中其實是沒有任何與Post
相關的字段的,但是如果我們保存的postDetail
數據中含有post
字段,則相當於將關聯postDetail
的所有post
限定爲保存的post
字段數據,例如我們保存的一個postDetail
中含有post:[]
數據,則意味着沒有關聯到此postDetail
的post
數據,所以,那些舊的和此postDetail
關聯的數據需要解除關聯關係(通過orphanedRowAction
來配置解除的方式是刪除數據還是將外鍵設置爲null),我們看下new OneToManySubjectBuilder(subjects).build()
對many-to-one
關係的處理
protected buildForSubjectRelation(subject: Subject, relation: RelationMetadata) {
let relatedEntityDatabaseRelationIds: ObjectLiteral[] = [];
if (subject.databaseEntity) { // related entities in the database can exist only if this entity (post) is saved
relatedEntityDatabaseRelationIds = relation.getEntityValue(subject.databaseEntity);
}
let relatedEntities: ObjectLiteral[] = relation.getEntityValue(subject.entity!);
if (relatedEntities === null) // we treat relations set to null as removed, so we don't skip it
relatedEntities = [] as ObjectLiteral[];
if (relatedEntities === undefined) // if relation is undefined then nothing to update
return;
const relatedPersistedEntityRelationIds: ObjectLiteral[] = [];
relatedEntities.forEach(relatedEntity => { // by example: relatedEntity is a category here
let relationIdMap = relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity); // by example: relationIdMap is category.id map here, e.g. { id: ... }
let relatedEntitySubject = this.subjects.find(subject => {
return subject.entity === relatedEntity;
});
if (relatedEntitySubject)
relationIdMap = relatedEntitySubject.identifier;
if (!relationIdMap) {
if (!relatedEntitySubject)
return;
relatedEntitySubject.changeMaps.push({
relation: relation.inverseRelation!,
value: subject
});
return;
}
const relationIdInDatabaseSubjectRelation = relatedEntityDatabaseRelationIds.find(relatedDatabaseEntityRelationId => {
return OrmUtils.compareIds(relationIdMap, relatedDatabaseEntityRelationId);
});
if (!relationIdInDatabaseSubjectRelation) {
if (!relatedEntitySubject) {
relatedEntitySubject = new Subject({
metadata: relation.inverseEntityMetadata,
parentSubject: subject,
canBeUpdated: true,
identifier: relationIdMap
});
this.subjects.push(relatedEntitySubject);
}
relatedEntitySubject.changeMaps.push({
relation: relation.inverseRelation!,
value: subject
});
}
EntityMetadata
.difference(relatedEntityDatabaseRelationIds, relatedPersistedEntityRelationIds)
.forEach(removedRelatedEntityRelationId => { // by example: removedRelatedEntityRelationId is category that was bind in the database before, but now its unbind
const removedRelatedEntitySubject = new Subject({
metadata: relation.inverseEntityMetadata,
parentSubject: subject,
identifier: removedRelatedEntityRelationId,
});
if (!relation.inverseRelation || relation.inverseRelation.orphanedRowAction === "nullify") {
removedRelatedEntitySubject.canBeUpdated = true;
removedRelatedEntitySubject.changeMaps = [{
relation: relation.inverseRelation!,
value: null
}];
} else if (relation.inverseRelation.orphanedRowAction === "delete") {
removedRelatedEntitySubject.mustBeRemoved = true;
}
this.subjects.push(removedRelatedEntitySubject);
});
}
核心邏輯是,將從數據庫中剛剛查出來的databaseEntity
與我們保存的entity
進行對比,將databaseEntity
中存在的不存在與entity
中的關聯數據標記爲刪除,同樣構造成一個subject保存起來,
最後將這些所有需要保存或者刪除的subject
構造成一個SubjectExecutor
,然後啓動一個事務await queryRunner.startTransaction()
,然後對所有的subject
執行SubjectExecutor.execute
方法,該插入的插入,該更新的更新,該刪除的刪除,最後提交事務,則保存邏輯就執行完了
find relation 原理
其實經過上面的分析,我們也能猜到relation是怎麼查出來的了,就像save時一樣,通過完整的entityMetadata
,我們可以找到任意關係,也就是說,只要我們定義了many-to-one
,one-to-one
...等等等關係,那麼metadata
中就會有關係的完整數據,那麼我們在查詢時想要攜帶relation
的數據,也就很容易了,,至於性能問題,其實通過getQuery
我們可以看到,其實relation
就是執行的join
,多表深層join
因爲會掃描大量數據,所以性能問題其實是join
的問題,但是如果我們追求性能的話,那麼使用queryBuilder
,通過on
和where
條件來限制的話,其實性能也有很大的提升空間。
queryBuilder執行過程
上文中提到的無論是表格模型的同步還是數據的查詢保存其實最終都是執行的queryBuilder
,也就是說queryBuilder
是我們執行一切數據庫操作的終點,接下來我們分析一下queryBuilder
斷點斷到任意一個createQueryBuilder
處,重點執行了new SelectQueryBuilder(this, entityOrRunner as QueryRunner|undefined)
我們看下SelectQueryBuilder
的基類QueryBuilder
中含有
/**
* Contains all properties of the QueryBuilder that needs to be build a final query.
*/
readonly expressionMap: QueryExpressionMap;
這樣一個屬性,他包含了關於最終查詢所要執行的所有語句。
而SelectQueryBuilder
類中,則包含了巨多的方法,囊括了我們使用queryBuilder
時可以使用的所有方法,也正是這麼多方法,構成了靈活豐富的queryBuilder。例如innerJoin
,innerJoinAndSelect
,andWhere
,select
等等等,他們所有函數內容都是爲了填充expressionMap
,我們來簡單分析一下.where()
的執行邏輯,
where(where: Brackets|string|((qb: this) => string)|ObjectLiteral|ObjectLiteral[], parameters?: ObjectLiteral): this {
this.expressionMap.wheres = []; // don't move this block below since computeWhereParameter can add where expressions
const condition = this.getWhereCondition(where);
if (condition)
this.expressionMap.wheres = [{ type: "simple", condition: condition }];
if (parameters)
this.setParameters(parameters);
return this;
}
獲取到where
的條件後,賦值給expressionMap
的wheres
屬性,其他的中間邏輯的queryBuilder
也是類似,真正構造sql語句並執行的是一些特別的方法如getOne()
, execute()
等,我們簡單分析一下execute()
const [sql, parameters] = this.getQueryAndParameters();
const queryRunner = this.obtainQueryRunner();
try {
return await queryRunner.query(sql, parameters); // await is needed here because we are using finally
} finally {
if (queryRunner !== this.queryRunner) { // means we created our own query runner
await queryRunner.release();
}
}
很簡單,直接queryRunner
執行通過this.getQueryAndParameters()
獲取到的sql語句,我們繼續看getQueryAndParameters()
const query = this.getQuery();
const parameters = this.getParameters();
return this.connection.driver.escapeQueryWithParameters(query, parameters, this.expressionMap.nativeParameters);
獲取sql,獲取我們傳入的參數,然後拼接,我們看下getQuery()
getQuery(): string {
let sql = this.createComment();
sql += this.createSelectExpression();
sql += this.createJoinExpression();
sql += this.createWhereExpression();
sql += this.createGroupByExpression();
sql += this.createHavingExpression();
sql += this.createOrderByExpression();
sql += this.createLimitOffsetExpression();
sql += this.createLockExpression();
sql = sql.trim();
if (this.expressionMap.subQuery)
sql = "(" + sql + ")";
return sql;
}
通過前面構造的expressionMap
,拼接出sql語句,邏輯很清晰,然後最後附上參數,直接執行sql語句,queryBuilder
則執行完畢
總結
typeorm
通過提供給我們各種描述表結構的裝飾器,構建完整的數據庫結構metadata
,接下來的一切操作其實都基於這些metadata
。其實源碼結構也很清晰,就是幾個非常大的class
,另外typeorm
還提供了非常方便的數據庫結構同步,遷移腳本編寫,關係模型定義等功能,大大提高了我們項目的開發維護效率,接下來筆者可能會寫一篇描述 typeorm
和 nestjs
搭配開發很好的實踐的文章,敬請關注!