To answer the question briefly: Yes, Prisma is a new kind of ORM that fundamentally differs from traditional ORMs and doesn't suffer from many of the problems commonly associated with these.

Traditional ORMs provide an object-oriented way for working with relational databases by mapping tables to model classes in your programming language. This approach leads to many problems that are caused by the object-relational impedance mismatch.

Prisma works fundamentally different compared to that. With Prisma, you define your models in the declarative Prisma schema which serves as the single source of truth for your database schema and the models in your programming language. In your application code, you can then use Prisma Client to read and write data in your database in a type-safe manner without the overhead of managing complex model instances. This makes the process of querying data a lot more natural as well as more predictable since Prisma Client always returns plain JavaScript objects.

In this article, you will learn in more detail about ORM patterns and workflows, how Prisma implements the Data Mapper pattern, and the benefits of Prisma's approach.

What are ORMs?

If you're already familiar with ORMs, feel free to jump to the next section on Prisma.

ORM Patterns - Active Record and Data Mapper

ORMs provide a high-level database abstraction. They expose a programmatic interface through objects to create, read, delete, and manipulate data while hiding some of the complexity of the database.

The idea with ORMs is that you define your models as classes that map to tables in a database. The classes and their instances provide you with a programmatic API to read and write data in the database.

There are two common ORM patterns: Active Record and Data Mapper which differ in how they transfer data between objects and the database. While both patterns require you to define classes as the main building block, the most notable difference between the two is that the Data Mapper pattern decouples in-memory objects in the application code from the database and uses the data mapper layer to transfer data between the two. In practice, this means that with Data Mapper the in-memory objects (representing data in the database) don't even know that there’s a database present.

Active Record

Active Record ORMs map model classes to database tables where the structure of the two representations is closely related, e.g. each field in the model class will have a matching column in the database table. Instances of the model classes wrap database rows and carry both the data and the access logic to handle persisting changes in the database. Additionally, model classes can carry business logic specific to on the data in the model.

The model class typically has methods that do the following:

  • Construct an instance of the model from an SQL query.
  • Construct a new instance for later insertion into the table.
  • Methods to wrap commonly used SQL queries and return Active Record objects.
  • Update the database and insert into it the data in the Active Record.
  • Get and set the fields.
  • Implement business logic.

Data Mapper

Data Mapper ORMs, in contrast to Active Record, decouple the application's in-memory representation of data from the database's representation. The decoupling is achieved by requiring you to separate the mapping responsibility into two types of classes:

  • Entity classes: The application's in-memory representation of entities which have no knowledge of the database
  • Mapper classes: These have two responsibilities:
    • Transforming the data between the two representations.
    • Generating the SQL necessary to fetch data from the database and persist changes in the database.

Data Mapper ORMs allow for greater flexibility between the problem domain as implemented in code and the database. This is because the data mapper pattern allows you to hide the ways in which your database is implemented which isn’t an ideal way to think about your domain behind the whole data-mapping layer.

One of the reasons that traditional data mapper ORMs do this is due to the structure of organizations where the two responsibilities would be handled by separate teams, e.g., DBAs and backend developers.

In reality, not all Data Mapper ORMs adhere to this pattern strictly. For example, TypeORM, a popular ORM in the TypeScript ecosystem which supports both Active Record and Data Mapper, takes the following approach to Data Mapper:

  • Entity classes use decorators (@Column) to map class properties to table columns and are aware of the database.
  • Instead of mapper classes, repository classes are used for querying the database and may contain custom queries. Repositories use the decorators to determine the mapping between entity properties and database columns.

Given the following User table in the database:

user-table

This is what the corresponding entity class would look like:

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ name: 'first_name' })
firstName: string
@Column({ name: 'last_name' })
lastName: string
@Column({ unique: true })
email: string
}

Migration workflows

A central part of developing applications that make use of a database is changing the database schema to accommodate new features and to better fit the problem you're solving. In this section, we'll discuss what migrations are and how they affect the workflow.

Because the ORM sits between the developer and the database, most ORMs provide a migration tool to assist with the creation and modification of the database schema.

A migration is a set of steps to take the database schema from one state to another. The first migration usually creates tables and indices. Subsequent migrations may add or remove columns, introduce new indices, or create new tables. Depending on the migration tool, the migration may be in the form of SQL statements or programmatic code which will get converted to SQL statements (as with ActiveRecord and SQLAlchemy).

Because databases usually contain data, migrations assist you with breaking down schema changes into smaller units which helps avoid inadvertent data loss.

Assuming you were starting a project from scratch, this is what a full workflow would look like: you create a migration that will create the User table in the database schema and define the User entity class as in the example above.

Then, as the project progresses and you decide you want to add a new salutation column to the User table, you would create another migration which would alter the table and add the salutation column.

Let's take a look at how that would look like with a TypeORM migration:

import { MigrationInterface, QueryRunner } from 'typeorm'
export class UserRefactoring1604448000 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" ADD COLUMN "salutation" TEXT`)
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "salutation"`)
}
}

Once a migration is carried out and the database schema has been altered, the entity and mapper classes must also be updated to account for the new lastName column.

With TypeORM that means adding a salutation property to the User entity class:

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ name: 'first_name' })
firstName: string
@Column({ name: 'last_name' })
lastName: string
@Column({ unique: true })
email: string
@Column()
salutation: string
}

Synchronizing such changes can be a challenge with ORMs because the changes are applied manually and are not easily verifiable programmatically. Renaming an existing column can be even more cumbersome and involve searching and replacing references to the column.

Note: Django's makemigrations CLI generates migrations by inspecting changes in models which, similar to Prisma, does away with the synchronization problem.

In summary, evolving the schema is a key part of building applications. With ORMs, the workflow for updating the schema involves using a migration tool to create a migration followed by updating the corresponding entity and mapper classes (depending on the implementation). As you'll see, Prisma takes a different approach to this.

Now that you've seen what migrations are and how they fit into the development workflows, you will learn more about the benefits and drawbacks of ORMs.

Benefits of ORMs

There are different reasons why developers choose to use ORMs:

  • ORMs facilitate implementing the domain model. The domain model is an object model that incorporates the behavior and data of your business logic. In other words, it allows you to focus on real business concepts rather than the database structure or SQL semantics.
  • ORMs help reduce the amount of code. They save you from writing repetitive SQL statements for common CRUD (Create Read Update Delete) operations and escaping user input to prevent vulnerabilities such as SQL injections.
  • ORMs require you to write little to no SQL (depending on your complexity you may still need to write the odd raw query). This is beneficial for developers who are not familiar with SQL but still want to work with a database.
  • Many ORMs abstract database-specific details. In theory, this means that an ORM can make changing from one database to another easier. It should be noted that in practice applications rarely change the database they use.

As with all abstractions that aim to improve productivity, there are also drawbacks to using ORMs.

Drawbacks of ORMs

The drawbacks of ORMs are not always apparent when you start using them. This section covers some of the commonly accepted ones:

  • With ORMs, you form an object graph representation of database tables which may lead to the object-relational impedance mismatch. This happens when the problem you are solving forms a complex object graph which doesn't trivially map to a relational database. Synchronizing between two different representations of data, one in the relational database, and the other in-memory (with objects) is quite difficult. This is because objects are more flexible and varied in the way they can relate to each other compared to relational database records.
  • While ORMs handle the complexity associated with the problem, the synchronization problem doesn't go away. Any changes to the database schema or the data model require the changes to be mapped back to the other side. This burden is often on the developer. In the context of a team working on a project, database schema changes require coordination.
  • ORMs tend to have a large API surface due to the complexity they encapsulate. The flip side of not having to write SQL is that you spend a lot of time learning how to use the ORM. This applies to most abstractions, however without understanding how the database works, improving slow queries can be difficult.
  • Some complex queries aren't supported by ORMs due to the flexibility that SQL offers. This problem is alleviated by raw SQL querying functionality in which you pass the ORM a SQL statement string and the query is run for you.

Now that the costs and benefits of ORMs have been covered, you can better understand what Prisma is and how it fits in.

Prisma

Prisma is a next-generation ORM that makes working with databases easy for application developers and features the following tools:

Note: Since Prisma Client is the most prominent tool, we often refer to it as simply Prisma.

The three tools use the Prisma schema as a single source of truth for the database schema, your application's object schema, and the mapping between the two. It's defined by you and is your main configuration file for Prisma.

Prisma makes you productive and confident in the software you're building with features such as type safety, rich auto-completion, and a natural API for fetching relations.

In the next section, you will learn about how Prisma implements the Data Mapper ORM pattern.

How Prisma implements the Data Mapper pattern

As mentioned earlier in the article, the Data Mapper pattern aligns well with organisations where the database and application are owned by different teams.

With the rise of modern cloud environments with managed database services and DevOps practices, more teams embrace a cross-functional approach, whereby teams own both the full development cycle including the database and operational concerns.

Prisma enables the evolution of the DB schema and object schema in tandem, thereby reducing the need for deviation in the first place, while still allowing you to keep your application and database somewhat decoupled using @map attributes. While this may seem like a limitation, it prevents the domain model's evolution (through the object schema) from getting imposed on the database as an afterthought.

To understand how Prisma's implementation of the Data Mapper pattern differs conceptually to traditional Data Mapper ORMs, here's a brief comparison of their concepts and building blocks:

ConceptDescriptionBuilding block in traditional ORMsBuilding block in PrismaSource of truth in Prisma
Object schemaThe in-memory data structures in your applicationsModel classesGenerated TypeScript typesModels in the Prisma schema
Data MapperThe code which transforms between the object schema and the databaseMapper classesGenerated functions in Prisma Client@map attributes in the Prisma schema
Database schemaThe structure of data in the database, e.g., tables and columnsSQL written by hand or with a programmatic APISQL generated by Prisma MigratePrisma schema

Prisma aligns with the Data Mapper pattern with the following added benefits:

  • Reducing the boilerplate of defining classes and mapping logic by generating Prisma Client based on the Prisma schema.
  • Eliminating the synchronization challenges between application objects and the database schema.
  • Database migrations are a first-class citizen as they're derived from the Prisma schema.

Now that we've talked about the concepts behind Prisma's approach to Data Mapper, we can go through how the Prisma schema works in practice.

Prisma schema

At the heart of Prisma's implementation of the Data Mapper pattern is the Prisma schema – a single source of truth for the following responsibilities:

  • Configuring how Prisma connects your database.
  • Generating Prisma Client – the type-safe ORM for use in your application code.
  • Creating and evolving the database schema with Prisma Migrate.
  • Defining the mapping between application objects and database columns.

Models in Prisma mean something slightly different to Active Record ORMs. With Prisma, models are defined in the Prisma schema as abstract entities which describe tables, relations, and the mappings between columns to properties in the Prisma Client.

As an example, here's a Prisma schema for a blog:

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String? @map("post_content")
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}

Here's a break down of the example above:

  • The datasource block defines the connection to the database.
  • The generator block tells Prisma to generate the client for TypeScript and Node.js.
  • The Post and User models map to database tables.
  • The two models have a 1-n relation where each User can have many related Posts.
  • Each field in the models has a type, e.g. the id has the type Int.
  • Fields may contain field attributes to define:
    • Primary keys with the @id attribute.
    • Unique keys with the @unique attribute.
    • Default values with the @default attribute.
    • Mapping between table columns and Prisma Client fields with the @map attribute, e.g., the content field (which will be accessible in Prisma Client) maps to the post_content database column.

The User / Post relation can be visualized with the following diagram:

1-n relation between User and Post

At a Prisma level, the User / Post relation is made up of:

  • The scalar authorId field, which is referenced by the @relation attribute. This field exists in the database table – it is the foreign key that connects Post and User.
  • The two relation fields: author and posts do not exist in the database table. Relation fields define connections between models at the Prisma level and exist only in the Prisma schema and generated Prisma Client, where they are used to access the relations.

Prima schema's declarative nature is concise and allows defining the database schema and corresponding representation in Prisma Client.

In the next section, you will learn about Prisma's supported workflows.

Prisma workflow

The workflow with Prisma is slightly different to traditional ORMs. You can use Prisma when building new applications from scratch or adopt it incrementally:

  • New application (greenfield): Projects that have no database schema yet can use Prisma Migrate to create the database schema.
  • Existing application (brownfield): Projects that already have a database schema can be introspected by Prisma to generate the Prisma schema and Prisma Client. This use-case works with any existing migration tool and is useful for incremental adoption. It's possible to switch to Prisma Migrate as the migration tool. However, this is optional.

With both workflows, the Prisma schema is the main configuration file.

Workflow for incremental adoption in projects with an existing database

Brownfield projects typically already have some database abstraction and schema. Prisma can integrate with such projects by introspecting the existing database to obtain a Prisma schema that reflects the existing database schema and to generate the Prisma Client. This workflow is compatible with any migration tool and ORM which you may already be using. If you prefer to incrementally evaluate and adopt, this approach can be used as part of a parallel adoption strategy.

A non-exhaustive list of setups compatible with this workflow:

  • Projects using plain SQL files with CREATE TABLE and ALTER TABLE to create and alter the database schema.
  • Projects using a third party migration library like db-migrate or Umzug.
  • Projects already using an ORM. In this case, database access through the ORM remains unchanged while the generated Prisma Client can be incrementally adopted.

In practice, these are the steps necessary to introspect an existing DB and generate Prisma Client:

  1. Create a schema.prisma defining the datasource (in this case, your existing DB) and generator:

    datasource db {
    provider = "postgresql"
    url = "postgresql://janedoe:janedoe@localhost:5432/hello-prisma"
    }
    generator client {
    provider = "prisma-client-js"
    }
  2. Run prisma introspect to populate the Prisma schema with models derived from your database schema.

  3. (Optional) Customize field and model mappings between Prisma Client and the database.

  4. Run prisma generate.

Prisma will generate Prisma Client inside the node_modules folder, from which it can be imported in your application. For more extensive usage documentation, see the Prisma Client API docs.

To summarise, Prisma Client can be integrated into projects with an existing database and tooling as part of a parallel adoption strategy. New projects will use a different workflow detailed next.

Workflow for new projects

Prisma is different from ORMs in terms of the workflows it supports. A closer look at the steps necessary to create and change a new database schema is useful for understanding Prisma Migrate.

Prisma Migrate is a CLI for declarative data modeling & migrations. Unlike most migration tools that come as part of an ORM, you only need to describe the current schema, instead of the operations to move from one state to another. Prisma Migrate infers the operations, generates the SQL and carries out the migration for you.

This example demonstrates using Prisma in a new project with a new database schema similar to the blog example above:

  1. Create the Prisma schema:
// schema.prisma
datasource db {
provider = "postgresql"
url = "postgresql://janedoe:janedoe@localhost:5432/hello-prisma"
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String? @map("post_content")
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
  1. Run prisma migrate to generates the SQL for the migration, apply it to the database, and generate Prisma Client.

For any further changes to the database schema:

  1. Apply changes to the Prisma schema, e.g., add a registrationDate field to the User model
  2. Run prisma migrate again.

The last step demonstrates how declarative migrations work by adding a field to the Prisma schema and using Prisma Migrate to transform the database schema to the desired state. After the migration is run, Prisma Client is automatically regenerated so that it reflects the updated schema.

If you don't want to use Prisma Migrate but still want to use the type-safe generated Prisma Client in a new project, see the next section.

Alternative for new projects without Prisma Migrate

The workflow above relies on an early access version of Prisma Migrate. It is possible to use Prisma Client in a new project with a third-party migration tool instead of Prisma Migrate. For example, a new project could choose to use the Node.js migration framework db-migrate to create the database schema and migrations and Prisma Client for querying. In essence, this is covered by the workflow for existing databases.

Accessing data with Prisma Client

So far, the article covered the concepts behind Prisma, its implementation of the Data Mapper pattern, and the workflows it supports. In this last section, you will see how to access data in your application using Prisma Client.

Accessing the database with Prisma Client happens through the query methods it exposes. All queries return plain old JavaScript objects. Given the blog schema from above, fetching a user looks as follows:

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const user = await prisma.user.findUnique({
where: {
email: 'alice@prisma.io',
},
})

In this query, the findUnique method is used to fetch a single row from the User table. By default, Prisma will return all the scalar fields in the User table.

Note: The example uses TypeScript to make full use of the type safety features offered by Prisma Client. However, Prisma also works with JavaScript in Node.js.

Prisma Client maps queries and results to structural types by generating code from the Prisma schema. This means that user has an associated type in the generated Prisma Client:

export type User = {
id: number
email: string
name: string | null
}

This ensures that accessing a non-existent field will raise a type error. More broadly, it means that the result's type for every query is known ahead of running the query, which helps catch errors. For example, the following code snippet will raise a type error:

console.log(user.lastName) // Property 'lastName' does not exist on type 'User'.

Fetching relations

Fetch relations with Prisma Client is done with the include option. For example, to fetch a user and their posts would be done as follows:

const user = await prisma.user.findUnique({
where: {
email: 'alice@prisma.io',
},
include: {
posts: true,
},
})

With this query, user's type will also include Posts which can be accessed with the posts array property:

console.log(user.posts[0].title)

The example only scratches the surface of the Prisma Client's API for CRUD operations which you can learn more about in the docs. The main idea is that all queries and results are backed by types and you have full control over how relations are fetched.

Conclusion

In summary, Prisma is a new kind of Data Mapper ORM that differs from traditional ORMs and doesn't suffer from the problems commonly associated with them.

Unlike traditional ORMs, with Prisma, you define the Prisma schema – a declarative single source of truth for the database schema and application models. All queries in Prisma Client return plain JavaScript objects which makes the process of interacting with the database a lot more natural as well as more predictable.

Prisma supports two main workflows for starting new projects and adopting in an existing project. For both workflows, the Prisma schema is the main configuration file.

Like all abstractions, both Prisma and other ORMs hide away some of the underlying details of the database with different assumptions.

These differences and your use case all affect the workflow and cost of adoption. Hopefully understanding how they differ can help you make an informed decision.

Edit this page on GitHub