The idea behind ORM(Object Relational Mappers) is to forcefully map the data model to Object, so that our ability to add, delete, or alter data can be converted into manipulation of Object.

Prisma is a modern Nodejs ORM library. See the official Prisma documentation to see how the library is designed and used.

An overview of the

Prisma provides a large number of tools, including Prisma Schema, Prisma Client, Prisma Migrate, Prisma CLI, Prisma Studio, etc. Prisma Schema and Prisma Client are the two most important ones, which describe application data model and Node operation API respectively.

Primsa uses a new syntax, Primsa Schema, to describe data models. Prisma generate generates a configuration file stored in node_modules/. Prisma /client.

Prisma Schema

Primsa Schema abstracts the relational relationships as closely as possible on the basis of the database structure description, and maintains the corresponding relationship with the data model behind it, which is well illustrated in the following figure:

It can be seen that, almost the same as the definition of the database, the only extra posts and authors actually make up for the unintuitive part of the foreign keys associated with the database table. These foreign keys are converted into entity objects, so that the existence of foreign keys or multiple tables can not be felt in the operation, and then converted into join operation in the specific operation. Here is the corresponding Prisma Schema:

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[]
}
Copy the code

Datasource DB declares the linked database information. The Generator Client declares that Prisma Client is used for client-side operations, which means that Prisma Client is in fact interchangeable; The model is the core model definition.

In the model definition, @map can change the field name mapping and @@map can change the table name mapping. By default, the field name is the same as the key name:

model Comment {
  title @map("comment_title")

  @@map("comments")
}
Copy the code

The field consists of the following four descriptions:

  • The field name.
  • Field type.
  • Optional type modification.
  • Optional attribute description.
model Tag {
  name String? @id
}
Copy the code

In this description, include field name, field type String, type modifier? , attribute Description @id.

The field type

The field type can be Model, such as the associative type field scenario:

model Post {
  id       Int       @id @default(autoincrement())
  // Other fields
  comments Comment[] // A post can have many comments
}

model Comment {
  id     Int
  // Other fields
  Post   Post? @relation(fields: [postId], references: [id]) // A comment can have one post
  postId Int?
}
Copy the code

There are four related scenarios: 1V1, NV1, 1VN and NVN. The field type can be the defined model name, and the attribute @relation is used to define the correlation. For example, here we describe the nV1 relationship between the commencement and Post. And comment.postid is associated with post.id.

Field types can also be underlying data types, as described by @db., for example:

model Post {
  id @db.TinyInt(1)
}
Copy the code

For types not supported by Prisma, you can also use the Unsupported modifier:

model Post {
  someField Unsupported("polygon")?
}
Copy the code

This type of field cannot be queried using the ORM API, but can be queried using queryRaw. QueryRaw is ORM support for raw SQL schema, which is covered in Prisma Client.

Type modified

The type modifier has? [] Two grammars, such as:

model User {
  name  String?
  posts Post[]
}
Copy the code

Represents optional and array respectively.

Property description

Attribute descriptions have the following syntax:

model User {
  id        Int     @id @default(autoincrement())
  isAdmin   Boolean @default(false)
  email     String  @unique

  @@unique([firstName, lastName])
}
Copy the code

@id Specifies the PRIMARY KEY of the database.

@default Sets the default value of the field, and can be used in conjunction with functions such as @default(autoincrement()), autoincrement(), cuID (), uuid(), now(), etc. You can also call the underlying database functions directly from dbgenerated, such as dbgenerated(“gen_random_uuid()”).

@unique sets the field value to be unique.

@relation sets the relation, which was mentioned above.

@map sets the mapping, as mentioned above.

The @updatedat modifier field is used to store the last update date, usually as a built-in database capability.

@ignore Invalid field for Prisma tag.

All attribute descriptions can be combined, and there is also a description for model level. Generally, two @ descriptions are used, including @@ID, @@unique, @@index, @@map and @@ignore.

ManyToMany

Prisma also made efforts in the description of many-to-many associations, supporting implicit association description:

model Post {
  id         Int        @id @default(autoincrement())
  categories Category[]
}

model Category {
  id    Int    @id @default(autoincrement())
  posts Post[]
}
Copy the code

It looks natural, but there’s a lot of implementation behind it. A many-to-many relationship is typically implemented in a third table, which stores the foreign key mapping between the two tables, so if you want to explicitly define it, it looks like this:

model Post {
  id         Int                 @id @default(autoincrement())
  categories CategoriesOnPosts[]
}

model Category {
  id    Int                 @id @default(autoincrement())
  posts CategoriesOnPosts[]
}

model CategoriesOnPosts {
  post       Post     @relation(fields: [postId], references: [id])
  postId     Int // relation scalar field (used in the `@relation` attribute above)
  category   Category @relation(fields: [categoryId], references: [id])
  categoryId Int // relation scalar field (used in the `@relation` attribute above)
  assignedAt DateTime @default(now())
  assignedBy String

  @@id([postId, categoryId])
}
Copy the code

The following SQL is generated behind:

CREATE TABLE "Category" (
    id SERIAL PRIMARY KEY
);
CREATE TABLE "Post" (
    id SERIAL PRIMARY KEY
);
-- Relation table + indexes -------------------------------------------------------
CREATE TABLE "CategoryToPost" (
    "categoryId" integer NOT NULL,
    "postId" integer NOT NULL,
    "assignedBy" text NOT NULL
    "assignedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP.FOREIGN KEY ("categoryId")  REFERENCES "Category"(id),
    FOREIGN KEY ("postId") REFERENCES "Post"(id)
);
CREATE UNIQUE INDEX "CategoryToPost_category_post_unique" ON "CategoryToPost"("categoryId" int4_ops,"postId" int4_ops);
Copy the code

Prisma Client

NPM install@prisma /client: Prisma generate: NPM install@prisma /client

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()
Copy the code

CRUD

Create a record with create:

const user = await prisma.user.create({
  data: {
    email: '[email protected]'.name: 'Elsa Prisma',}})Copy the code

Create multiple records using createMany:

const createMany = await prisma.user.createMany({
  data: [{name: 'Bob'.email: '[email protected]' },
    { name: 'Bobo'.email: '[email protected]' }, // Duplicate unique key!
    { name: 'Yewande'.email: '[email protected]' },
    { name: 'Angelique'.email: '[email protected]'},].skipDuplicates: true.// Skip 'Bobo'
})
Copy the code

Use findUnique to find a single record:

const user = await prisma.user.findUnique({
  where: {
    email: '[email protected]',}})Copy the code

In the case of federated indexes:

model TimePeriod {
  year    Int
  quarter Int
  total   Decimal

  @@id([year, quarter])
}
Copy the code

We need to nest another layer of _ concatenated keys:

const timePeriod = await prisma.timePeriod.findUnique({
  where: {
    year_quarter: {
      quarter: 4.year: 2020,}}})Copy the code

Query multiple records with findMany:

const users = await prisma.user.findMany()
Copy the code

You can use a variety of conditional statements in SQL. The syntax is as follows:

const users = await prisma.user.findMany({
  where: {
    role: 'ADMIN',},include: {
    posts: true,}})Copy the code

Update records with update:

const updateUser = await prisma.user.update({
  where: {
    email: '[email protected]',},data: {
    name: 'Viola the Magnificent',}})Copy the code

Update multiple records using updateMany:

const updateUsers = await prisma.user.updateMany({
  where: {
    email: {
      contains: 'prisma.io',}},data: {
    role: 'ADMIN',}})Copy the code

Delete a record using delete:

const deleteUser = await prisma.user.delete({
  where: {
    email: '[email protected]',}})Copy the code

Use deleteMany to delete multiple records:

const deleteUsers = await prisma.user.deleteMany({
  where: {
    email: {
      contains: 'prisma.io',}}})Copy the code

Use include to indicate whether the associated query takes effect. For example:

const getUser = await prisma.user.findUnique({
  where: {
    id: 19,},include: {
    posts: true,}})Copy the code

This will query all associated POST tables along with the user table. Relational queries also support nesting:

const user = await prisma.user.findMany({
  include: {
    posts: {
      include: {
        categories: true,},},},})Copy the code

Filter criteria support equals, NOT, in, notIn, LT, LTE, GT, GTE, contains, search, mode, startsWith, endsWith, AND, OR, AND NOT.

const result = await prisma.user.findMany({
  where: {
    name: {
      equals: 'Eleanor',}}})Copy the code

This statement replaces SQL where name=”Eleanor”, which expresses semantics through nested objects.

Prisma can also write native SQL directly:

const email = '[email protected]'
const result = await prisma.$queryRaw(
  Prisma.sql`SELECT * FROM User WHERE email = ${email}`
)
Copy the code

The middleware

The way Prisma supports middleware is extended during execution, as shown in the following example:

const prisma = new PrismaClient()

// Middleware 1
prisma.$use(async (params, next) => {
  console.log(params.args.data.title)
  console.log('1')
  const result = await next(params)
  console.log('6')
  return result
})

// Middleware 2
prisma.$use(async (params, next) => {
  console.log('2')
  const result = await next(params)
  console.log('5')
  return result
})

// Middleware 3
prisma.$use(async (params, next) => {
  console.log('3')
  const result = await next(params)
  console.log('4')
  return result
})

const create = await prisma.post.create({
  data: {
    title: 'Welcome to Prisma Day 2020',}})const create2 = await prisma.post.create({
  data: {
    title: 'How to Prisma! ',}})Copy the code

The output is as follows:

Welcome to Prisma Day 2020 1 2 3 4 5 6 How to Prisma! One, two, three, four, five, sixCopy the code

As you can see, the middleware execution order is an Onion model and every operation is triggered. We can use middleware to extend business logic or to keep track of operation time.

Intensive reading

Two design patterns for ORM

ORM has two design patterns, Active Record and Data Mapper. Active Record makes the object completely correspond to the SQL query behind it, which is not very popular now, and the object in Data Mapper mode does not know the existence of the database. That is, there is a layer of mapping in the middle, and there is not even a corresponding database behind it, so you can do some very light debugging functions.

Prisma uses the Data Mapper pattern.

ORM is prone to performance problems

When the data volume is large, or performance or resource sensitive, we need to optimize SQL, or even we need to do some seemingly meaningless statement tuning for certain kernel errors IN a particular version of Mysql (such as IN scope before WHERE). Sometimes you can achieve amazing performance improvements.

ORM is based on an idealized theory, that is, the data model can be transformed into object operations, but object operations are shielded from details, so we can’t tune SQL specifically.

In addition, thanks to the convenience of object operation, we can easily pass obj. Obj. When we write these SQL, we consider performance factors in advance, but when we call them through objects, we think ORM has the idea of magic optimization, etc. Write a lot of SQL that actually doesn’t make sense.

Benefits of Prisma Schema

Prisma Schema is syntactically equivalent to Typeorm’s Class + decorator-based extensions, but Prisma Schema has the advantage of reducing boilerplate code and stable database models.

Reducing boilerplate code is easier to understand because Prisma Schema does not appear in the code, whereas a stable model means that the data model does not change as long as Prisma generate is not executed, and Prisma Schema exists independently of Node. It can even be left out of the project’s source code, which can be modified more discreetly, whereas models defined entirely in Node can be changed suddenly because they are part of the code and do not perform database structure synchronization.

If Prisma is used in the project, the process of executing Prisma DB pull to update the database structure and Prisma Generate to update the client API after the model changes is clear.

conclusion

Prisma Schema is a feature of Prisma, because this part of the description is independent of the code, which brings the following benefits:

  1. Definition is much cleaner than Node Class.
  2. No redundant code structures are generated.
  3. Prisma Client is much lighter and queries return Pure Objects.

As for the Prisma Client API design, there is no significant improvement over either Sequelize or TypeorM API design, but the style is different.

For record creation, though, I prefer Prisma’s API:

// typeorm - save API
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)

// typeorm - insert API
const userRepository = getManager().getRepository(User)
userRepository.insert({
  name: 'Alice',})// sequelize
const user = User.build({
  name: 'Alice',})await user.save()

// Mongoose
const user = await User.create({
  name: 'Alice'.email: '[email protected]',})// prisma
const newUser = await prisma.user.create({
  data: {
    name: 'Alice',}})Copy the code

First, prisMA, the top-level variable, is very convenient to use. In addition, in terms of API expansion, although Mongoose is designed more succinct, it will be less extensible when some conditions are added, resulting in unstable structure, which is not conducive to unified memory.

The Prisma Client API uses the following structure:

await prisma.modelName.operateName({
  // Data, such as create, update
  data: / * *... * /.// Condition, most cases can be used
  where: / * *... * /.// Other special arguments, or operater-specific arguments
})
Copy the code

So overall, Prisma doesn’t revolutionize ORM, but it does a good job of micro-innovation and API optimization, and github updates are active, so if you decide to use ORM for your development project, Prisma is recommended.

In practice, Prisma Middleware can be used to monitor query performance and use Prisma.$queryRaw native SQL queries where performance is poor to avoid performance problems caused by ORM’s clumsy SQL.

The discussion address is: Close reading the Use of Prisma · Issue #362 · dt-fe/weekly

If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.

Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)