typeorm源碼解析

當(dāng)我們的應(yīng)用面對(duì)數(shù)據(jù)庫(kù)連接時(shí),選擇一個(gè)好用的orm框架是非常重要的,他可以為你解決sql注入,數(shù)據(jù)庫(kù)切換,數(shù)據(jù)模型遷移等等問題,也會(huì)為你提供易讀的優(yōu)雅的語(yǔ)法,讓你告別拼接sql語(yǔ)句

typeorm 作為對(duì)typescript支持度最好的orm框架除了擁有這些優(yōu)勢(shì)外,還提供了緩存關(guān)系,日志等等開箱即用的功能,使用typescriptquerybuilder可以模擬出任何復(fù)雜的sql語(yǔ)句并且不會(huì)丟失返回?cái)?shù)據(jù)類型,并提供query方法直接執(zhí)行sql語(yǔ)句,來(lái)滿足對(duì)舊有sql語(yǔ)句的遷移,

typescript可以運(yùn)行在 NodeJS、Browser、Cordova、PhoneGap、Ionic、React Native、Expo 和 Electron平臺(tái)上,并且官方支持MySQL / MariaDB / Postgres / SQLite / Microsoft SQL Server / Oracle / sql.js / mongodb

本文對(duì)typeorm源碼的解析主要從幾個(gè)疑問上切入

  1. synchronize:true時(shí)數(shù)據(jù)模型同步原理,以及為什么在生產(chǎn)環(huán)境時(shí)不能使用
  2. reposity.save()的執(zhí)行過程,在cascade: true時(shí)是如何自動(dòng)將關(guān)系一起保存的,有數(shù)據(jù)時(shí)更新,無(wú)數(shù)據(jù)時(shí)插入是如何實(shí)現(xiàn)的
  3. 在查詢relations時(shí)是否會(huì)有性能問題,關(guān)系是如何維持的,
  4. queryBuilder是如何構(gòu)造出各種復(fù)雜的sql語(yǔ)句的

準(zhǔn)備階段

我們可以直接clone typeorm官方倉(cāng)庫(kù)代碼,官方倉(cāng)庫(kù)包含sample目錄,里面是每個(gè)功能的示例代碼,我們直接對(duì)這里面的代碼進(jìn)行debug,來(lái)分析每個(gè)功能的源碼實(shí)現(xiàn)

git clone https://github.com/typeorm/typeorm
cd typeorm
npm install
npm run compile

vscode debug功能 launch.json

{
  // 使用 IntelliSense 了解相關(guān)屬性。
  // 懸停以查看現(xiàn)有屬性的描述。
  // 欲了解更多信息,請(qǐng)?jiān)L問: 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"]
    }
  ]
}

此例是對(duì)sample下的 sample3-many-to-one/app.ts進(jìn)行調(diào)試,我們先看下主要代碼

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實(shí)體模型中通過many-to-one保存對(duì)PostDetail的引用,非常經(jīng)典的多對(duì)一關(guān)系,我們以此示例代碼來(lái)分析本文中所要完成的四個(gè)問題

synchronize原理

數(shù)據(jù)庫(kù)模型的同步發(fā)生在我們剛剛連接到數(shù)據(jù)庫(kù)時(shí),我們將debug 斷點(diǎn)斷到createConnection(options)處,主要執(zhí)行的是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的對(duì)象初始化時(shí)獲得的,不同的數(shù)據(jù)庫(kù)使用不同的driver。比如mysql使用的是mysqlnodejs包,然后分別連接到配置文件中配置的數(shù)據(jù)庫(kù)和存儲(chǔ)查詢緩存的緩存數(shù)據(jù)庫(kù),接下來(lái)有一個(gè)很關(guān)鍵的邏輯buildMetadatas,為所有我們通過@entity,@colomn等裝飾器定義的表和字段初始化元數(shù)據(jù),函數(shù)里主要執(zhí)行
connectionMetadataBuilder.buildEntityMetadatas(this.options.entities || []),接下來(lái)是new EntityMetadataBuilder(this.connection,getMetadataArgsStorage()).build(allEntityClasses),getMetadataArgsStorage()是獲取到我們通過裝飾器@Entity() 和 @Column()等定義的實(shí)體和屬性,然后重點(diǎn)是build的執(zhí)行,我們進(jìn)入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;
    }

這里的邏輯很多,每個(gè)forEach都有自己的邏輯,例如獲得表格的名字,獲取他的關(guān)系等等,最終構(gòu)造出來(lái)一個(gè)EntityMetadata類,,他包含巨多的屬性,我們debug到這里時(shí)可以查看一下每個(gè)字段,也可以直接看他的class定義,每個(gè)屬性的值都來(lái)源于我們通過typeorm提供的各種裝飾器定義,最終構(gòu)造出來(lái)的metadatas將存在于全局,并在各個(gè)邏輯中被頻繁使用,構(gòu)造完metadatas后,我們可以看到有對(duì)synchronize的判斷

if (this.options.synchronize)
                await this.synchronize();

當(dāng)我們啟用synchronize,會(huì)直接執(zhí)行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();
    }

主要執(zhí)行了await schemaBuilder.build(),mongodb 和 其他關(guān)系型有不一樣的構(gòu)建邏輯,我們關(guān)注一下關(guān)系型數(shù)據(jù)庫(kù)

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();
        }
    }

重點(diǎn)執(zhí)行了await this.queryRunner.getTables(tablePaths),里面重點(diǎn)執(zhí)行了loadTables,通過查詢關(guān)系型數(shù)據(jù)庫(kù)INFORMATION_SCHEMA表,來(lái)獲取到所有的表的信息,包括名稱,主外鍵,字段類型,字段大小等等等。。保存在loadedTables中,然后getTables執(zhí)行完畢,接下來(lái)則是真正的數(shù)據(jù)庫(kù)結(jié)構(gòu)同步邏輯,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();

根據(jù)名字就可以看到,刪除舊表,刪除舊外鍵,刪除舊索引,添加字段,添加表等等等,我們挑選createNewTables()來(lái)看一下,

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);
        }
    }

循環(huán)遍歷this.entityToSyncMetadatas,即我們上文提到的構(gòu)建的通過各種裝飾器定義的所有表的元屬性,接下來(lái)在我們剛剛得到的loadedTablesfind每一個(gè)metadatatable,如果找到了,繼續(xù)循環(huán),未找到說明數(shù)據(jù)庫(kù)中還沒有此表格,那么接下來(lái)執(zhí)行新建表格的sql語(yǔ)句。
通過一個(gè)createNewTables()邏輯的分析,可以看到,就是通過將數(shù)據(jù)庫(kù)中真正表格的狀況和我們通過裝飾器定義的各種表格的元屬性進(jìn)行對(duì)比,來(lái)判斷是插入還是刪除還是更新。然后直接執(zhí)行對(duì)應(yīng)的sql語(yǔ)句,所以,如果我們修改了一個(gè)字段的名稱,可能會(huì)執(zhí)行的語(yǔ)句是刪除掉舊的字段,增加新的字段,而不是通過alter修改字段的名稱,所以會(huì)導(dǎo)致舊的字段的所有數(shù)據(jù)全部丟失,所以生產(chǎn)環(huán)境要慎用數(shù)據(jù)庫(kù)模型同步synchronize:true,如果真正的要修改字段名, typeorm為我們提供了數(shù)據(jù)遷移的功能,通過編寫數(shù)據(jù)遷移腳本,可以安全的進(jìn)行數(shù)據(jù)遷移,并且可以按照版本回滾,非常人性化。

save執(zhí)行過程

斷點(diǎn)斷到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);
    }

主要執(zhí)行了EntityPersistExecutor().execute()方法,主要內(nèi)容是

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的那個(gè)對(duì)象,本例中就是post,為每一個(gè)entity建立一個(gè)subject對(duì)象,subject對(duì)象包含了我們保存需要的一切相關(guān)內(nèi)容,此例中只有post一個(gè),但是接下來(lái)的cascadesSubjectBuilder.build(subject, this.mode)則是為我們post的所有關(guān)系,繼續(xù)執(zhí)行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);
            });
    }

通過遍歷我們數(shù)據(jù)模型初始化時(shí)存儲(chǔ)的relations,如果哪個(gè)關(guān)系的字段名在此次保存的數(shù)據(jù)中存在,則為那一條關(guān)系也創(chuàng)建一個(gè)subject并且插入到所有subject的數(shù)組中,此方法是遞歸的,也就是如果關(guān)系數(shù)據(jù)中還有關(guān)系,則繼續(xù)深度執(zhí)行,我們注意一下在subjectcanBeInserted等屬性值進(jìn)行判斷時(shí),判斷條件中含有isCascadeInsert,此值則來(lái)源于我們的cascade屬性的設(shè)定。此例PostDetails關(guān)系存在,則被插入到數(shù)組中,接下來(lái)我們待同步到數(shù)據(jù)庫(kù)的subject有兩個(gè),PostpostDetail

接下來(lái)

await new SubjectDatabaseEntityLoader(queryRunner, subjects).load(this.mode);

這個(gè)函數(shù)通過判斷我們保存的數(shù)據(jù)中含有不含有主鍵字段如id,如果沒有id則是新的數(shù)據(jù),直接執(zhí)行插入等,如果有id說明是舊的數(shù)據(jù),則通過主鍵id從數(shù)據(jù)庫(kù)里查詢舊數(shù)據(jù),接下來(lái)準(zhǔn)備執(zhí)行更新操作,

/**
     * 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);
    }

可以看到,如果是準(zhǔn)備更新的數(shù)據(jù),則將subjectdatabaseEntity屬性設(shè)置為要保存的值entity,并將subjectidentifier屬性設(shè)置為此數(shù)據(jù)的主鍵,
接下來(lái)是一個(gè)比較關(guān)鍵的邏輯,也是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);
                        }
                    });
                }

當(dāng)我們?cè)诒4嬉粋€(gè)不包含關(guān)系字段但含有關(guān)系數(shù)據(jù)的entity時(shí),例如此例中,PostDetail對(duì)Post的關(guān)系是one-to-many,Post中包含一個(gè)PostDetail的外鍵postDetailId,而PostDetail中其實(shí)是沒有任何與Post相關(guān)的字段的,但是如果我們保存的postDetail數(shù)據(jù)中含有post字段,則相當(dāng)于將關(guān)聯(lián)postDetail的所有post限定為保存的post字段數(shù)據(jù),例如我們保存的一個(gè)postDetail中含有post:[]數(shù)據(jù),則意味著沒有關(guān)聯(lián)到此postDetailpost數(shù)據(jù),所以,那些舊的和此postDetail關(guān)聯(lián)的數(shù)據(jù)需要解除關(guān)聯(lián)關(guān)系(通過orphanedRowAction來(lái)配置解除的方式是刪除數(shù)據(jù)還是將外鍵設(shè)置為null),我們看下new OneToManySubjectBuilder(subjects).build()對(duì)many-to-one關(guān)系的處理

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);
            });
    }

核心邏輯是,將從數(shù)據(jù)庫(kù)中剛剛查出來(lái)的databaseEntity與我們保存的entity進(jìn)行對(duì)比,將databaseEntity中存在的不存在與entity中的關(guān)聯(lián)數(shù)據(jù)標(biāo)記為刪除,同樣構(gòu)造成一個(gè)subject保存起來(lái),
最后將這些所有需要保存或者刪除的subject構(gòu)造成一個(gè)SubjectExecutor,然后啟動(dòng)一個(gè)事務(wù)await queryRunner.startTransaction(),然后對(duì)所有的subject執(zhí)行SubjectExecutor.execute方法,該插入的插入,該更新的更新,該刪除的刪除,最后提交事務(wù),則保存邏輯就執(zhí)行完了

find relation 原理

其實(shí)經(jīng)過上面的分析,我們也能猜到relation是怎么查出來(lái)的了,就像save時(shí)一樣,通過完整的entityMetadata,我們可以找到任意關(guān)系,也就是說,只要我們定義了many-to-one,one-to-one...等等等關(guān)系,那么metadata中就會(huì)有關(guān)系的完整數(shù)據(jù),那么我們?cè)诓樵儠r(shí)想要攜帶relation的數(shù)據(jù),也就很容易了,,至于性能問題,其實(shí)通過getQuery我們可以看到,其實(shí)relation就是執(zhí)行的join,多表深層join因?yàn)闀?huì)掃描大量數(shù)據(jù),所以性能問題其實(shí)是join的問題,但是如果我們追求性能的話,那么使用queryBuilder,通過onwhere條件來(lái)限制的話,其實(shí)性能也有很大的提升空間。

queryBuilder執(zhí)行過程

上文中提到的無(wú)論是表格模型的同步還是數(shù)據(jù)的查詢保存其實(shí)最終都是執(zhí)行的queryBuilder,也就是說queryBuilder是我們執(zhí)行一切數(shù)據(jù)庫(kù)操作的終點(diǎn),接下來(lái)我們分析一下queryBuilder
斷點(diǎn)斷到任意一個(gè)createQueryBuilder處,重點(diǎn)執(zhí)行了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;

這樣一個(gè)屬性,他包含了關(guān)于最終查詢所要執(zhí)行的所有語(yǔ)句。
SelectQueryBuilder類中,則包含了巨多的方法,囊括了我們使用queryBuilder時(shí)可以使用的所有方法,也正是這么多方法,構(gòu)成了靈活豐富的queryBuilder。例如innerJoin,innerJoinAndSelect,andWhere,select等等等,他們所有函數(shù)內(nèi)容都是為了填充expressionMap,我們來(lái)簡(jiǎn)單分析一下.where()的執(zhí)行邏輯,

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的條件后,賦值給expressionMapwheres屬性,其他的中間邏輯的queryBuilder也是類似,真正構(gòu)造sql語(yǔ)句并執(zhí)行的是一些特別的方法如getOne(), execute()等,我們簡(jiǎn)單分析一下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();
            }
        }

很簡(jiǎn)單,直接queryRunner執(zhí)行通過this.getQueryAndParameters()獲取到的sql語(yǔ)句,我們繼續(xù)看getQueryAndParameters()

const query = this.getQuery();
        const parameters = this.getParameters();
        return this.connection.driver.escapeQueryWithParameters(query, parameters, this.expressionMap.nativeParameters);

獲取sql,獲取我們傳入的參數(shù),然后拼接,我們看下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;
    }

通過前面構(gòu)造的expressionMap,拼接出sql語(yǔ)句,邏輯很清晰,然后最后附上參數(shù),直接執(zhí)行sql語(yǔ)句,queryBuilder則執(zhí)行完畢

總結(jié)

typeorm通過提供給我們各種描述表結(jié)構(gòu)的裝飾器,構(gòu)建完整的數(shù)據(jù)庫(kù)結(jié)構(gòu)metadata,接下來(lái)的一切操作其實(shí)都基于這些metadata。其實(shí)源碼結(jié)構(gòu)也很清晰,就是幾個(gè)非常大的class,另外typeorm還提供了非常方便的數(shù)據(jù)庫(kù)結(jié)構(gòu)同步,遷移腳本編寫,關(guān)系模型定義等功能,大大提高了我們項(xiàng)目的開發(fā)維護(hù)效率,接下來(lái)筆者可能會(huì)寫一篇描述 typeormnestjs搭配開發(fā)很好的實(shí)踐的文章,敬請(qǐng)關(guān)注!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容