Implementing many-to-one relationships with Drizzle ORM
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…
June 3, 2024
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
CASCADEoption in PostgreSQL to delete all articles the user is an author of
- create an article with the
author_idthat does not point to a valid user - update existing articles and change the
author_idto point to a user that does not exist
Creating a many-to-one relationship with the Drizzle ORM#
Previously, we created the basics of the users and articles tables. Let’s add the author_id column to our schema.
database-schema.ts#
import { integer, pgTable, serial, text } from "drizzle-orm/pg-core"
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: text("email").unique(),
name: text("name"),
password: text("password"),
addressId: integer("address_id")
.unique()
.references(() => addresses.id),
})
export const articles = pgTable("articles", {
id: serial("id").primaryKey(),
title: text("title"),
content: text("content"),
authorId: integer("author_id").references(() => users.id),
})
// ...
export const databaseSchema = {
articles,
addresses,
users,
usersAddressesRelation,
}Now, we can use the Drizzle ORM Kit to create a migration file.
npx drizzle-kit generate --name add-author-idWhen we run the above command, Drizzle creates a SQL migration file.
0002_add-author-id.sql#
ALTER TABLE "articles" ADD COLUMN "author_id" integer;--> statement-breakpoint
DO $$ BEGIN
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;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;The last step is to run the migration.
npx drizzle-kit migrateThere 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#
import { integer, pgTable, serial } from "drizzle-orm/pg-core"
export const addresses = pgTable("addresses", {
id: serial("id").primaryKey(), // ...
})
export const users = pgTable("users", {
id: serial("id").primaryKey(), // ...
addressId: integer("address_id")
.unique()
.references(() => addresses.id),
})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#
import { Injectable } from "@nestjs/common"
import { DrizzleService } from "../database/drizzle.service"
import { databaseSchema } from "../database/database-schema"
import { CreateArticleDto } from "./dto/create-article.dto"
@Injectable()
export class ArticlesService {
constructor(private readonly drizzleService: DrizzleService) {}
async create(article: CreateArticleDto, authorId: number) {
const createdArticles = await this.drizzleService.db
.insert(databaseSchema.articles)
.values({
authorId,
title: article.title,
content: article.content,
})
.returning()
return createdArticles.pop()
} // ...
}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.
articles.controller.ts#
import { Body, Controller, Post, Req, UseGuards } from "@nestjs/common"
import { ArticlesService } from "./articles.service"
import { CreateArticleDto } from "./dto/create-article.dto"
import { JwtAuthenticationGuard } from "../authentication/jwt-authentication.guard"
import { RequestWithUser } from "../authentication/request-with-user.interface"
@Controller("articles")
export class ArticlesController {
constructor(private readonly articlesService: ArticlesService) {}
@Post()
@UseGuards(JwtAuthenticationGuard)
create(@Body() article: CreateArticleDto, @Req() request: RequestWithUser) {
return this.articlesService.create(article, request.user.id)
} // ...
}The RequestWithUser type extends the Request type from Express.
request-with-user.interface.ts#
import { Request } from "express"
export interface RequestWithUser extends Request {
user: {
id: number
name: string
email: string
}
}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#
import { Injectable, NotFoundException } from "@nestjs/common"
import { DrizzleService } from "../database/drizzle.service"
import { databaseSchema } from "../database/database-schema"
import { eq } from "drizzle-orm"
@Injectable()
export class ArticlesService {
constructor(private readonly drizzleService: DrizzleService) {}
async getWithAuthor(articleId: number) {
const allResults = await this.drizzleService.db
.select()
.from(databaseSchema.articles)
.where(eq(databaseSchema.articles.id, articleId))
.innerJoin(
databaseSchema.users,
eq(databaseSchema.articles.authorId, databaseSchema.users.id),
)
const result = allResults.pop()
if (!result) {
throw new NotFoundException()
}
return {
...result.articles,
author: result.users,
}
} // ...
}We also need to use the getWithAuthor method in our controller.
articles.controller.ts#
import { Controller, Get, Param, ParseIntPipe } from "@nestjs/common"
import { ArticlesService } from "./articles.service"
@Controller("articles")
export class ArticlesController {
constructor(private readonly articlesService: ArticlesService) {}
@Get(":id")
getById(@Param("id", ParseIntPipe) id: number) {
return this.articlesService.getWithAuthor(id)
} // ...
}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#
import { relations } from "drizzle-orm"
import { integer, pgTable, serial, text } from "drizzle-orm/pg-core"
// ...
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: text("email").unique(),
name: text("name"),
password: text("password"),
addressId: integer("address_id")
.unique()
.references(() => addresses.id),
})
export const articles = pgTable("articles", {
id: serial("id").primaryKey(),
title: text("title"),
content: text("content"),
authorId: integer("author_id").references(() => users.id),
})
export const articlesAuthorsRelation = relations(articles, ({ one }) => ({
author: one(users, {
fields: [articles.authorId],
references: [users.id],
}),
}))
export const databaseSchema = {
articles,
addresses,
users,
usersAddressesRelation,
articlesAuthorsRelation,
}Thanks to creating the articlesAuthorsRelation, we can now use the Query API to fetch articles and their authors.
articles.service.ts#
import { Injectable, NotFoundException } from "@nestjs/common"
import { DrizzleService } from "../database/drizzle.service"
import { databaseSchema } from "../database/database-schema"
import { eq } from "drizzle-orm"
@Injectable()
export class ArticlesService {
constructor(private readonly drizzleService: DrizzleService) {}
async getWithAuthor(articleId: number) {
const article = await this.drizzleService.db.query.articles.findFirst({
with: {
author: true,
},
where: eq(databaseSchema.articles.id, articleId),
})
if (!article) {
throw new NotFoundException()
}
return article
} // ...
}We can take it further and fetch both the author and their address.
articles.service.ts#
import { Injectable, NotFoundException } from "@nestjs/common"
import { DrizzleService } from "../database/drizzle.service"
import { databaseSchema } from "../database/database-schema"
import { eq } from "drizzle-orm"
@Injectable()
export class ArticlesService {
constructor(private readonly drizzleService: DrizzleService) {}
async getWithAuthor(articleId: number) {
const article = await this.drizzleService.db.query.articles.findFirst({
with: {
author: {
with: {
address: true,
},
},
},
where: eq(databaseSchema.articles.id, articleId),
})
if (!article) {
throw new NotFoundException()
}
return article
} // ...
}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!