For a long time, TypeORM seemed to have a reputation of being somewhat stagnant. Lately, they stepped up their game, though, and started releasing many new versions. When doing that, they introduced a large number of breaking changes. In this article, we go through the most significant differences between 0.2.x and 0.3.x versions so far and see how they affect NestJS projects.
When writing this article, I was using TypeORM 0.3.7 and @nestjs/typeorm 8.1.4
Changes made to the repository API
TypeORM made a few changes to the functions we use to query the data.
Changes to the findOne method
First, they got rid of the findOne() function returning the first row from the table when not provided with an argument. It was caused by findOne(undefined) being confusing.
What’s more, TypeORM dropped the support for using findOne(id) to make the syntax more consistent. So now, we can only use findOne() by providing it with an object.
Because of all of the above, the following code no longer works:
1this.categoriesRepository.findOne(
2 id,
3 {
4 relations: ['posts'],
5 withDeleted: true
6 }
7);error TS2554: Expected 1 arguments, but got 2.
Instead, we must pass a single object to the findOne() method.
1this.categoriesRepository.findOne(
2 {
3 where: {
4 id
5 },
6 relations: ['posts'],
7 withDeleted: true
8 }
9);To see all of the available options, check out the FindOneOptions interface.
Although the above code with relations: ['posts'] still works, it is now deprecated and will soon be removed. Instead, we should use a new object-literal notation.
1this.categoriesRepository.findOne(
2 {
3 where: {
4 id
5 },
6 relations: {
7 posts: true
8 },
9 withDeleted: true
10 }
11);If we want to load a nested relation, we can create an object like this one:
1relations: {
2 posts: {
3 author: true
4 }
5}Changes to the find method
TypeORM made a similar set of changes to the find() method. Because of that, the following code is no longer valid:
1this.commentsRepository.find({
2 post: {
3 id: query.postId
4 }
5});error TS2345: Argument of type { post: { id: number; }; } is not assignable to parameter of type FindManyOptions<Comment>.
Instead, we need to provide the where object explicitly.
1this.commentsRepository.find({
2 where: {
3 post: {
4 id: query.postId
5 }
6 }
7});Besides the findOne and find methods, similar changes happened to findOneOrFail, count, and findAndCount.
Using the new findOneBy and findBy functions
When querying data, we don’t always need additional options, such as relations. In that case, we can use the new findOneBy method.
1this.databaseFilesRepository.findOneBy({
2 id: fileId
3});Similarly, we can use the new findBy method if we want to find multiple entities and don’t need to provide additional options such as relations.
1this.commentsRepository.findBy({
2 post: {
3 id: query.postId
4 }
5});Besides the above, we also have new findOneByOrFail, countBy, and findAndCountBy functions.
Deprecating the findByIds method
The findByIds is now deprecated and will soon be removed. Instead, we can use the findBy method with the In operator.
1import { In } from 'typeorm';
2
3postsRepository.findBy({
4 id: In([1, 2, 3])
5})Renaming Connection to DataSource
Before TypeORM 0.3.0, the configuration with our database used to be called a Connection. Recently, TypeORM renamed it to DataSource.
Throughout this series, we didn’t interact with the Connection much except for working with transactions.
1import { Injectable, InternalServerErrorException } from '@nestjs/common';
2import { InjectRepository } from '@nestjs/typeorm';
3import { Repository, Connection } from 'typeorm';
4import User from './user.entity';
5import { FilesService } from '../files/files.service';
6
7@Injectable()
8export class UsersService {
9 constructor(
10 @InjectRepository(User)
11 private usersRepository: Repository<User>,
12 private readonly filesService: FilesService,
13 private connection: Connection
14 ) {}
15
16 async deleteAvatar(userId: number) {
17 const queryRunner = this.connection.createQueryRunner();
18 const user = await this.getById(userId);
19 const fileId = user.avatar?.id;
20 if (fileId) {
21 await queryRunner.connect();
22 await queryRunner.startTransaction();
23 try {
24 await queryRunner.manager.update(User, userId, {
25 ...user,
26 avatar: null
27 });
28 await this.filesService.deletePublicFileWithQueryRunner(fileId, queryRunner);
29 await queryRunner.commitTransaction();
30 } catch (error) {
31 await queryRunner.rollbackTransaction();
32 throw new InternalServerErrorException();
33 } finally {
34 await queryRunner.release();
35 }
36 }
37 }
38
39 // ...
40}Unfortunately, the above code is no longer valid. Instead of Connection, we should use DataSource instead.
1import { Injectable, InternalServerErrorException } from '@nestjs/common';
2import { InjectRepository } from '@nestjs/typeorm';
3import { Repository, DataSource } from 'typeorm';
4import User from './user.entity';
5import { FilesService } from '../files/files.service';
6
7@Injectable()
8export class UsersService {
9 constructor(
10 @InjectRepository(User)
11 private usersRepository: Repository<User>,
12 private readonly filesService: FilesService,
13 private dataSource: DataSource
14 ) {}
15
16 async deleteAvatar(userId: number) {
17 const queryRunner = this.dataSource.createQueryRunner();
18 const user = await this.getById(userId);
19 const fileId = user.avatar?.id;
20 if (fileId) {
21 await queryRunner.connect();
22 await queryRunner.startTransaction();
23 try {
24 await queryRunner.manager.update(User, userId, {
25 ...user,
26 avatar: null
27 });
28 await this.filesService.deletePublicFileWithQueryRunner(fileId, queryRunner);
29 await queryRunner.commitTransaction();
30 } catch (error) {
31 await queryRunner.rollbackTransaction();
32 throw new InternalServerErrorException();
33 } finally {
34 await queryRunner.release();
35 }
36 }
37 }
38}Changes to the configuration
TypeORM made a few significant changes to how we configure our database connection. First, let’s take a look at our current configuration.
1import { Module } from '@nestjs/common';
2import { TypeOrmModule } from '@nestjs/typeorm';
3import { ConfigModule, ConfigService } from '@nestjs/config';
4
5@Module({
6 imports: [
7 TypeOrmModule.forRootAsync({
8 imports: [ConfigModule],
9 inject: [ConfigService],
10 useFactory: (configService: ConfigService) => ({
11 type: 'postgres',
12 username: configService.get('POSTGRES_USER'),
13 password: configService.get('POSTGRES_PASSWORD'),
14 // ...
15 entities: [
16 __dirname + '/../**/*.entity{.ts,.js}',
17 ],
18 })
19 }),
20 ],
21})
22export class DatabaseModule {}Unfortunately, strings with entities, migrations, and subscribers is now deprecated. Therefore, in future TypeORM versions, we will be able only to use entity references.
1import { Module } from '@nestjs/common';
2import { TypeOrmModule } from '@nestjs/typeorm';
3import { ConfigModule, ConfigService } from '@nestjs/config';
4import Post from '../posts/post.entity';
5import User from '../users/user.entity';
6
7@Module({
8 imports: [
9 TypeOrmModule.forRootAsync({
10 imports: [ConfigModule],
11 inject: [ConfigService],
12 useFactory: (configService: ConfigService) => ({
13 type: 'postgres',
14 username: configService.get('POSTGRES_USER'),
15 password: configService.get('POSTGRES_PASSWORD'),
16 // ...
17 entities: [
18 Post,
19 User,
20 // ...
21 ],
22 })
23 }),
24 ],
25})
26export class DatabaseModule {}Having to list all of our entities might be a bit troublesome. Fortunately, @nestjs/typeorm implements the autoLoadEntities option that we can use to auto-load entities.
1import { Module } from '@nestjs/common';
2import { TypeOrmModule } from '@nestjs/typeorm';
3import { ConfigModule, ConfigService } from '@nestjs/config';
4import Address from '../users/address.entity';
5
6@Module({
7 imports: [
8 TypeOrmModule.forRootAsync({
9 imports: [ConfigModule],
10 inject: [ConfigService],
11 useFactory: (configService: ConfigService) => ({
12 type: 'postgres',
13 username: configService.get('POSTGRES_USER'),
14 password: configService.get('POSTGRES_PASSWORD'),
15 entities: [
16 Address
17 ],
18 autoLoadEntities: true,
19 // ...
20 })
21 }),
22 ],
23})
24export class DatabaseModule {}Please notice that we still include the Address entity in the entities array above. The above is because we need to manually add every entity we don’t use through TypeOrmModule.forFeature(). In our case, a good example is the Address entity we use only through a one-to-one relationship.
New array operators
Previously, we had to use raw SQL to run more advanced queries with PostgreSQL arrays.
1async getPostsWithParagraph(paragraph: string) {
2 return this.postsRepository
3 .query(`SELECT * from post WHERE ${paragraph} = ANY(paragraphs)`);
4}Now, TypeORM 0.3.1 introduced new array operators that can let us handle such cases.
1import { ArrayContains } from 'typeorm';
2
3async getPostsWithParagraph(paragraph: string) {
4 return this.postsRepository
5 .findBy({
6 paragraphs: ArrayContains([paragraph])
7 })
8}Besides the above, we also have the ArrayContainedBy and ArrayOverlap operators.
Other changes
Besides all the changes mentioned so far, TypeORM fixed a ton of minor and significant issues and added performance improvements.
It is worth tracking the changelog in the GitHub releases page.
Some of the important changes are:
- requiring NodeJS 14+
- supporting TypeScript 4.8
- upgrading the ioredis library to v5
- supporting the Google Cloud Spanner database,
- implementing the FOR KEY SHARE lock mode for PostgreSQL,
- possibility of naming primary keys, foreign keys, and indices explicitly,
- supporting Common Table Expressions.
Summary
In this article, we’ve gone through the most significant changes made to TypeORM in the last changes. Some of them forced us to refactor our code to be able to bump the TypeORM version we use. All the changes we’ve handled in this article seem like steps in the right direction. A crucial thing to remember is that TypeORM does not follow semantic versioning. Because of that, going from 0.3.0 to 0.3.1 can include breaking changes, for example. Because of that, we need to be super careful when updating TypeORM and read the changelog very carefully to see if it affects our project.