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 graphql @gqloom/core @gqloom/mikro-ormpnpm add graphql @gqloom/core @gqloom/mikro-ormyarn add graphql @gqloom/core @gqloom/mikro-ormbun add graphql @gqloom/core @gqloom/mikro-ormdeno add npm:graphql npm:@gqloom/core npm:@gqloom/mikro-orm使用丝线
只需要使用 mikroSilk 包裹 MikroORM Entity,即可将它们作为丝线使用。定义实体的方式有两种:
使用 defineEntity 定义实体时,用 mikroSilk 包裹即可。在解析器中使用之前需初始化 MikroORM,示例如下:
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).ref(),
}),
})
export interface IPost extends InferEntity<typeof PostEntity> {}
export const User = mikroSilk(UserEntity)
export const Post = mikroSilk(PostEntity)import { createMemoization, useResolvingFields } from "@gqloom/core/context"
import { MikroORM } from "@mikro-orm/libsql"
import { Post, User } from "./entities"
export const orm = MikroORM.initSync({
entities: [User, Post],
dbName: ":memory:",
})
export const useEm = createMemoization(() => orm.em.fork())
export const useSelectedFields = () => {
return Array.from(useResolvingFields()?.selectedFields ?? ["*"]) as []
}import { weave } from "@gqloom/core"
import { MikroResolverFactory } from "@gqloom/mikro-orm"
import { Post, User } from "./entities"
import { useEm } from "./provider"
const userResolver = new MikroResolverFactory(User, useEm).resolver()
const postResolver = new MikroResolverFactory(Post, useEm).resolver()
export const schema = weave(userResolver, postResolver)现在我们可以在解析器中使用它们了:
手写解析器
你可以直接在 resolver 里使用 mikroSilk 包裹的 MikroORM 实体:
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 变更。
关键要点
- Entity Manager:通过
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: field.hidden 后,该字段将不会出现在生成的 GraphQL Schema 中。
混合字段
对 json、enum 等字段,若要在 TypeScript 与 GraphQL 中一致地推导出类型,可借助 valibot 或 zod:
import { mikroSilk } from "@gqloom/mikro-orm"
import { asEnumType } from "@gqloom/valibot"
import { defineEntity, type InferEntity, p } from "@mikro-orm/core"
import * as v from "valibot"
const Role = v.pipe(
v.picklist(["admin", "user"]),
asEnumType({
name: "Role",
valuesConfig: {
admin: { description: "Admin user" },
user: { description: "Regular user" },
},
})
)
const ContactInformation = v.object({
email: v.nullish(v.string()),
phone: v.nullish(v.string()),
address: v.nullish(v.string()),
})
const UserEntity = defineEntity({
name: "User",
properties: {
id: p.integer().primary().autoincrement(),
createdAt: p.datetime().onCreate(() => new Date()),
name: p.string(),
role: p.enum(Role.options).onCreate(() => "user"),
contactInformation: p
.json<v.InferOutput<typeof ContactInformation>>()
.nullable(),
},
})
export interface IUser extends InferEntity<typeof UserEntity> {}
export const User = mikroSilk(UserEntity, {
fields: {
role: Role,
contactInformation: v.nullish(ContactInformation),
},
})解析器工厂
除了手写解析器,@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)MikroResolverFactory 的构造函数支持两种用法:
new MikroResolverFactory(Entity, getEntityManager):传入实体和获取EntityManager的函数。new MikroResolverFactory(Entity, options):传入实体和配置对象{ getEntityManager, input? }。
说明
input 选项用于配置各字段在 filter / create / update 中的可见性与校验。
关系字段
解析器工厂提供了 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 关系。
查询
解析器工厂预置了常用的查询方法,它们在内部调用 EntityManager 的对应方法:
- countQuery — 计数
- findQuery — 列表查询
- findAndCountQuery — 列表 + 总数
- findByCursorQuery — 游标分页
- findOneQuery — 单条查询(可空)
- findOneOrFailQuery — 单条查询(不存在则抛错)
查询的 where 参数会生成对应的 Filter 类型。通过 MikroWeaver.config 的 dialect 选项可以控制是否暴露 PostgreSQL 专有操作符(如 ilike、overlap),从而在不同数据库下生成兼容的 API。
你可以直接使用它们:
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 — 批量 upsert
内置持久化
工厂提供的变更方法已内置 em.flush(),通常无需手动添加 flusher 中间件。
你可以直接使用它们:
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 变更。工厂将自动创建输入类型和解析函数。
自定义输入字段
通过构造函数中的 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 } }))
)
),
})以上示例将输入参数手动转换为 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()MikroResolverFactory 提供了两个核心方法来生成 Resolver:
queriesResolver(name?):创建一个仅包含查询与关系字段的解析器。resolver(name?):在查询与关系字段的基础上,增加变更字段(如createUser、updateUser等)。
提示
可选参数 name 用于控制字段名前缀。例如传入 "User" 会生成 findOneUser、createUser 等字段。
Weaver 配置与自定义类型映射
通过 MikroWeaver.config 统一配置织入行为。推荐在应用中全局设置一次,并在织入 Schema 时传入:
presetGraphQLType(property):覆盖默认的类型映射。dialect:设置数据库方言(如"PostgreSQL","MySQL","SQLite","MongoDB"),用于精简 Filter 中的操作符。
示例:将 datetime 映射为 GraphQLDateTime。
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 时传入该配置:
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 |