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.
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.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.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 AddRankToLegionary3. Run the migration - only now does the change reach the database:
1typeorm migration:run4. 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.
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:revertrevert 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.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.The legion's archive keeps itself, and you know how to read it:
synchronize: true can be convenient locally, but on production it can wipe out data,MigrationInterface: up() applies the change, down() must reverse it exactly,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(),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.