MikroORM
MikroORM 是一个适用于 Node.js 的 TypeScript ORM,支持 PostgreSQL、MySQL、MariaDB、SQLite 和 MongoDB 等多种数据库。它基于 Data Mapper、Unit of Work 和 Identity Map 模式,旨在提供一个功能强大且易于使用的数据库工具集。
@gqloom/mikro-orm
提供了 GQLoom 与 MikroORM 的集成:
- 使用 MikroORM Entity 作为丝线使用;
- 使用解析器工厂从 MikroORM 快速生成 CRUD 操作。
安装
请参考 MikroORM 的入门指南安装 MikroORM 与对应的数据库驱动。
在完成 MikroORM 安装后,安装 @gqloom/mikro-orm
:
npm i @gqloom/core @gqloom/mikro-orm
pnpm add @gqloom/core @gqloom/mikro-orm
yarn add @gqloom/core @gqloom/mikro-orm
bun add @gqloom/core @gqloom/mikro-orm
使用丝线
只需要使用 mikroSilk
包裹 MikroORM Entity,我们就可以轻松地将它们作为丝线使用。
export const User = mikroSilk(UserEntity)
export const Post = mikroSilk(PostEntity)
import { mikroSilk } from "@gqloom/mikro-orm"
import { type InferEntity, defineEntity } from "@mikro-orm/core"
const UserEntity = defineEntity({
name: "User",
properties: (p) => ({
id: p.integer().primary().autoincrement(),
createdAt: p.datetime().onCreate(() => new Date()),
email: p.string(),
name: p.string(),
role: p.string().$type<"admin" | "user">().default("user"),
posts: () => p.oneToMany(PostEntity).mappedBy("author"),
}),
})
export interface IUser extends InferEntity<typeof UserEntity> {}
const PostEntity = defineEntity({
name: "Post",
properties: (p) => ({
id: p.integer().primary().autoincrement(),
createdAt: p.datetime().onCreate(() => new Date()),
updatedAt: p
.datetime()
.onCreate(() => new Date())
.onUpdate(() => new Date()),
published: p.boolean().default(false),
title: p.string(),
author: () => p.manyToOne(UserEntity),
}),
})
export interface IPost extends InferEntity<typeof PostEntity> {}
export const User = mikroSilk(UserEntity)
export const Post = mikroSilk(PostEntity)
在解析器中使用它们之前,我们需要初始化 MikroORM 并提供一个请求作用域的 Entity Manager。
import type { Middleware } from "@gqloom/core"
import { createMemoization, useResolvingFields } from "@gqloom/core/context"
import { MikroORM } from "@mikro-orm/libsql"
import { Post, User } from "./entities"
export let orm: MikroORM
export const ormPromise = MikroORM.init({
entities: [User, Post],
dbName: ":memory:",
debug: true,
}).then(async (o) => {
orm = o
await orm.getSchemaGenerator().updateSchema()
})
export const useEm = createMemoization(() => orm.em.fork())
export const useSelectedFields = () => {
return Array.from(useResolvingFields()?.selectedFields ?? ["*"]) as []
}
export const flusher: Middleware = async ({ next }) => {
const result = await next()
await useEm().flush()
return result
}
现在我们可以在解析器中使用它们了:
import { field, mutation, query, resolver } from "@gqloom/core"
import * as v from "valibot"
import { Post, User } from "./entities"
import { flusher, useEm, useSelectedFields } from "./provider"
export const userResolver = resolver.of(User, {
user: query(User.nullable())
.input({ id: v.number() })
.resolve(async ({ id }) => {
const user = await useEm().findOne(
User,
{ id },
{ fields: useSelectedFields() }
)
return user
}),
users: query(User.list()).resolve(() => {
return useEm().findAll(User, { fields: useSelectedFields() })
}),
createUser: mutation(User)
.input({
data: v.object({
name: v.string(),
email: v.string(),
}),
})
.use(flusher)
.resolve(async ({ data }) => {
const user = useEm().create(User, data)
useEm().persist(user)
return user
}),
posts: field(Post.list())
.derivedFrom("id")
.resolve((user) => {
return useEm().find(
Post,
{ author: user.id },
{ fields: useSelectedFields() }
)
}),
})
如上面的代码所示,我们可以直接在 resolver
里使用 mikroSilk
包裹的 MikroORM 实体。在这里我们使用了 User
作为 resolver.of
的父类型,并定义了 user
和 users
两个查询,以及一个 createUser
变更。
所有数据库操作都通过 useEm()
获取的请求作用域 Entity Manager 来执行。
对于变更操作,我们使用了一个 flusher
中间件,它会在变更操作成功后自动调用 em.flush()
来将更改持久化到数据库。
我们还通过 useSelectedFields()
函数来确保只选择 GraphQL 查询中请求的字段,这有助于优化数据库查询性能。此函数需要启用上下文。
派生字段
为数据库实体添加派生字段非常简单:
import { field, resolver } from "@gqloom/core"
import * as v from "valibot"
import { type IUser, User } from "./entities"
export const userResolver = resolver.of(User, {
display: field(v.string())
.derivedFrom("name", "email")
.resolve((user) => {
return `${user.name} <${user.email}>`
}),
})
注意:派生字段需要使用 derivedFrom
方法声明所依赖的列,以便 useSelectedFields
方法能正确地选取所需要的列。
隐藏字段
@gqloom/mikro-orm
默认将暴露所有字段。如果你希望隐藏某些字段,例如密码,你可以使用 field.hidden
:
import { field, resolver } from "@gqloom/core"
import { User } from "./entities"
export const userResolver = resolver.of(User, {
password: field.hidden,
})
在上面的代码中,我们隐藏了 password
字段,这意味着它将不会出现在生成的 GraphQL Schema 中。
解析器工厂
@gqloom/mikro-orm
提供了 MikroResolverFactory
来帮助你创建解析器工厂。 使用解析器工厂,你可以快速定义常用的查询、变更和字段,解析器工厂还预置了常见操作的输入类型,使用解析器工厂可以大大减少样板代码,这在快速开发时非常有用。
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import { Post, User } from "./entities"
import { useEm } from "./provider"
export const userResolverFactory = new MikroResolverFactory(User, useEm)
export const postResolverFactory = new MikroResolverFactory(Post, useEm)
在上面的代码中,我们为 User
和 Post
模型创建了的解析器工厂。MikroResolverFactory
接受两个参数,第一个是作为丝线的实体,第二个是返回 EntityManager
实例的函数。
关系字段
解析器工厂提供了 referenceField
和 collectionField
方法来定义关系字段:
import { field, query, resolver } from "@gqloom/core"
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import * as v from "valibot"
import { Post, User } from "./entities"
import { useEm } from "./provider"
export const userResolverFactory = new MikroResolverFactory(User, useEm)
export const postResolverFactory = new MikroResolverFactory(Post, useEm)
export const userResolver = resolver.of(User, {
user: userResolverFactory.findOneQuery(),
posts: userResolverFactory.collectionField('posts'),
})
export const postResolver = resolver.of(Post, {
author: postResolverFactory.referenceField('author'),
})
在上面的代码中,我们使用 userResolverFactory.collectionField('posts')
和 postResolverFactory.referenceField('author')
来定义关系字段。collectionField
用于 one-to-many
和 many-to-many
关系,而 referenceField
用于 many-to-one
和 one-to-one
关系。
查询
解析器工厂预置了常用的查询:
你可以直接使用它们:
import { query, resolver } from "@gqloom/core"
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import * as v from "valibot"
import { User } from "./entities"
import { useEm } from "./provider"
export const userResolverFactory = new MikroResolverFactory(User, useEm)
export const userResolver = resolver.of(User, {
user: userResolverFactory.findOneQuery(),
posts: userResolverFactory.collectionField('posts'),
})
在上面的代码中,我们使用 userResolverFactory.findOneQuery()
来定义 user
查询。解析器工厂将自动创建输入类型和解析函数。
变更
解析器工厂预置了常用的变更:
- createMutation
- insertMutation
- insertManyMutation
- deleteMutation
- updateMutation
- upsertMutation
- upsertManyMutation
你可以直接使用它们:
import { resolver } from "@gqloom/core"
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import { Post } from "./entities"
import { useEm } from "./provider"
export const postResolverFactory = new MikroResolverFactory(Post, useEm)
export const postResolver = resolver.of(Post, {
createPost: postResolverFactory.createMutation(),
author: postResolverFactory.referenceField('author'),
})
在上面的代码中,我们使用 postResolverFactory.createMutation()
来定义 createPost
变更。工厂将自动创建输入类型和解析函数。
自定义输入字段
解析器工厂默认预置的输入是可以配置的,通过在构造 MikroResolverFactory
时传入 input
选项,可以配置各个字段的输入验证行为和展示行为:
import { field } from "@gqloom/core"
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import * as v from "valibot"
const userFactory = new MikroResolverFactory(User, {
getEntityManager: useEm,
input: {
email: v.pipe(v.string(), v.email()), // 验证邮箱格式
password: {
filters: field.hidden, // 在查询过滤器中隐藏该字段
create: v.pipe(v.string(), v.minLength(6)), // 在创建时验证最小长度为6
update: v.pipe(v.string(), v.minLength(6)), // 在更新时验证最小长度为6
},
},
})
自定义输入对象
解析器工厂预置的查询和变更支持自定义输入对象,你可以通过 input
选项来定义输入类型:
import { resolver } from "@gqloom/core"
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import * as v from "valibot"
import { User } from "./entities"
import { useEm } from "./provider"
export const userResolverFactory = new MikroResolverFactory(User, useEm)
export const userResolver = resolver.of(User, {
user: userResolverFactory.findOneQuery().input(
v.pipe(
v.object({ id: v.number() }),
v.transform(({ id }) => ({ where: { id } }))
)
),
})
在上面的代码中,我们使用 valibot
来定义输入类型,v.object({ id: v.number() })
定义了输入对象的类型,v.transform(({ id }) => ({ where: { id } }))
将输入参数转换为 MikroORM 的查询参数。
添加中间件
解析器工厂预置的查询、变更和字段支持添加中间件,你可以通过 use
方法来添加中间件:
import { resolver } from "@gqloom/core"
import { createMemoization } from "@gqloom/core/context"
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import { GraphQLError } from "graphql"
import { Post } from "./entities"
import { useEm } from "./provider"
const postResolverFactory = new MikroResolverFactory(Post, useEm)
const useAuthedUser = createMemoization(async () => ({ id: 1, name: "test" }))
const postResolver = resolver.of(Post, {
createPost: postResolverFactory.createMutation().use(async (next) => {
const user = await useAuthedUser()
if (user == null) throw new GraphQLError("Please login first")
return next()
}),
})
在上面的代码中,我们使用 use
方法来添加一个中间件,useAuthedUser()
是一个自定义的函数,用于获取当前登录的用户,如果用户未登录,则抛出一个错误,否则调用 next()
继续执行。
完整解析器
你可以从解析器工厂中直接创建一个完整解析器:
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import { User } from "./entities"
import { useEm } from "./provider"
export const userResolverFactory = new MikroResolverFactory(User, useEm)
// Readonly Resolver
const userQueriesResolver = userResolverFactory.queriesResolver()
// Full Resolver
const userResolver = userResolverFactory.resolver()
有两个用于创建 Resolver 的函数:
usersResolverFactory.queriesResolver()
: 创建一个只包含查询、关系字段的 Resolver。usersResolverFactory.resolver()
: 创建一个包含所有查询、变更和关系字段的 Resolver。
自定义类型映射
为了适应更多的 MikroORM 类型,我们可以拓展 GQLoom 为其添加更多的类型映射。
首先我们使用 MikroWeaver.config
来定义类型映射的配置。这里我们导入来自 graphql-scalars 的 GraphQLDateTime
,当遇到 datetime
类型时,我们将其映射到对应的 GraphQL 标量。
import { MikroWeaver } from "@gqloom/mikro-orm"
import { GraphQLDateTime } from "graphql-scalars"
export const mikroWeaverConfig = MikroWeaver.config({
presetGraphQLType: (property) => {
if (property.type === "datetime") {
return GraphQLDateTime
}
},
})
在编织 GraphQL Schema 时传入配置到 weave 函数中:
export const schema = weave(mikroWeaverConfig, userResolver, postResolver)
默认类型映射
下表列出了 GQLoom 中 MikroORM 类型与 GraphQL 类型之间的默认映射关系:
MikroORM 类型 | GraphQL 类型 |
---|---|
(primary) | GraphQLID |
string | GraphQLString |
number | GraphQLFloat |
float | GraphQLFloat |
double | GraphQLFloat |
decimal | GraphQLFloat |
integer | GraphQLInt |
smallint | GraphQLInt |
mediumint | GraphQLInt |
tinyint | GraphQLInt |
bigint | GraphQLInt |
boolean | GraphQLBoolean |
(other) | GraphQLString |