1

API with NestJS #139. Using UUID as primary keys with Prisma and PostgreSQL

 8 months ago
source link: https://wanago.io/2024/01/01/api-nestjs-uuid-prisma-postgresql/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

API with NestJS #139. Using UUID as primary keys with Prisma and PostgreSQL

NestJS SQL

January 1, 2024

This entry is part 139 of 139 in the API with NestJS

Each record in our database should have a unique identifier. Typically, we use a numerical sequence to generate them. However, we can use an alternative approach that includes Universally Unique Identifiers (UUID). In this article, we discuss their advantages and disadvantages and implement them in a project with NestJS, Prisma, and PostgreSQL.

For the full code from this article check out this repository.

The idea behind UUID

A Universally Unique Identifier is a number represented using the hexadecimal system. While we’re used to the decimal system, which uses ten symbols (0-9) for values, there are alternatives. For example, the binary system uses just two symbols (0 and 1), and the hexadecimal system uses sixteen symbols, ranging from 0 to 9 and then A to F.

The hexadecimal system is good at representing very big numbers. It is vital in the context of UUID because it can contain a number that’s over 340 billion. One undecillion is a number equal to 1, followed by 36 zeros.

By using the hexadecimal system, we can shorten the representation of 340 undecillion from 340,000,000,000,000,000,000,000,000,000,000,000,000 to 0xFFC99E3C66FD68D2206F414000000000.

Hexadecimal numbers are often prefixed with 0x to indicate that they use the hexadecimal system.

To make it more readable, we store UUIDs using dashes that divide them into five groups, such as ffc99e3c-66fd-68d2-206f-414000000000. In the hexadecimal notation, both uppercase and lowercase are valid and represent the same values, but UUIDs usually use lowercase.

UUIDs are globally unique

There are various algorithms we can use to generate the UUID. Some consider aspects such as the current time and the machine’s MAC address.

The MAC (Media Access Control) address is a unique identifier assigned to every device that connects to the network.

However, the most common specification for generating UUIDs is labeled version 4 (v4) and generates IDs using pseudo-random numbers.

Most computer systems generate pseudo-random numbers rather than truly random numbers, due to the deterministic nature of computers.

While it is theoretically possible to generate the same UUID more than once, the chances are low enough to be ignored by most applications. If we generate 103 trillion v4 UUIDs, the chance of finding a duplicate is approximately one in a billion. This is thanks to the huge number of possible values that the v4 UUID algorithm can generate.

Benefits of UUIDs

Since UUIDs are designed to be globally unique, we won’t find duplicates across different tables, databases, and even systems. This has several benefits, such as the possibility to merge data from multiple sources without worrying about colliding IDs. This also means that various distributed systems can generate UUIDs independently without the risk of duplication.

UUIDs don’t reveal any information about our data, such as the number of records, as opposed to incremental numeric IDs. This makes it practically impossible for attackers to guess the ID of a particular record. While relying solely on security by obscurity is not a good practice, some might consider this a benefit.

Thanks to the UUIDs being unique across all systems, they can make debugging and tracing more straightforward. If we see a particular UUID in our logs, we can find the associated database row even if we don’t know which database table it comes from.

Downsides of UUIDs

Unfortunately, a single UUID takes 16 bytes and is larger than a traditional integer ID that typically takes 4 or 8 bytes. This can lead to more storage usage and potentially hurt our performance. Also, generating UUIDs requires more computational resources than generating regular sequential IDs.

Besides the above, UUIDs can be harder to read because they are longer and random as opposed to sequentially generated IDs.

Implementing UUIDs with Prisma

To start using UUID with Prisma, we must define the primary key as a string and set up its default value using the uuid() function.

The primary key is a unique identifier of each record in the table. No two rows can have the same values as the primary key.

schema.prisma
model Article {
  id      String  @id @default(uuid())
  title   String
  content String?
// ...

Let’s generate a migration that creates the table for our model.

If you want to know more about migrations with Prisma, check out API with NestJS #115. Database migrations with Prisma

npx prisma migrate dev --name create-article-table
migration.sql
-- CreateTable
CREATE TABLE "Article" (
    "id" TEXT NOT NULL,
    "title" TEXT NOT NULL,
    "content" TEXT,
    CONSTRAINT "Article_pkey" PRIMARY KEY ("id")

The migration shows us that the UUID values are not generated on the database level because they are not mentioned in the migration. Instead, they are generated by the Prisma’s query engine.

We also need to use strings instead of numbers in our controller when we expect the user to provide the ID.

articles.controller.ts
import {
  Body,
  Controller,
  Delete,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
@Controller('articles')
export default class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}
  @Get()
  getAll() {
    return this.articlesService.getAll();
  @Get(':id')
  getById(@Param('id') id: string) {
    return this.articlesService.getById(id);
  @Post()
  create(@Body() article: CreateArticleDto) {
    return this.articlesService.create(article);
  @Patch(':id')
  update(@Param('id') id: string, @Body() article: UpdateArticleDto) {
    return this.articlesService.update(id, article);
  @Delete(':id')
  async delete(@Param('id') id: string) {
    await this.articlesService.delete(id);

We need to make sure to adjust the types in the service as well.

When we make a request to create the article, we can see that Prisma generates a valid UUID for us.

Screenshot-from-2023-12-30-17-37-22.png

Generating UUIDs through PostgreSQL

So far, we’ve relied on Prisma to generate the UUID value. Instead, we can let PostgreSQL do that for us. For it to work, we need to use the pgcrypto extension.

schema.prisma
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["postgresqlExtensions"]
datasource db {
  provider   = "postgresql"
  url        = env("DATABASE_URL")
  extensions = [pgcrypto]
// ...

Once we’ve got that, we can use dbgenerated("gen_random_uuid()") to generate UUIDs through PostgreSQL. Let’s add @db.Uuid to change the type of the id column from TEXT to UUID to make it more storage-efficient.

schema.prisma
model Article {
  id      String  @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  title   String
  content String?
// ...

Let’s now generate the migration to apply the above changes.

npx prisma migrate dev --create-only --name generate-uuid-through-database

Unfortunately, Prisma generates a migration that removes the existing column and recreates it.

migration.sql
  Warnings:
  - The primary key for the `Article` table will be changed. If it partially fails, the table could be left without primary key constraint.
  - The `id` column on the `Article` table would be dropped and recreated. This will lead to data loss if there is data in the column.
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- AlterTable
ALTER TABLE "Article" DROP CONSTRAINT "Article_pkey",
DROP COLUMN "id",
ADD COLUMN     "id" UUID NOT NULL DEFAULT gen_random_uuid(),
ADD CONSTRAINT "Article_pkey" PRIMARY KEY ("id");

This would cause our IDs to be recreated. Let’s rewrite our migration to prevent that.

migration.sql
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- AlterTable
ALTER TABLE "Article"
ALTER COLUMN "id" TYPE UUID USING id::UUID,
ALTER COLUMN "id" SET DEFAULT gen_random_uuid();

With the above approach, we reuse the existing IDs instead of recreating them.

There are a few benefits of generating UUIDs through PostgreSQL. By handling it at the database level, we ensure consistency if more than one application connects to the database. This also applies when we interact with our database through pgAdmin or raw queries.

Screenshot-from-2023-12-30-18-57-35.png

This way, we don’t have to provide the ID value manually.

Summary

In this article, we explored the Universally Unique Identifiers (UUIDs) as an alternative to traditional numerical sequences. We learned how they are generated and provided examples using NestJS, Prisma, and PostgreSQL. This included generating UUIDs through Prisma and, alternatively, through PostgreSQL. Thanks to discussing their advantages and disadvantages, we now know when and if UUIDs are worth implementing.

Series Navigation<< API with NestJS #138. Filtering records with Prisma


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK