使用 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);
}
};
...