Nest.js Tutorial

Implementing many-to-one relationships with Drizzle ORM

Marcin Wanago
NestJSSQL

Managing relationships between tables is a significant part of dealing with SQL databases. In this article, we continue learning to use Drizzle ORM with NestJS and implement many-to-one relationships.

Check out this repository if you want to see the full code from this article.

The many-to-one relationship

With many-to-one relationships, a row from the first table can be connected to multiple rows in the second table. What’s crucial is that the row from the second table can relate to just one row from the first table.

A great example is an article that can have a single author. On the other hand, a user can be an author of multiple articles. To implement that, we need to store the author’s ID in the articles table. This way, the author_id will act as a foreign key and match rows from the users table.

When defining a foreign key, PostgreSQL creates a foreign key constraint to ensure our database stays consistent. This prevents us from having an author_id value that points to a user that does not exist. This means that we can’t:

  • delete a user that’s an author of an article first, we would have to delete the article or change its author alternatively, we could use the CASCADE option in PostgreSQL to delete all articles the user is an author of
  • create an article with the author_id that does not point to a valid user
  • update existing articles and change the author_id to point to a user that does not exist

Creating a many-to-one relationship with the Drizzle ORM

In one of the previous parts of this series, we created the basics of the users and articles tables. Let’s add the author_id column to our schema.

database-schema.ts
1import { serial, text, integer, pgTable } from 'drizzle-orm/pg-core';
2 
3export const users = pgTable('users', {
4  id: serial('id').primaryKey(),
5  email: text('email').unique(),
6  name: text('name'),
7  password: text('password'),
8  addressId: integer('address_id')
9    .unique()
10    .references(() => addresses.id),
11});
12 
13export const articles = pgTable('articles', {
14  id: serial('id').primaryKey(),
15  title: text('title'),
16  content: text('content'),
17  authorId: integer('author_id').references(() => users.id),
18});
19 
20// ...
21 
22export const databaseSchema = {
23  articles,
24  addresses,
25  users,
26  usersAddressesRelation,
27};

Now, we can use the Drizzle ORM Kit to create a migration file.

Check out API with NestJS #149. Introduction to the Drizzle ORM with PostgreSQL to learn more about managing migrations with the Drizzle ORM Kit
1npx drizzle-kit generate --name add-author-id

When we run the above command, Drizzle creates a SQL migration file.

0002_add-author-id.sql
1ALTER TABLE "articles" ADD COLUMN "author_id" integer;--> statement-breakpoint
2DO $$ BEGIN
3 ALTER TABLE "articles" ADD CONSTRAINT "articles_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
4EXCEPTION
5 WHEN duplicate_object THEN null;
6END $$;

The last step is to run the migration.

1npx drizzle-kit migrate

There is one crucial thing to consider here. The new column we added is not nullable. If we already have some articles in our database, adding a new non-nullable column without a default value will cause an error. If that’s the case for you, you can create a default user in your database and provide their ID as the value for the author_id in the existing articles.

One-to-one vs many-to-one

In the previous article, we created a one-to-one relationship.

database-schema.ts
1import { serial, integer, pgTable } from 'drizzle-orm/pg-core';
2 
3export const addresses = pgTable('addresses', {
4  id: serial('id').primaryKey(),
5  // ...
6});
7 
8export const users = pgTable('users', {
9  id: serial('id').primaryKey(),
10  // ...
11  addressId: integer('address_id')
12    .unique()
13    .references(() => addresses.id),
14});

To ensure that a particular address belongs to only one user, we added the unique constraint.

However, when adding the author_id column, we don’t include the unique constraint. This allows multiple articles to have the same author.

Creating an article with an author

When creating articles, we should provide the author’s ID.

articles.service.ts
1import { Injectable } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { CreateArticleDto } from './dto/create-article.dto';
5 
6@Injectable()
7export class ArticlesService {
8  constructor(private readonly drizzleService: DrizzleService) {}
9 
10  async create(article: CreateArticleDto, authorId: number) {
11    const createdArticles = await this.drizzleService.db
12      .insert(databaseSchema.articles)
13      .values({
14        authorId,
15        title: article.title,
16        content: article.content,
17      })
18      .returning();
19 
20    return createdArticles.pop();
21  }
22 
23  // ...
24}

What’s important is that we shouldn’t expect the author’s ID to be provided directly in the body of the POST request. Instead, we should get this data by decoding the JWT authentication token.

To learn more JWT tokens and authentication, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
articles.controller.ts
1import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
2import { ArticlesService } from './articles.service';
3import { CreateArticleDto } from './dto/create-article.dto';
4import { JwtAuthenticationGuard } from '../authentication/jwt-authentication.guard';
5import { RequestWithUser } from '../authentication/request-with-user.interface';
6 
7@Controller('articles')
8export class ArticlesController {
9  constructor(private readonly articlesService: ArticlesService) {}
10 
11  @Post()
12  @UseGuards(JwtAuthenticationGuard)
13  create(@Body() article: CreateArticleDto, @Req() request: RequestWithUser) {
14    return this.articlesService.create(article, request.user.id);
15  }
16 
17  // ...
18}

The RequestWithUser type extends the Request type from Express.

request-with-user.interface.ts
1import { Request } from 'express';
2 
3export interface RequestWithUser extends Request {
4  user: {
5    id: number;
6    name: string;
7    email: string;
8  };
9}

Combining the article’s data and the author

Right now, when we fetch the details of a particular article, we only get the author’s ID.

Let’s change it to send the author’s details.

Making a join query

One solution would be to do a join query. In SQL databases, joins are used to combine rows from two or more tables.

The most basic type of join is the inner join, which returns records with matching rows in both tables. Since each article has an author, an inner join is a valid approach.

articles.service.ts
1import { Injectable, NotFoundException } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { eq } from 'drizzle-orm';
5 
6@Injectable()
7export class ArticlesService {
8  constructor(private readonly drizzleService: DrizzleService) {}
9  
10  async getWithAuthor(articleId: number) {
11    const allResults = await this.drizzleService.db
12      .select()
13      .from(databaseSchema.articles)
14      .where(eq(databaseSchema.articles.id, articleId))
15      .innerJoin(
16        databaseSchema.users,
17        eq(databaseSchema.articles.authorId, databaseSchema.users.id),
18      );
19 
20    const result = allResults.pop();
21 
22    if (!result) {
23      throw new NotFoundException();
24    }
25 
26    return {
27      ...result.articles,
28      author: result.users,
29    };
30  }
31 
32  
33  // ...
34}

We also need to use the getWithAuthor method in our controller.

articles.controller.ts
1import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
2import { ArticlesService } from './articles.service';
3 
4@Controller('articles')
5export class ArticlesController {
6  constructor(private readonly articlesService: ArticlesService) {}
7 
8  @Get(':id')
9  getById(@Param('id', ParseIntPipe) id: number) {
10    return this.articlesService.getWithAuthor(id);
11  }
12 
13  // ...
14}

With this approach, we respond with the data of an article combined with the details of its author.

Using the Query API

Alternatively, we can use the Query API built into the Drizzle ORM to avoid doing the join query manually. To do that, we need to provide Drizzle with more details about the relationship between the users and articles.

database-schema.ts
1import { serial, text, integer, pgTable } from 'drizzle-orm/pg-core';
2import { relations } from 'drizzle-orm';
3 
4// ...
5 
6export const users = pgTable('users', {
7  id: serial('id').primaryKey(),
8  email: text('email').unique(),
9  name: text('name'),
10  password: text('password'),
11  addressId: integer('address_id')
12    .unique()
13    .references(() => addresses.id),
14});
15 
16export const articles = pgTable('articles', {
17  id: serial('id').primaryKey(),
18  title: text('title'),
19  content: text('content'),
20  authorId: integer('author_id').references(() => users.id),
21});
22 
23export const articlesAuthorsRelation = relations(articles, ({ one }) => ({
24  author: one(users, {
25    fields: [articles.authorId],
26    references: [users.id],
27  }),
28}));
29 
30export const databaseSchema = {
31  articles,
32  addresses,
33  users,
34  usersAddressesRelation,
35  articlesAuthorsRelation,
36};

Thanks to creating the articlesAuthorsRelation, we can now use the Query API to fetch articles and their authors.

articles.service.ts
1import { Injectable, NotFoundException } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { eq } from 'drizzle-orm';
5 
6@Injectable()
7export class ArticlesService {
8  constructor(private readonly drizzleService: DrizzleService) {}
9 
10  async getWithAuthor(articleId: number) {
11    const article = await this.drizzleService.db.query.articles.findFirst({
12      with: {
13        author: true,
14      },
15      where: eq(databaseSchema.articles.id, articleId),
16    });
17 
18    if (!article) {
19      throw new NotFoundException();
20    }
21 
22    return article;
23  }
24  
25  // ...
26}

We can take it further and fetch both the author and their address.

articles.service.ts
1import { Injectable, NotFoundException } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { eq } from 'drizzle-orm';
5 
6@Injectable()
7export class ArticlesService {
8  constructor(private readonly drizzleService: DrizzleService) {}
9 
10  async getWithAuthor(articleId: number) {
11    const article = await this.drizzleService.db.query.articles.findFirst({
12      with: {
13        author: {
14          with: {
15            address: true,
16          },
17        },
18      },
19      where: eq(databaseSchema.articles.id, articleId),
20    });
21 
22    if (!article) {
23      throw new NotFoundException();
24    }
25 
26    return article;
27  }
28 
29  // ...
30}

Under the hood, Drizzle ORM will make two join queries—one to retrieve the user and the other to retrieve their address.

Summary

In this article, we’ve explained the many-to-one relationship and implemented it using the Drizzle ORM and NestJS. When doing that, we learned how to combine data from two tables through SQL join queries and by using the Query API built into the Drizzle ORM.

We still have more to learn about relationships with PostgreSQL and Drizzle ORM, so stay tuned!