使用 tRPC 和 Next.js 应用设置 Drizzle 和 Postgres
在本教程中,我们将学习如何使用 Drizzle ORM 将 Postgres 数据库连接到 tRPC express 后端。我还为我们的财务跟踪器应用程序创建了一个简单的前端。您可以从此处的存储库复制前端代码。

这是第 2 部分,请在此处阅读第 1 部分:使用 tRPC 和 Next.js 14 构建全栈应用程序
后端
如果您没有在本地安装 Postgres,请安装它,或者您也可以使用托管数据库。
一旦准备好 Postgres,请将 `DATABASE_URL` 添加到你的 `.env` 中:
DATABASE_URL=postgres://postgres:password@localhost:5432/myDB
使用 drizzle 设置数据库
要设置 drizzle,首先安装以下软件包:
yarn add drizzle-orm pg dotenv yarn add -D drizzle-kit tsx @types/pg
现在,您要做的就是将 drizzle 连接到数据库。为此,请创建 `src/utils/db.ts` 文件并配置 drizzle:
import { drizzle } from "drizzle-orm/node-postgres"; import pg from "pg"; const { Pool } = pg; export const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: process.env.NODE_ENV === "production", }); export const db = drizzle(pool);
就这样!我们的数据库设置已准备就绪。现在我们可以创建表并使用 drizzle ORM 与数据库交互。
创建第一个模块
关于项目结构,主要有两种类型:
. └── feature/ ├── feature.controller.ts ├── feature.routes.ts ├── feature.schema.ts └── feature.service.ts
. ├── controllers/ │ ├── feature1.controller.ts │ └── feature2.controller.ts ├── services/ │ ├── feature1.service.ts │ └── feature2.service.ts └── models/ ├── feature1.model.ts └── feature2.model.ts
我个人更喜欢模块,因为它很有意义()。
现在,让我们创建第一个名为“transaction”的模块。这是我们的核心功能。首先创建“src/modules/transaction/transaction.schema.ts”文件。这是我们使用 drizzle 定义事务模式的地方。
使用 drizzle 编写模式的优点在于它允许我们使用 typescript。因此您无需学习新语法并确保模式的类型安全。
要记录一笔交易(txn),我们需要的最基本的东西是:
首先,让我们为交易类型和标签创建枚举:
import { pgEnum, } from "drizzle-orm/pg-core"; export const txnTypeEnum = pgEnum("txnType", ["Incoming", "Outgoing"]); export const tagEnum = pgEnum("tag", [ "Food", "Travel", "Shopping", "Investment", "Salary", "Bill", "Others", ]);
然后,让我们创建模式:
import { integer, pgTable, serial, text, timestamp, } from "drizzle-orm/pg-core"; export const transactions = pgTable("transactions", { id: serial("id").primaryKey(), amount: integer("amount").notNull(), txnType: txnTypeEnum("txn_type").notNull(), summary: text("summary"), tag: tagEnum("tag").default("Others"), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => new Date()), });
如你所见,我们只是编写了 Typescript 代码并创建了一个表!
运行迁移
在我们开始与数据库交互之前的最后一步是将更改应用于数据库,以便创建所有表。为此,我们必须运行迁移。Drizzle 有一个很棒的工具,称为“drizzle-kit”,它可以为我们处理迁移,所以我们所要做的就是运行一个命令。
在此之前,我们必须在项目根目录中创建一个名为“drizzle.config.ts”的文件,其中包含有关数据库和模式的所有信息。
import "dotenv/config"; import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./src/**/*.schema.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL!, ssl: process.env.NODE_ENV === "production", }, });
准备就绪后,运行以下命令:
yarn dlx drizzle-kit push
就这样!现在我们可以开始与数据库交互并编写业务逻辑了。
业务逻辑
让我们添加逻辑来添加新的交易。
如果你还不知道:
创建“transaction/transaction.service.ts”并编写逻辑以将新交易添加到数据库:
import { TRPCError } from "@trpc/server"; import { db } from "../../utils/db"; import { transactions } from "./transaction.schema"; export default class TransactionService { async createTransaction(data: typeof transactions.$inferInsert) { try { return await db.insert(transactions).values(data).returning(); } catch (error) { console.log(error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to create transaction", }); } } }
使用 drizzle ORM 的另一个好处是它为不同的 CRUD 方法(如 `$inferInsert`、`$inferSelect`)提供了类型定义,因此无需再次定义类型。在这里,通过使用 `typeof transaction.$inferInsert`,我们不必为主键等字段以及具有默认值的字段(如 `createdAt` 和 `updatedAt`)提供值,因此 typescript 不会抛出错误。
Drizzle 还具有 drizzle-zod 等扩展,可用于生成 zod 模式。drizzle 🫡 避免了另一个令人头疼的问题。因此,打开“transaction.schema.ts”并为插入操作创建 zod 模式:
import { createInsertSchema } from "drizzle-zod"; export const insertUserSchema = createInsertSchema(transactions).omit({ id: true, createdAt: true, updatedAt: true, });
让我们在控制器中使用它,创建“transaction/transaction.controller.ts”:
export default class TransactionController extends TransactionService { async createTransactionHandler(data: typeof transactions.$inferInsert) { return await super.createTransaction(data); } }
现在,剩下的就是通过端点公开此控制器。为此,请创建“transaction/transaction.routes.ts”。由于我们使用 tRPC,因此要创建端点,我们必须定义一个过程:
import { publicProcedure, router } from "../../trpc"; import TransactionController from "./transaction.controller"; import { insertUserSchema } from "./transaction.schema"; const transactionRouter = router({ create: publicProcedure .input(insertUserSchema) .mutation(({ input }) => new TransactionController().createTransactionHandler(input) ), }); export default transactionRouter;
如果您还记得第 1 部分,我们创建了一个可重用的“路由器”,可用于对程序进行分组,以及创建端点的“publicProcedure”。
最后,打开`src/routes.ts`并使用上面的`transactionRouter`:
import transactionRouter from "./modules/transaction/transaction.routes"; import { router } from "./trpc"; const appRouter = router({ transaction: transactionRouter, }); export default appRouter;
就这样!后端已经准备好了。这是最终的后端结构:
. ├── README.md ├── drizzle │ ├── 0000_true_junta.sql │ └── meta │ ├── 0000_snapshot.json │ └── _journal.json ├── drizzle.config.ts ├── package.json ├── src/ │ ├── index.ts │ ├── modules/ │ │ └── transaction/ │ │ ├── transaction.controller.ts │ │ ├── transaction.routes.ts │ │ ├── transaction.schema.ts │ │ └── transaction.service.ts │ ├── routes.ts │ ├── trpc.ts │ └── utils/ │ ├── db.ts │ └── migrate.ts ├── tsconfig.json └── yarn.lock
给你挑战
在进行前端集成之前,作为一项挑战,创建一个用于获取所有交易的端点。
前端
现在是时候将创建的端点集成到我们的前端了。由于这不是前端教程,所以我让你直接从 repo 中复制代码。
我所改变的只是:
另外,正如你所看到的,我在这里也使用了类似模块的结构。如果你也喜欢这种结构,你可以从我以前的项目 Publish Studio 和 My One Post 中了解更多信息
在第 1 部分中,我们使用内置的 tRPC react-query 查询数据。
... const { data } = trpc.test.useQuery(); return ({data} ); ...
所以,如果你已经了解 react-query,那么就没有什么可学的了,除了使用 tRPC 我们不必创建“queryFn”或“mutationFn”,因为我们直接调用后端方法。
突变的使用方式如下:
... const { mutateAsync: createTxn, isLoading: isCreating } = trpc.transaction.create.useMutation({ onSuccess: async () => { form.reset(); await utils.transaction.getAll.invalidate(); }, }); const addTransaction = async (data: z.infer) => { try { await createTxn(data); } catch (error) { console.error(error); } }; ...