使用 tRPC 和 Next.js 应用设置 Drizzle 和 Postgres

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

finance tracker

这是第 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 与数据库交互。

创建第一个模块

关于项目结构,主要有两种类型:

  • 模块:将功能划分为不同的模块并将所有相关文件放在一起。 NestJs 和 Angular 等流行框架都采用这种结构。
  • .
    └── 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 中复制代码。

    我所改变的只是:

  • 设置shadcn/ui
  • 更改 src/components/modules/dashboard/index.tsx
  • 另外,正如你所看到的,我在这里也使用了类似模块的结构。如果你也喜欢这种结构,你可以从我以前的项目 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);
        }
      };
    ...