prisma.io 是一个用 Scala写的 ORM-like 的服务层,可以方便的作为多种数据库的连接层。主要的特点是可以用模式化的方式将数据库规整,并自带了 Graphql / Restful / OpenAPI / gRPC 的支持,可以很方便的将数据库的基本请求client化。用途主要是自动生成相关API,自动生成数据库迁移脚本,提供安全的数据库访问,时实数据库连接能力,以及跨语言跨平台的类型安全的数据库读写操作能力。 利用 prisma 快速创建 graphql API 供客户端使用 利用 prisma 快速创建 Restful API 供客户端使用 利用 prisma 作为数据连接层供其他服务调用 更详细的特点可以查看其官网。我们今天讨论一下为什么使用 Prisma 以及什么才是解决复杂性需要的 ORM。

为什么选择 Prisma

  • Prisma 可以快速创建数据连接层,在真数据库前做隔离,增加数据库的相应能力;
  • Prisma 完美支持 graphql 协议,解决数据库连接的统一性问题;
  • Prisma 使用 graphql 的 schema,统一数据库表的管理,用一个文件做到表结构的 code-first 管理,随着代码库一起更新;
  • Prisma 支持自动 migration,对于 nodejs 各种弱弱的 ORM 的 migration 强的多;
  • Prisma 自带一个数据管理 Admin,可以理解为简化但支持多后端的 phpmyadmin;
  • Prisma 可以快速生成数据访问客户端,支持 CRUD 和比较细粒度的查询操作,支持 ts / js / golang

谁需要 Prisma

  • mobile / webapp first 的应用开发者,只需定义好表结构,就可以有现成的 graphql 后端可以使用
  • 业务变化非常快速的场景,数据表变化快,查询操作变化快。有 prisma 可以大大加快需求变化后的开发速度

简单试用 Prisma

创建基本环境

使用 docker 创建一个 mysql 结合 prisma 的开发环境。创建一个目录,在里面加入一个 docker-compose.yml 文件:

version: '3'
services:
  prisma:
    image: prismagraphql/prisma:1.31
    restart: always
    ports:
      - '4466:4466'
    environment:
      PRISMA_CONFIG: |
        port: 4466

        databases:
          default:
            connector: mysql
            host: mysql
            port: 3306
            user: root
            password: prisma
  mysql:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: prisma
    volumes:
      - ./mysql:/var/lib/mysql

在该目录下再建一个 mysql 文件夹,然后执行命令启动 docker 容器:

docker-compose up -d

服务启动后,可以在 http://localhost:4466 看到 prisma 的交互试命令行。

创建一个简单的数据表

先安装 prisma 工具链

# 安装 prisma-cli
npm install -g prisma
# 初始化连接
prisma init --endpoint [http://localhost:4466](http://localhost:4466)

此时在文件夹里会出现 prisma.yml 和 datamodel.prisma 文件,前者是 prisma 服务的定义文件,后者是数据模型文件。 我们现在模拟一个订单场景,定义如下的模型,表示一个订单数据结构: 订单模型类图 我们在 datamodel.prisma 文件中,我们写入如下代码,代表设定的数据表的结构:

type Product {
  id: ID! @id
  name: String!
  price: Int!
}

type Order {
  id: ID! @id
  totalPrice: Int!
  details: [OrderDetail!]!
}

type OrderDetail {
  id: ID! @id
  count: Int!
  product: Product! @relation(link: INLINE)
  order: Order! @relation(link: INLINE)
}

代表上图的 3 个模型的建立和表字段。然后我们使用命令:

prisma deploy

将这个数据表模型写入数据库。成功后可以通过 mysql-client 等工具查看新建的数据表,也可以使用 prisma-admin 查看数据,或者手动添加数据。

用 prisma 导出 js-client

在 prisma.yml 内加入:

generate:  
- generator: javascript-client
  output: ./generated/prisma-client/

然后执行命令:

prisma generate

就能生成 js 客户端,在目录 ./generated/prisma-client/ 下。

使用 prisma 库执行我们的测试

先在当前目录创建 nodejs 的开发环境:

# 创建 js 脚本
touch index.js
# 初始化 nodejs 环境
npm init -y
npm install --save prisma-client-lib

在 index.js 里键入如下内容,创建数据和简单的查询:

const { prisma } = require('./generated/prisma-client')

async function createDB() {
    // 创建一个新产品
    const newProduct = await prisma.createProduct({ name: 'Thinkpad', price: 10000})
    console.log(`得到返回 ${JSON.stringify(newProduct)}`)
    // 创建一个订单
    const newOrder = await prisma.createOrder({
        totalPrice: newProduct.price * 3,
        details: {
            create: [
                {
                    product: {
                        connect: {
                            id: newProduct.id
                        }
                    },
                    count: 3
                }
            ]
        }
    })

    // 读取创建的订单
    const fragment = `
        fragment OrderWithDetails on Order {
            id
            totalPrice
            details {
                id
                count
                product {
                    id
                    name
                    price
                }
            }
        }
    `
    const order = await prisma.orders().$fragment(fragment)
    console.log(`得到订单所有 ${JSON.stringify(order)}`)
}

createDB().catch(e => console.error(e))

得到结果: primsa 执行的结果

简单评价 prisma

使用简单几个步骤和前端开发者都很熟的 graphql 语句,我们就快速的创建了一个数据库表,一个 graphql 的数据 orm 客户端,可以直接在 js 客户端进行调用的库。对于熟悉前端开发这套的开发者,这个是非常方便的。

但是我们也可以看到,我在写入和查询的过程,需要对数据表结构非常熟悉,每一层结构必须清晰的表达在写入或者查询的命令中。换句话说,作为一个 ORM,他没法将数据关联到 ddd 的领域模型中,充其量就是一个贫血模型,无法有业务结构在其中。这对富业务的情景很难应用。富业务场景下,模型对数据库和数据表无感知,他知道的只是对象,对象含有领域知识,比如一个订单中,订单总价一定是订单详情的价格组合累加得到的,所以在业务场景中,应用层不会知道如何写入订单详情的数据到数据库,否则就可能产生逻辑泄漏的情况,也就是总价和真实价格不符合的情况。在简单的应用中,我们可以加厚应用层避免这种情况,可是在复杂的企业场景下,特别是应用 ddd 的架构场景下,必须做到对象为第一公民的开发形式。

比如说我前端提交了一个订单信息,在应用层我知道我收到了一个订单请求,将他转化成模型后,我使用模型里的领域方法,比如计算总价,合并重复项(一个订单内不应该有两个同样产品的订单详情),生成一个合法的订单对象,然后写入数据库。这个过程中,写入数据是一股脑儿的,我只知道是一个对象需要持久化,而不知道具体哪个字段对应着哪个数据表,这个活超出了领域知识,应该由基础设施来完成,也就是 ORM。

ORM 的作用是对象为先,先有对象,自动映射到 SQL,而不是先有数据库,再将表映射到对象。后者叫面向数据库编程,可以解决问题也很方便,但是不适合业务复杂的企业应用。前者的对象优先的 ORM,比如.net 的 EF 框架,就是这种类型,这样的 ORM 让模型与数据库无关,与存储过程无关,只与业务模型有关,理清软件的核心–业务。这也是洋葱架构的核心观点。

总上所述 prisma 不适合做面向对象编程的 orm,只能作为快速 crud 数据的用途。没法和模型强关联,所以无法作为一个单独的模块引入洋葱架构体系。需要能引入模型系统,起码要有如下功能:

  1. 自动将模型的字段映射成 graphql 的指令
  2. 自动将 crud 操作映射成 graphql 的命令
  3. 自动将关联关系的操作映射
  4. Change tracking

Prisma 适合那中逻辑比较简单,需要快速开发,甚至设计一个表,客户端就能直接用的场景,比如社交软件。但不适合重业务的场景,比如仓库管理系统。

联想:什么才是 DDD 想要的 ORM?

论坛上我和人讨论了一个小问题,如何才能让应用层无感知的让聚合根(Aggregate Root) 的修改被写入数据库?所谓聚合根就是在一个领域中会有封闭概念的多个实体,他们的逻辑形成闭环,无法将其切割。比如上面例子里的订单和订单详情,订单详情无法脱离订单存在,外界不需要也不能直接索引一个订单详情,反过来订单也需要有订单详情的信息,才能计算出正确的总价,少了任何一个订单详情,订单的逻辑就错了。这样一个结构,我们认为就是一个聚合,而订单就是这个聚合的聚合根,所有的外界的操作必须有聚合根进入而无权直接修改聚合内部的实体,避免逻辑泄漏。

那么在这么一个场景中,网友提出,我现在通过领域方法修改了某个聚合下面的实体,我如何让 ORM 知道这个修改,并写入数据库呢?如图: 如何自动知道某个聚合下的实体被修改了

因为应用层只知道聚合根被修改了,他也只能持久化聚合根,那么如何将下层的数据修改写入对应的数据库表,就是 ORM 需要解决的问题了。

传统 ORM 比如 EF 解决这个问题的方法是 change tracking,从我取出数据的那个时间,我就对所有模型进行脏数据检测,在写入数据时,我对所有的对象进行遍历,将有修改的数据写入对应的表中,实现隐式更新数据的目的。显然 Prisma 或者是 micro-orm 都没有这样的能力,因为他们只是将模型操作简单的解释成 sql 语句,但是到底是修改哪个表,还是需要显式指定。

另一个思路,我可以把整个聚合模型塞给 ORM,让 ORM 不管三七二十一,将所有模型解释成 sql 语句,给数据库写一遍。这个思路其实是把 change tracking 的工作交给数据库自己去完成。说起来巧了,postgresql 就支持这个能力,我们将整个聚合转换成 json,塞给数据库,数据库就能自己去完成写入操作,怪不得说 postgresql 是最适合 DDD 的数据库了。