We use cookies to enhance your experience on the site
CodeWorlds

Migrations and Entities - the Roman legion's archive

You added a

rank
field to the
Legionary
entity. It works on your machine - the development database created the column by itself. You push the code, and the next morning a message arrives from a colleague: the application will not start, because his database has no
rank
column. On production it would be worse - nobody there lets a database rebuild itself, because along the way it can wipe out data.

The problem is the same one code had before git was invented: changes to the database structure have no history. Nobody knows who changed what, in what order, or how to undo it. The Romans kept an archive for exactly this - every change to the legion's register written down as a separate dated document. In TypeORM that document is called a migration.

What a migration is

A migration is a file holding one schema change - adding a table, a column, an index. Its name carries a timestamp, so migrations line up in order, and the database remembers which ones it has already run. This is schema versioning: exactly what git does for code, only for the structure of your tables.

Thanks to that, every colleague runs the same migrations in the same order and ends up with an identical database. And on production the change goes in deliberately, with one command, rather than by an ORM's guesswork. That is why

synchronize: true
- the option that lets TypeORM rebuild the database to match the entities - is convenient locally but must never reach production.

Anatomy of a migration: up and down

A migration is a class implementing the

MigrationInterface
interface with two methods:

1import { MigrationInterface, QueryRunner } from 'typeorm';
2
3export class CreateLegionaryTable1710000000000 implements MigrationInterface {
4  public async up(queryRunner: QueryRunner): Promise<void> {
5    await queryRunner.query(`
6      CREATE TABLE "legionary" (
7        "id" SERIAL PRIMARY KEY,
8        "name" character varying(100) NOT NULL,
9        "rank" character varying(50)
10      )
11    `);
12  }
13
14  public async down(queryRunner: QueryRunner): Promise<void> {
15    await queryRunner.query(`DROP TABLE "legionary"`);
16  }
17}

These two methods are the road there and back.

up()
applies the change - here it creates the table.
down()
undoes it - it drops that same table. The rule is simple:
down()
must reverse exactly what
up()
did. Added a column in
up
? Drop it in
down
. Without that, reverting a migration leaves the database in a state nobody planned for.

Note the argument of both methods:

queryRunner
. It is the same object you will meet again in the lesson on transactions - it represents a database connection on which we execute commands. The number in the class name (
1710000000000
) is the creation timestamp; TypeORM orders migrations by it, not by the file name.

The migration workflow

In daily work you rarely write migrations by hand. The order is always the same and worth remembering as four steps:

1. Change the entity - add a field, change a type, remove a column. This is the only place where you describe what the database should look like.

2. Generate the migration with the command that compares the entities against the database and records the difference:

1typeorm migration:generate -n AddRankToLegionary

3. Run the migration - only now does the change reach the database:

1typeorm migration:run

4. Check the result in the database - is the column there, does it have the right type, did the data survive.

Between steps two and three sits something easy to forget: read the generated file. The generator compares entities with the database and sometimes interprets a change differently than you intended - instead of renaming a column, it may drop the old one and add a new one, losing all the data on the way. That review takes half a minute, and I recommend it as a habit, @name.

generate vs create - two different commands

It is worth separating two commands with misleadingly similar names.

migration:generate
compares the entities with the database and fills in
up()
and
down()
for you - this is the command you will use in 95% of cases.
migration:create
creates an empty migration skeleton which you fill in yourself - useful when the change does not follow from the entities, for instance when moving data between columns.

When a migration turns out to be wrong, you undo it with one command:

1typeorm migration:revert

revert
calls the
down()
method of the last executed migration - one, not all of them. To step back three migrations, you run it three times. And here you see the point of that discipline in writing
down()
: the rollback will work exactly as well as you described the road back.

The entity steers the migration - column options

Since migrations are born from entities, column options are really instructions for the generator. A few are worth knowing, because they decide the shape of the table directly:

1@Entity()
2export class Tribute {
3  @PrimaryGeneratedColumn()
4  id: number;
5
6  @Column({ type: 'varchar', length: 100 })
7  name: string;
8
9  @Column({ type: 'decimal', precision: 10, scale: 2 })
10  value: number;
11
12  @Column({ default: false })
13  isCursed: boolean;
14}

@PrimaryGeneratedColumn()
is a primary key assigned automatically by the database - you never set it by hand.
type: 'varchar'
with
length: 100
gives text of limited length; an attempt to save a longer name is rejected by the database rather than silently truncated.

The most interesting is

decimal
with the
precision
and
scale
pair, because that pair tends to confuse.
precision
is the total number of digits
, and
scale
is how many of them sit after the decimal point
. So
precision: 10, scale: 2
means eight digits before the point and two after it - values up to 99,999,999.99. Why not a plain
float
? Because floating-point numbers round - with money and tribute values the totals would stop adding up.
decimal
stores the exact value.

default: false
writes a default value into the column definition in the database - a new tribute will be uncursed even if the code never sets that field.

Summary

The legion's archive keeps itself, and you know how to read it:

  • a migration is schema versioning - git for the structure of your tables,
  • synchronize: true
    can be convenient locally, but on production it can wipe out data,
  • a migration class implements
    MigrationInterface
    :
    up()
    applies the change,
    down()
    must reverse it exactly,
  • the workflow: change the entity,
    migration:generate
    , read the generated file,
    migration:run
    , check the database,
  • generate
    fills a migration from the entity-database difference,
    create
    gives an empty skeleton to write by hand,
  • migration:revert
    undoes one, the last migration, by calling its
    down()
    ,
  • column options steer the generator:
    length
    limits text,
    decimal
    with
    precision
    and
    scale
    stores exact amounts,
    default
    writes a default value into the database.

In the next lesson we will connect the tables with relations - because a legionary with no centurion and no tribute is still a lonely entry in the register. For now remember: a migration is a document in the archive - it describes one change and the road back, so that every database in the Empire can reach the same state.

Go to CodeWorlds