Overview

To answer the question briefly: No, Prisma is not an ORM.

The main goal of Prisma is to make working with databases easier for application developers. In that sense, it shares the same goals with ORMs, but takes a fundamentally different approach.

ORMs are libraries that map tables in your database to classes in your programming language. Prisma, on the other hand, is a database toolkit. The toolkit includes Prisma Client, which is an auto-generated query builder that exposes queries which are tailored to your models. All Prisma Client queries return plain old JavaScript objects.

To understand how Prisma and ORMs differ conceptually, here's a brief overview of how their building blocks relate to databases:

DatabaseORMsPrisma
SQL SchemaMigrations and model classesPrisma schema
TablesModel classesModels in the Prisma schema
ColumnsProperties in model classesModel fields in the Prisma schema
RecordsInstances of a model class (objects)Plain JavaScript objects
Foreign keysForeign keys on model classesRelations between Prisma models

In this article, you will learn in more detail about the differences of Prisma compared to traditional ORMs and other database access libraries.

What are ORMs?

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

Concepts

ORMs provide a high-level database abstraction. They expose a programmatic interface to create, read, delete, and manipulate data while hiding some of the complexity of the database. Database abstractions vary in terms of the complexity they hide and the approach they take (for example, query builders like knex.js and MassiveJS).

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 flexible API to read and write data in the database.

Database records are represented as instances of the model classes which carry the logic for storage, retrieval, serialization, and deserialization. They often also contain business logic.

Model classes typically have several responsibilities:

  • Mapping the table's columns to the model's properties, e.g. mapping a createdAt column to a createdAt property on your model.
  • Mapping the foreign keys in a table to relations of the corresponding models, e.g. a 1:n relationship between a blog and posts as represented by a blog_id field in the posts table.
  • Implementing business/domain logic e.g. isUserEmailConfirmed method to check whether the account's email has been confirmed.

Workflows

As an example for what "typical" ORM workflows look like, Sequelize, a popular ORM in the Node.js ecosystem, will be used. Like most ORMs, Sequelize supports workflows for data modeling, querying, and schema migrations.

Data modeling

For example, assume a hypothetical User table:

user-table

The equivalent SQL in PostgreSQL dialect:

CREATE TABLE "public"."User" (
id SERIAL PRIMARY KEY NOT NULL,
first_name VARCHAR(255),
last_name VARCHAR(255),
email VARCHAR(255) UNIQUE NOT NULL,
email_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
birth_date DATE NOT NULL,
);

Here's what its corresponding model class will look like with Sequelize. You define the model's fields, map them to the table's fields, and define a class and instance method:

import { Model } from 'sequelize'
class User extends Model {
// Class method that can be called directly on the model class
static async isEmailConfirmed(email) {
const count = await this.count({ where: { email, emailConfirmed: true } })
return count === 0
}
// Model instance method
getFullName() {
return [this.firstname, this.lastname].join(' ')
}
}
User.init(
{
id: {
allowNull: false
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
}
firstName: {
type: Sequelize.STRING,
field: 'first_name',
allowNull: true
},
lastName: {
type: Sequelize.STRING,
field: 'last_name',
allowNull: true
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
emailConfirmed: {
type: Sequelize.BOOLEAN,
field: 'email_confirmed',
allowNull: false,
defaultValue: false
},
birthDate: {
type: Sequelize.DATE,
field: 'birth_date'
}
},
{ sequelize }
)

Model instances represent database records and contain three important things:

  • The in-memory representation of the record data as persisted in the database
  • The logic for storage, retrieval, serialization, and deserialization of its data and related data.
  • The inherited business/domain logic defined in the model class

Querying

You can fetch and update a model instance with Sequelize as follows:

const ada = await User.findOne({ where: { firstName: 'Ada' } })
ada.lastName = 'Lovelace'
await ada.save()
ada.getFullName() // Ada Lovelace

Schema migrations

A key part of using a database is changing the schema to accommodate new features and to better fit the problem you're solving. 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 alter tables, 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.

Sequelize for example, has a programmatic API for migrations.

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

  1. Create the first migration:
// migrations/20191217102908-create-user.js
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
firstName: {
type: Sequelize.STRING,
field: 'first_name',
},
lastName: {
type: Sequelize.STRING,
field: 'last_name',
},
email: {
type: Sequelize.STRING,
},
emailConfirmed: {
type: Sequelize.BOOLEAN,
field: 'email_confirmed',
allowNull: false,
defaultValue: false,
},
birthDate: {
type: Sequelize.DATE,
},
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users')
},
}
  1. Create the corresponding model as in the previous section.
  2. To add a new table, create a new migration and corresponding model. To change the Users table, create a new migration which will add the field and update the model to reflect the changes.

The first two steps will be part of any schema change that uses Sequelize both as an ORM and for migrations. The second step is where the synchronization culprit is. In other words, it is where the responsibility is on the developer to ensure she has correctly defined the model in line with the database schema.

Other ORMs have a workflow similar to this. Some, like Django's ORM, support a simpler workflow where you're required to only change the models and using a CLI tool, your models are scanned and compared to the versions currently contained in your migration files, and then a new set of migrations is written out.

Now that you've seen migrations and workflows, you will look at the benefits and drawbacks of ORMs.

Benefits of ORMs

There are different reasons for 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.
  • As you evolve your schema, the changes happen in a single place: the models. This saves you from having to search and replace SQL statements scattered throughout your code.
  • ORMs help reduce the amount of code. It saves 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 and 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; this is known as the object-relational impedance mismatch. Depending on your use-case, the problem you are solving may form 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 the way the database works, understanding and 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 database toolkit that makes working with databases easy for application developers. It currently consists of two main tools:

  • Prisma Client: Auto-generated and type-safe database client
  • Prisma Migrate (experimental): A CLI for declarative data modeling and migrations

Both Prisma Client and Prisma Migrate rely on the Prisma schema. The Prisma schema is a declarative representation of your database schema. It serves as the single source truth for both the database schema and your application models.

How Prisma compares

Unlike ORMs, with Prisma you don't create model classes nor do you map fields in your codebase to database fields. Instead, you use the generated Prisma Client API like an advanced query builder that returns plain JavaScript objects. Schema changes are synced automatically from the models in the Prisma schema to the database.

Models in Prisma mean something slightly different to ORMs. When using ORMs, models are represented as classes. With Prisma, models are defined in the Prisma schema as abstract entities which describe the relationships between tables:

model User {
id Int @default(autoincrement()) @id
birthDate DateTime
email String @unique
firstName String?
lastName String?
posts Post[]
}
model Post {
content String?
post_id Int @default(autoincrement()) @id
title String
author_id User
}

Prima schema's declarative nature is concise compared to imperative model definitions in ORMs.

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

  • 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.
  • New application (greenfield): Projects that have no database schema yet can use Prisma Migrate to create the database schema.

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.
  • 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"
}
  1. Run prisma introspect
  2. 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 and carries out the migration for you.

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

  1. Create the Prisma schema:
// schema.prisma
datasource db {
provider = "sqlite"
url = "file:data.db"
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id
birthDate DateTime
email String @unique
firstName String?
lastName String?
}
  1. prisma migrate save --experimental: Save the migration. Typically migrations are saved in the code repository
  2. prisma migrate up --experimental: Run the migration which will create the database schema
  3. prisma generate: Re-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. Follow steps 2-4.

The last two steps demonstrate 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. Lastly, the Prisma Client was 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 Prisma Migrate, which is still experimental. 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.

Conclusion

Both Prisma and ORMs are powerful tools that aim to make working with databases easier and more productive.

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

The workflow with ORMs is centered around model classes and instances to encapsulate data and related business logic–a pattern fits the OOP (Object Oriented Programming) paradigm.

Prisma's fundamental differences are the declarative schema as a single source of truth and the use of plain JavaScript objects in Prisma Client.

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