Skip to content

TypeORM: synchronize ๋Œ€์‹  migration

Youngho Kim edited this page Nov 23, 2022 · 2 revisions
  • ์ž‘์„ฑ์ž: J045_๊น€์˜ํ˜ธ

Migration๊ณผ Synchronize

synchronize์™€ migration

TypeORM์„ DB์™€ ์—ฐ๊ฒฐ์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ๋Š” DataSourceOption์„ ์ž‘์„ฑํ•ด์•ผํ•œ๋‹ค. ์ด๋•Œ, DB์˜ Table๊ณผ Entity๋“ค์„ ์—ฐ๋™์‹œํ‚ค๊ธฐ ์œ„ํ•ด ์šฐ๋ฆฌ๋Š” ์˜ต์…˜์— synchronize: true์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, migration์„ ์ˆ˜ํ–‰ํ•˜์—ฌ์•ผ ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ synchronize๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹คํ–‰๋  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ๋ฅผ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค. ๋‹ค์‹œ ๋งํ•ด Entity๋ฅผ ์ด์šฉํ•˜์—ฌ ํ•ญ์ƒ Table์„ ์ƒ์„ฑํ•ด์ค€๋‹ค๋Š” ๋œป์ด๋‹ค. ๋‹ค๋งŒ, ํ•ด๋‹น ์˜ต์…˜์€ Data ์†์‹ค ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— production ๋ ˆ๋ฒจ์—์„œ๋Š” ์‚ฌ์šฉํ•ด์„œ๋Š” ์•ˆ๋˜๋ฉฐ, ๋”ฐ๋ผ์„œ ๊ฐœ๋ฐœ, ํ˜น์€ ๋””๋ฒ„๊น… ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.

synchronize ์˜ต์…˜์€ ์™œ ์œ„ํ—˜ํ•œ๊ฐ€?

synchronize ์˜ต์…˜์€ Entity์™€ ์„ค์ •์„ ๋™์ผํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ๋‹จ์ˆœํžˆ Column์„ ADD/DROP ํ•˜๋Š” ํ˜•์‹์œผ๋กœ ํ…Œ์ด๋ธ”์„ ๋ณ€ํ˜•ํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ์— ๋งŒ์•ฝ ํŠน์ • Entity์˜ ์ด๋ฆ„์ด A์—์„œ B๋กœ ๋ฐ”๋€Œ์—ˆ๋‹ค๋ฉด, sync ์˜ต์…˜์ด ์ผœ์ง„ ๊ฒฝ์šฐ A Column์„ Drop์‹œํ‚ค๊ณ , B Column์„ Addํ•˜๋Š” ์‹์œผ๋กœ ์ž‘์—…์„ ํ•˜๊ธฐ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์†Œ์‹ค๋˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•œ๋‹ค. ์ด๋Ÿฌํ•œ ํ˜„์ƒ์„ Entity ๋ช…์ด ๋ฐ”๋€Œ์–ด๋„ ๋™์ผํ•˜๊ฒŒ ์ƒ๊ธด๋‹ค. (์ด ์ƒํ™ฉ์€ ์‹ฌ์ง€์–ด Table ์ž์ฒด๋ฅผ DROP ์‹œํ‚ค๊ณ  CREATE๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ๋” ์œ„ํ—˜ํ•ด์กŒ๋‹ค.)

์šฐ๋ฆฌ๋Š” ํ•ด๋‹น ์˜ต์…˜์„ DataSource ๊ฐ์ฒด๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ณผ์ •์—์„œ ์˜ต์…˜์˜ ์ผ๋ถ€๋กœ ์‚ฌ์šฉํ•œ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด, synchronize ์˜ต์…˜์ด ํ™œ์„ฑํ™”๋˜์—ˆ์„ ๋•Œ ์–ด๋–ค ๋™์ž‘์„ ํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด๋Š” ๊ฒƒ์ด ์ข‹์„ ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ ์œ„ํ•ด TypeORM์˜ ๊นƒํ—ˆ๋ธŒ ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, /src/data-source/DataSource.ts ์— DataSource ํด๋ž˜์Šค๊ฐ€ ์ •์˜๋˜์–ด์žˆ์œผ๋ฉฐ, ์ด ์ค‘ synchronize ์˜ต์…˜๊ณผ ๊ด€๋ จ๋œ ๋ถ€๋ถ„์€ initialize() ๋ฉ”์„œ๋“œ์—์„œ ํ•ด๋‹น ์˜ต์…˜์ด ์ผœ์ ธ์žˆ์„ ๊ฒฝ์šฐ synchronize() ๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š” ๋ถ€๋ถ„์ด๋‹ค. initialize์™€ synchronize ๋ฉ”์„œ๋“œ์˜ ๋ช…์„ธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

/**
 * Performs connection to the database.
 * This method should be called once on application bootstrap.
 * This method not necessarily creates database connection (depend on database type),
 * but it also can setup a connection pool with database to use.
 */
async initialize(): Promise<this> {
    if (this.isInitialized)
        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, { isInitialized: true })

    try {
        // build all metadatas registered in the current connection
        await 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.migrationsRun)
            await this.runMigrations({
                transaction: this.options.migrationsTransactionMode,
            })

        // if option is set - automatically synchronize a schema
        if (this.options.synchronize) await this.synchronize()
    } 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
}
async synchronize(dropBeforeSync: boolean = false): Promise<void> {
    if (!this.isInitialized)
        throw new CannotExecuteNotConnectedError(this.name)

    if (dropBeforeSync) await this.dropDatabase()

    const schemaBuilder = this.driver.createSchemaBuilder()
    await schemaBuilder.build()
}

๋ช…์„ธ ์ƒ์—์„œ initialize ๋ฉ”์„œ๋“œ๋Š” synchronize์—๊ฒŒ ์–ด๋– ํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜๋„ ์ „๋‹ฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ synchronize์˜ dropDatabase๋Š” ๋ฌด์‹œํ•ด๋„ ๋  ๊ฒƒ์ด๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์‹ค์งˆ์ ์œผ๋กœ Entity๋ฅผ DB์™€ ์—ฐ๋™ํ•˜๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์€ schemaBuilder์ผ ๊ฒƒ์ด๋‹ค.

์—ฌ๊ธฐ์„œ driver๋Š” DriverFactory ๊ฐ์ฒด๋กœ๋ถ€ํ„ฐ create ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•จ์œผ๋กœ์จ ์ƒ์„ฑ๋˜๋ฉฐ(this.driver = new DriverFactory().create(this) - ์ƒ์„ฑ์ž ํ•จ์ˆ˜, ์ผ๋ถ€ ์ƒ๋žต), DriverFactory๋Š” DataSource์˜ type ์˜ต์…˜์— ๋”ฐ๋ผ ์ƒ์„ฑํ•˜๊ณ , ๋Œ€๋ถ€๋ถ„์€ RDBMS๋“ค์˜ ๊ฒฝ์šฐ createSchemaBuilder๋กœ๋ถ€ํ„ฐ RdbmsSchemaBuilder๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.

์ถ”์ ์„ ๊ฑฐ๋“ญํ•œ ๊ฒฐ๊ณผ, ์šฐ๋ฆฌ๋Š” schemaBuilder.build()์˜ ๋ช…์„ธ๋ฅผ ์ฐพ์•„๋‚ผ ์ˆ˜ ์žˆ์—ˆ๊ณ , ์ด๋Š” ํ•˜๋‹จ๊ณผ ๊ฐ™๋‹ค.

/**
 * Creates complete schemas for the given entity metadatas.
 */
async build(): Promise<void> {
    this.queryRunner = this.connection.createQueryRunner()

    // this.connection.driver.database || this.currentDatabase;
    this.currentDatabase = this.connection.driver.database
    this.currentSchema = this.connection.driver.schema

    // 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.
    // In Spanner queries against the INFORMATION_SCHEMA can be used in a read-only transaction,
    // but not in a read-write transaction.
    const isUsingTransactions =
        !(this.connection.driver.options.type === "cockroachdb") &&
        !(this.connection.driver.options.type === "spanner") &&
        this.connection.options.migrationsTransactionMode !== "none"

    await this.queryRunner.beforeMigration()

    if (isUsingTransactions) {
        await this.queryRunner.startTransaction()
    }

    try {
        await this.createMetadataTableIfNecessary(this.queryRunner)
        // Flush the queryrunner table & view cache
        const tablePaths = this.entityToSyncMetadatas.map((metadata) =>
            this.getTablePath(metadata),
        )

        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.afterMigration()

        await this.queryRunner.release()
    }
}

์œ„์˜ ์ฝ”๋“œ๋“ค ์ค‘์—์„œ DB๋ฅผ ์กฐ์ž‘ํ•˜๋Š” ๊ฒƒ์€ this.executeSchemaSyncOperationsInProperOrder() ์ด๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, ์ผ๋ถ€ ๊ฒฝ์šฐ๋ฅผ ์ œ์™ธํ•˜๊ณ ๋Š” ์ปฌ๋Ÿผ์„ dropํ•œ ๋’ค add๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜, table์„ dropํ•œ ๋’ค createํ•˜๋Š” ํ˜•์‹์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.

/**
 * Executes schema sync operations in a proper order.
 * Order of operations matter here.
 */
protected async executeSchemaSyncOperationsInProperOrder(): Promise<void> {
    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()
}

๊ทธ๋ ‡๋‹ค๋ฉด ์˜ˆ์™ธ ์ผ€์ด์Šค๋Š” ๋ฌด์—‡์ธ๊ฐ€? renameColumns์˜ ๊ฒฝ์šฐ, ์˜ค๋กœ์ง€ Table์—์„œ ๋‹จ ํ•˜๋‚˜์˜ Column๋งŒ์ด ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ์—๋Š” Rename์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ์ด๋Ÿฌํ•œ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ Table์— ๋Œ€ํ•ด ์ˆ˜ํ–‰ํ•˜๋Š” ์‹์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ํŒŒ์•…ํ•˜๊ณ ์ž ํ•  ๊ฒฝ์šฐ ์ฃผ์„์„ ์ฐธ๊ณ ํ•œ๋‹ค.

renameColumns์˜ ์ฃผ์„ (์„ธ๋ถ€ ์ฒ˜๋ฆฌ๋Š” ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•  ๊ฒƒ.)

  • Renames columns.
  • Works if only one column per table was changed.
  • Changes only column name. If something besides name was changed, these changes will be ignored.

๋”ฐ๋ผ์„œ Entity ๋‚ด์—์„œ ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ์ด ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ Column์„ Dropํ•œ ๋’ค Addํ•˜๋Š” ํ˜•์‹์œผ๋กœ ์ฒ˜๋ฆฌ๋  ๊ฒƒ์ด๋ผ ์ง์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฉ”์„œ๋“œ ๋ช…์ด updateExistColumns์—ฌ์„œ ์ปฌ๋Ÿผ์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋งŒ ๋ณ€๊ฒฝ๋  ๊ฒƒ ๊ฐ™์€ ์ด ๋ฉ”์„œ๋“œ๋Š” changeColumns๋ผ๋Š” QueryRunner์˜ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ์‚ดํŽด๋ณด๋ฉด ์ผ๋ถ€ ์กฐ๊ฑด์˜ ๊ฒฝ์šฐ Drop ํ›„ Add๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ด ์™ธ์˜ ๊ฒฝ์šฐ ALTER ~ CHANGE COLUMN ํ˜•์‹์œผ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค.

  • Column์˜ ํƒ€์ž…์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Œ.

  • Column์˜ length(ํ˜น์€ size)๊ฐ€ ๋ณ€๊ฒฝ๋จ. (ex. varchar(50)โ†’varchar(60))

  • ๊ธฐ์กด๊ณผ ์ƒˆ ์ปฌ๋Ÿผ ๋‘˜ ๋‹ค generatedType์ด ์ •์˜๋˜์–ด์žˆ์ง€๋งŒ, ๋‘˜์˜ generatedType์ด ์„œ๋กœ ๋‹ค๋ฆ„.

  • generatedType์ด VIRTUAL์—์„œ undefined๊ฐ€ ๋˜๊ฑฐ๋‚˜, ๊ทธ ์—ญ์ผ ๊ฒฝ์šฐ

    ์ด๋ ‡๊ฒŒ synchronize๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ตœ๋Œ€ํ•œ ๋ณด์กดํ•ด์ฃผ๊ธฐ ์œ„ํ•ด ์•ˆ์ „์žฅ์น˜๋ฅผ ์ตœ๋Œ€ํ•œ ๋งˆ๋ จํ•˜์ง€๋งŒ, ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  Column ์ž์ฒด๊ฐ€ ๋‚ ์•„๊ฐ”๋‹ค๊ฐ€ ์ถ”๊ฐ€๋˜๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, synchronize๋Š” ์•ˆ์ „ํ•˜์ง€ ์•Š๋‹ค๊ณ  ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

    ์ด์— ๋ฐ˜ํ•˜์—ฌ migration์˜ ๊ฒฝ์šฐ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ด์ „ Entity์—์„œ ๋ณ€๊ฒฝ๋œ Entity๋กœ ์ˆ˜์ •ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ์ฟผ๋ฆฌ๋ฌธ์„ ์ง์ ‘ ์ž‘์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋ฐ์ดํ„ฐ ์†์‹ค์—์„œ ๊ทธ๋‚˜๋งˆ ์•ˆ์ „ํ•˜๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.

Migration ํ•ด๋ณด๊ธฐ

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ค€๋น„ - DataSourceOption ์ž‘์„ฑ

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์–ด๋–ค ํด๋”์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ์žˆ๊ณ , ์–ด๋–ค Table์— ์—ฌํƒœ๊นŒ์ง€ ์‹คํ–‰ํ•ด์™”๋˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋“ค์— ๋Œ€ํ•œ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ์žˆ๋Š”์ง€ ์ •์˜ํ•ด์•ผ ํ•œ๋‹ค. ์ด๋Ÿฌํ•œ ์ •๋ณด๋“ค์€ DataSourceOption์—์„œ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ•ด๋‹น ์˜ต์…˜์€ Nest.js์˜ ๊ฒฝ์šฐ TypeOrmModule.forRoot()์—์„œ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜, ์•„๋‹ˆ๋ฉด config ํŒŒ์ผ์„ ๋”ฐ๋กœ ์ž‘์„ฑํ•˜์—ฌ ์ด๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. (๋ณดํ†ต์€ ormconfig.ts๋กœ ์“ด๋‹ค.)

DataSourceOption์—๋Š” DBMS ํƒ€์ž…, DB ์ ‘๊ทผ ๋ฐฉ๋ฒ•, ์‚ฌ์šฉํ•  Entity, migrations, migrationsTableName์„ ์ •์˜ํ•˜์—ฌ DB์™€ Entity์™€์˜ ์—ฐ๋™ ๋ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฒ˜๋ฆฌ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • migrations

    • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ˆ˜ํ–‰ํ•  MigrationInterface ๊ตฌํ˜„์ฒด ํŒŒ์ผ ๋ชฉ๋ก.
    • ์ผ๋ฐ˜์ ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ๋“ค์„ ์ €์žฅํ•œ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ €์žฅํ•œ๋‹ค.
    • ex. migrations: ['src/migrations/**/*.ts']
  • migrationsTableName

    • ์ด๋ฏธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์— ์‚ฌ์šฉํ•œ MigrationInterface์˜ ๋ชฉ๋ก์„ ์ €์žฅํ•  ํ…Œ์ด๋ธ” ์ด๋ฆ„.
    • ๋ฏธ์ง€์ • ์‹œ migrations๋กœ ์ง€์ •๋œ๋‹ค.
    • ex. migrationsTableName: "migrations"

    ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฒ˜๋ฆฌ ์‹œ TypeORM์€ DB์— ๋ฐ˜์˜ํ•˜์ง€ ์•Š์€ (=Pending ์ƒํƒœ์ธ) MigrationInterface ๊ตฌํ˜„์ฒด๋“ค์„ ์ถ”์ถœํ•˜์—ฌ ์ด๋“ค์„ ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๊ฒƒ๋“ค๋ถ€ํ„ฐ ์‹คํ–‰ํ•œ๋‹ค. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณผ์ •์€ ๋ชจ๋‘ Transaction์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ, ๋”ฐ๋ผ์„œ ๋„์ค‘์— ์‹คํŒจํ•œ๋‹ค๊ณ  ํ•œ๋“ค Table์ด ๊นจ์ ธ๋ฒ„๋ฆฌ๋Š” ๋ถˆ์ƒ์‚ฌ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

    ์ด๋ฅผ ์ ์šฉํ•œ ํ”„๋กœ์ ํŠธ์˜ ormconfig.ts๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•˜์˜€๋‹ค. ์ฃผ์˜ํ•  ์ ์€, ํ•ด๋‹น ํŒŒ์ผ์—์„œ exportํ•˜๋Š” DataSource๋Š” ๋ฐ˜๋“œ์‹œ ๋‹จ ํ•œ๊ฐœ๋งŒ ์กด์žฌํ•˜์—ฌ์•ผ ํ•˜๋ฉฐ, ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•˜์—ฌ์•ผ ํ•œ๋‹ค.

import { join } from 'path';
import { DataSource, DataSourceOptions } from 'typeorm';
import * as dotenv from 'dotenv';

dotenv.config();
export const config: DataSourceOptions = {
  type: 'mysql',
  host: process.env.MYSQL_HOST,
  port: parseInt(process.env.MYSQL_PORT),
  username: process.env.MYSQL_USERNAME,
  password: process.env.MYSQL_PASSWORD,
  database: process.env.MYSQL_DATABASE,
  entities: [join(__dirname, '/**/*.entity{.ts,.js}')],
  migrationsRun: true,
  migrations: [process.env.NODE_ENV === 'develop' ? 'src/migrations/**/*.ts' : 'dist/migrations/**/*.js'],
  migrationsTableName: 'migrations',
  synchronize: process.env.NODE_ENV === 'develop',
};
export default new DataSource(config);

CLI๋ฅผ ํ†ตํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ˆ˜ํ–‰

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ˆ˜๋™์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋ ค๋ฉด, typeorm-cli๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์ด๋ฅผ ๋”ฐ๋กœ ์„ค์น˜ํ•  ํ•„์š”๋Š” ์—†๊ณ , TypeScript๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ package.json์— script๋กœ ๋“ฑ๋กํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค. (JS๋ฅผ ์“ฐ๋Š” ๊ฒฝ์šฐ ๋‹จ์ˆœํžˆ typeorm ์„ค์น˜ ํ›„ npx typeorm <param> ํ˜•์‹์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.)

TypeORM์˜ CLI๋Š” ์˜ค๋กœ์ง€ js ํŒŒ์ผ์„ ์‹คํ–‰ํ•  ๋ชฉ์ ์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ๊ธฐ์—, TypeScript ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ด๋ฅผ ํŠธ๋žœ์ŠคํŒŒ์ผ๋งํ•  ๋„๊ตฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ๊ทธ๋ ‡๊ธฐ์— ts-node๋ฅผ ์„ค์น˜ํ•œ ๋’ค, package.json์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉฐ, ์ถ”ํ›„ npx typeorm ์ด๋‚˜ npm run typeorm ์ด๋ผ๋Š” ์‹์œผ๋กœ CLI๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

// commonjs ํ”„๋กœ์ ํŠธ์ผ ๊ฒฝ์šฐ
"script": {
	// ...
	"typeorm": "typeorm-ts-node-commonjs"
}
// ESM ํ”„๋กœ์ ํŠธ์ผ ๊ฒฝ์šฐ
"script": {
	// ...
	"typeorm": "typeorm-ts-node-esm"
}

๋งŒ์•ฝ npm ์Šคํฌ๋ฆฝํŠธ์—๊ฒŒ ๋„˜๊ฒจ์•ผ ํ•  ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, --์„ ์ถ”๊ฐ€ํ•œ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ช…๋ น์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

# Case 1
npm run typeorm -- migration:run -d <dataSource File location>
# Case 2
npm run typeorm migration:run -- -d <dataSource File location>

์‹คํ–‰ ์˜ˆ์ œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ด€๋ จ ๋ช…๋ น =========================================
# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰
npm run typeorm -- migration:run -d <dataSource File location>
# ๊ฐ€์žฅ ์ตœ๊ทผ์— ์‹คํ–‰๋œ migrationInterface ๊ตฌํ˜„์ฒด์˜ ์‹คํ–‰์„ ์ทจ์†Œ
npm run typeorm -- migration:revert -d <dataSource File location>
# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ชฉ๋ก ์ถœ๋ ฅ (๋ฐ˜์˜ = [X], ๋ฏธ๋ฐ˜์˜ = [ ])
npm run typeorm -- migration:show -d <dataSource File location>
# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ
npm run typeorm -- migration:create <์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ>/<template ํŒŒ์ผ๋ช…>
# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž๋™ ์ƒ์„ฑ
npm run typeorm -- migration:generate <์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ>/<template ํŒŒ์ผ๋ช…>

# ์ฐธ๊ณ  ==========================================================
# synchronization ์ˆ˜ํ–‰ (synchronize=true ์‹œ์˜ ์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰, ์ฃผ์˜ ์š”๊ตฌ)
npm run typeorm -- schema:sync
# sync ์š”๊ตฌ ์‹œ ์ฒ˜๋ฆฌํ•  ์ฟผ๋ฆฌ๋ฌธ ๋ชฉ๋ก ํ™•์ธ (๊ฐ€์งœ๋กœ sync๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.)
npm run typeorm -- schema:log

migration:create์™€ migration:generate

์œ„์˜ CLI ์˜ˆ์ œ๋ฅผ ๋ณด๋ฉด, migration:create๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ…œํ”Œ๋ฆฟ์„ ์ƒ์„ฑํ•˜๋ฉฐ, migration:generate๋Š” ํ˜„์žฌ Entity์™€ DB์˜ ์Šคํ‚ค๋งˆ๋ฅผ ์„œ๋กœ ๋น„๊ตํ•˜์—ฌ, ์‹คํ–‰ํ•˜์—ฌ์•ผํ•  ์ฟผ๋ฆฌ๋ฌธ๋“ค์„ ์ƒ์„ฑํ•˜์—ฌ ์ด๋ฅผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์œผ๋กœ ์ถœ๋ ฅํ•œ๋‹ค. ๋‘ ๋ช…๋ น ๋ชจ๋‘ ์ด๋ฆ„์„ ์ผ€๋ฐฅ ์ผ€์ด์Šค๋กœ ์ด๋ฆ„์„ ์ง€์ •ํ•ด์ฃผ๋ฉด, ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ด๋ฆ„์€ {TIME_STAMP}-{Kebap-case-name}.ts ํ˜•์‹์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ฉฐ, ๊ตฌํ˜„์ฒด์˜ ์ด๋ฆ„์€ {camelCaseName}{TIME_STAMP} ํ˜•์‹์œผ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค.

ํ•˜์ง€๋งŒ ๋‚ด์šฉ๋ฌผ์€ ์กฐ๊ธˆ ๋‹ค๋ฅด๋‹ค. migration:create์˜ ๊ฒฝ์šฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์˜ ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ๋งŒ์„ ์ƒ์„ฑํ•˜๋ฉฐ, ๋‚ด์šฉ๋ฌผ์„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ฑ„์šฐ๋„๋ก ์š”๊ตฌํ•œ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์˜ ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ, ๋‹ค์‹œ ๋งํ•ด migration:create๋กœ ์ƒ์„ฑ๋œ ํŒŒ์ผ์˜ ๋‚ด์šฉ๋ฌผ์ด๋‹ค.

 import { MigrationInterface, QueryRunner } from 'typeorm';

export class addThumbnailOnWorkspace1669037051304 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {}

  public async down(queryRunner: QueryRunner): Promise<void> {}
}

MigrationInterface๋Š” ๊ตฌํ˜„์ฒด์—๊ฒŒ up๊ณผ down์„ ๊ตฌํ˜„ํ•  ๊ฒƒ์„ ์š”๊ตฌํ•œ๋‹ค. ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋“ค์—๊ฒŒ๋Š” DataSource๋ฅผ ์ด์šฉํ•˜์—ฌ DB์™€ ์—ฐ๊ฒฐ๋œ QueryRunner๊ฐ€ ์ฃผ์ž…๋˜๋ฉฐ, ์ด๋ฅผ ์ด์šฉํ•˜์—ฌ ์ฟผ๋ฆฌ๋ฅผ ์ •์˜ํ•˜๋ฉด ๋œ๋‹ค.

  • up

    • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์‹œ ์ฒ˜๋ฆฌํ•  ์ฟผ๋ฆฌ๋ฅผ ์ •์˜ํ•œ๋‹ค.
    • migration:run ์‹คํ–‰ ์‹œ ์ฒ˜๋ฆฌํ•  ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
  • down

    • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ทจ์†Œํ•˜๊ณ ์ž ํ•  ๋•Œ ์‹คํ–‰ํ•  ์ฟผ๋ฆฌ๋ฅผ ์ •์˜ํ•œ๋‹ค.
    • migration:revert ์‹คํ–‰ ์‹œ ์ฒ˜๋ฆฌํ•  ์ฟผ๋ฆฌ๋ฅผ ์ •์˜ํ•œ๋‹ค.

    ํ•˜๋‹จ์˜ ์ฝ”๋“œ๋Š” Entity์— Column์ด ํ•˜๋‚˜ ์ถ”๊ฐ€๋จ์— ๋”ฐ๋ผ ํ”„๋กœ์ ํŠธ์—์„œ ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ์— ์ž‘์„ฑํ•œ ์ฝ”๋“œ์ด๋‹ค.

import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';

export class addThumbnailOnWorkspace1669037051304 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.addColumn(
      'workspace',
      new TableColumn({
        name: 'thumbnail',
        type: 'varchar',
        length: '2083',
        isNullable: true,
      }),
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropColumn('workspace', 'thumbnail');
  }
}

์ด์— ๋ฐ˜ํ•ด migration:generate๋Š” DB์˜ ์Šคํ‚ค๋งˆ์™€ ํ˜„ Entity๋ฅผ ๋น„๊ตํ•˜์—ฌ ์ฟผ๋ฆฌ๊ฐ€ ์ด๋ฏธ ์ž‘์„ฑ๋˜์–ด์žˆ๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค. ๋”ฐ๋ผ์„œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. ๋‹ค๋งŒ ์ฃผ์˜ํ•  ์ ์€, ์ด ๋˜ํ•œ synchronize์™€ ๋น„์Šทํ•œ ์ฝ”๋“œ๊ฐ€ ๋„์ถœ๋  ์ˆ˜ ์žˆ๊ธฐ์— ๋ฐ์ดํ„ฐ ์†์‹ค์˜ ์šฐ๋ ค๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋”ฐ๋ผ์„œ generate๋œ ํŒŒ์ผ์˜ ์ฟผ๋ฆฌ๋ฌธ์„ ์ง์ ‘ ํ™•์ธํ•ด๋ณผ ํ•„์š”๊ฐ€ ์žˆ๋‹ค.

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฟผ๋ฆฌ ์ž‘์„ฑ ์‹œ

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ QueryRunner๋ฅผ ํ™œ์šฉํ•˜๋ฏ€๋กœ, ์ž‘์„ฑ ์‹œ์— ์ด์˜ API๋ฅผ ์–ด๋Š์ •๋„ ์ธ์ง€ํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค. ์ž์„ธํ•œ ์‚ฌํ•ญ์€ ๋งํฌ๋ฅผ ์ฐธ์กฐํ•œ๋‹ค.

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

  • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์‹œ๋„ํ•˜๋ฉด MODULE_NOT_FOUND ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒ
    • ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€์—์„œ ๋ชจ๋“ˆ๋“ค์„ ๋ชจ๋‘ ์ƒ๋Œ€๊ฒฝ๋กœ๋กœ import ํ•˜์˜€๋Š”์ง€ ํ™•์ธ.
    • ์ ˆ๋Œ€๊ฒฝ๋กœ๋กœ import ๋œ ๋ชจ๋“ˆ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์œ„์˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•จ.
    • https://stackoverflow.com/questions/66991600/typeorm-migration-error-cannot-find-module

์ฐธ๊ณ ์ž๋ฃŒ

๐Ÿ“š ๊ทธ๋ผ์šด๋“œ ๋ฃฐ

โœ๏ธ ์ปจ๋ฒค์…˜

๐Ÿง‘โ€๐Ÿซ ๋ฉ˜ํ† ๋ง

๐Ÿ“ ์• ์ž์ผ ํ”„๋กœ์„ธ์Šค

๊ธฐํš
๋ฐ์ผ๋ฆฌ ์Šคํฌ๋Ÿผ
์Šคํ”„๋ฆฐํŠธ ๋ฆฌ๋ทฐ
์Šคํ”„๋ฆฐํŠธ ํšŒ๊ณ 
ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…
๊ธฐํƒ€ ์‚ฐ์ถœ๋ฌผ

๐Ÿ“– ๊ธฐ์ˆ ๋ฌธ์„œ

Week2
Week3
Week4
Week5

๐Ÿ—‚ ์ฐธ๊ณ ๋ฌธ์„œ

Clone this wiki locally