» Node.js:使用Express构建REST API » 2. 开发 » 2.8 数据库:mongoDB

数据库:mongoDB

如果你更青睐 NoSQL 数据库,mongoDB 绝对是你的最佳选择。

尝试 mongoDB

  1. 在你的机器上安装 mongoDB 并启动它。

注意:在生产项目中记得为你的集合中字段创建所需索引。

  1. 添加 mongo 依赖:
npm i mongodb
  1. 更新代码。

使用 mongoDB 执行 CRUD 操作

添加一个新的领域实体 Review,在 domain/model/review.ts 中:

// Review 表示书评
export interface Review {
  id: string;
  book_id: number;
  author: string;
  title: string;
  content: string;
  created_at: Date;
  updated_at: Date;
}

domain/gateway/review_manager.ts 中声明其业务能力:

import { Review } from "@/domain/model";

export interface ReviewManager {
  createReview(r: Review): Promise<string>;
  updateReview(id: string, r: Review): Promise<void>;
  deleteReview(id: string): Promise<void>;
  getReview(id: string): Promise<Review | null>;
  getReviewsOfBook(book_id: number): Promise<Review[]>;
}

infrastructure/database/mongo.ts 中实现这些方法:

import { MongoClient, Db, Collection, ObjectId } from "mongodb";
import { Review } from "@/domain/model";
import { ReviewManager } from "@/domain/gateway";

const coll_review = "reviews";

export class MongoPersistence implements ReviewManager {
  private db!: Db;
  private coll!: Collection;

  constructor(mongoURI: string, dbName: string) {
    const client = new MongoClient(mongoURI);
    client.connect().then(() => {
      this.db = client.db(dbName);
      this.coll = this.db.collection(coll_review);
      console.log("Connected to mongodb.");
    });
  }

  async createReview(r: Review): Promise<string> {
    const result = await this.coll.insertOne(r);
    return result.insertedId.toHexString();
  }

  async updateReview(id: string, r: Review): Promise<void> {
    const objID = new ObjectId(id);
    const updateValues = {
      title: r.title,
      content: r.content,
      updated_at: r.updated_at,
    };
    const result = await this.coll.updateOne(
      { _id: objID },
      { $set: updateValues }
    );
    if (result.modifiedCount === 0) {
      throw new Error("Review does not exist");
    }
  }

  async deleteReview(id: string): Promise<void> {
    const objID = new ObjectId(id);
    const result = await this.coll.deleteOne({ _id: objID });
    if (result.deletedCount === 0) {
      throw new Error("Review does not exist");
    }
  }

  async getReview(id: string): Promise<Review | null> {
    const objID = new ObjectId(id);
    const reviewDoc = await this.coll.findOne({ _id: objID });
    if (!reviewDoc) {
      return null;
    }
    return {
      id: reviewDoc._id.toHexString(),
      book_id: reviewDoc.book_id,
      author: reviewDoc.author,
      title: reviewDoc.title,
      content: reviewDoc.content,
      created_at: reviewDoc.created_at,
      updated_at: reviewDoc.updated_at,
    };
  }

  async getReviewsOfBook(book_id: number): Promise<Review[]> {
    const cursor = this.coll.find({ book_id });
    const reviewDocs = await cursor.toArray();
    return reviewDocs.map((reviewDoc) => ({
      id: reviewDoc._id.toHexString(),
      book_id: reviewDoc.book_id,
      author: reviewDoc.author,
      title: reviewDoc.title,
      content: reviewDoc.content,
      created_at: reviewDoc.created_at,
      updated_at: reviewDoc.updated_at,
    }));
  }
}

添加 mongodb 配置项,infrastructure/config/config.ts

@@ -3,6 +3,8 @@ import { readFileSync } from "fs";
 interface DBConfig {
   fileName: string;
   dsn: string;
+  mongo_uri: string;
+  mongo_db_name: string;
 }
 
 interface ApplicationConfig {

添加配置值,config.json

@@ -4,6 +4,8 @@
   },
   "db": {
     "file_name": "test.db",
-    "dsn": "mysql://test_user:test_pass@127.0.0.1:3306/lr_book?charset=utf8mb4"
+    "dsn": "mysql://test_user:test_pass@127.0.0.1:3306/lr_book?charset=utf8mb4",
+    "mongo_uri": "mongodb://localhost:27017",
+    "mongo_db_name": "lr_book"
   }
 }

在应用层添加 review_operatorapplication/executor/review_operator.ts

import { ReviewManager } from "@/domain/gateway";
import { Review } from "@/domain/model";
import { ReviewBody } from "@/application/dto";

export class ReviewOperator {
  private reviewManager: ReviewManager;

  constructor(b: ReviewManager) {
    this.reviewManager = b;
  }

  async createReview(rb: ReviewBody): Promise<Review> {
    const now = new Date();
    const r: Review = {
      ...rb,
      created_at: now,
      updated_at: now,
      id: "",
    };
    const id = await this.reviewManager.createReview(r);
    r.id = id;
    return r;
  }

  async getReview(id: string): Promise<Review | null> {
    return await this.reviewManager.getReview(id);
  }

  async getReviewsOfBook(book_id: number): Promise<Review[]> {
    return await this.reviewManager.getReviewsOfBook(book_id);
  }

  async updateReview(id: string, r: Review): Promise<Review> {
    await this.reviewManager.updateReview(id, r);
    return r;
  }

  async deleteReview(id: string): Promise<void> {
    await this.reviewManager.deleteReview(id);
  }
}

application/dto/review.ts 中定义ReviewBody 接口:

export interface ReviewBody {
  book_id: number;
  author: string;
  title: string;
  content: string;
}

调整 application/wire_helper.ts 来引入 mongodb 连接:

@@ -1,16 +1,25 @@
-import { MySQLPersistence } from "@/infrastructure/database";
+import { MySQLPersistence, MongoPersistence } from "@/infrastructure/database";
 import { Config } from "@/infrastructure/config";
-import { BookManager } from "@/domain/gateway";
+import { BookManager, ReviewManager } from "@/domain/gateway";
 
 // WireHelper is the helper for dependency injection
 export class WireHelper {
-  private persistence: MySQLPersistence;
+  private sql_persistence: MySQLPersistence;
+  private no_sql_persistence: MongoPersistence;
 
   constructor(c: Config) {
-    this.persistence = new MySQLPersistence(c.db.dsn);
+    this.sql_persistence = new MySQLPersistence(c.db.dsn);
+    this.no_sql_persistence = new MongoPersistence(
+      c.db.mongo_uri,
+      c.db.mongo_db_name
+    );
   }
 
   bookManager(): BookManager {
-    return this.persistence;
+    return this.sql_persistence;
+  }
+
+  reviewManager(): ReviewManager {
+    return this.no_sql_persistence;
   }
 }

添加 review 相关路由,adaptor/router.ts

@@ -2,14 +2,16 @@ import express, { Request, Response } from "express";
 import morgan from "morgan";
 
 import { Book } from "@/domain/model";
-import { BookOperator } from "@/application/executor";
+import { BookOperator, ReviewOperator } from "@/application/executor";
 import { WireHelper } from "@/application";
 
 class RestHandler {
   private bookOperator: BookOperator;
+  private reviewOperator: ReviewOperator;
 
-  constructor(bookOperator: BookOperator) {
+  constructor(bookOperator: BookOperator, reviewOperator: ReviewOperator) {
     this.bookOperator = bookOperator;
+    this.reviewOperator = reviewOperator;
   }
 
   // Get all books
@@ -81,21 +83,100 @@ class RestHandler {
       res.status(404).json({ error: "Failed to delete" });
     }
   }
+
+  // Get all book reviews
+  public async getReviewsOfBook(req: Request, res: Response): Promise<void> {
+    const bookID = parseInt(req.params.id, 10);
+    if (isNaN(bookID)) {
+      res.status(400).json({ error: "invalid book id" });
+      return;
+    }
+
+    try {
+      const books = await this.reviewOperator.getReviewsOfBook(bookID);
+      res.status(200).json(books);
+    } catch (err) {
+      console.error(`Failed to get reviews of book ${bookID}: ${err}`);
+      res.status(404).json({ error: "failed to get books" });
+    }
+  }
+
+  // Get single review
+  public async getReview(req: Request, res: Response): Promise<void> {
+    const id = req.params.id;
+
+    try {
+      const review = await this.reviewOperator.getReview(id);
+      res.status(200).json(review);
+    } catch (err) {
+      console.error(`Failed to get the review ${id}: ${err}`);
+      res.status(404).json({ error: "failed to get the review" });
+    }
+  }
+
+  // Create a new review
+  public async createReview(req: Request, res: Response): Promise<void> {
+    const reviewBody = req.body;
+
+    try {
+      const review = await this.reviewOperator.createReview(reviewBody);
+      res.status(201).json(review);
+    } catch (err) {
+      console.error(`Failed to create: ${err}`);
+      res.status(404).json({ error: "failed to create the review" });
+    }
+  }
+
+  // Update an existing review
+  public async updateReview(req: Request, res: Response): Promise<void> {
+    const id = req.params.id;
+    const reqBody = req.body;
+
+    try {
+      const book = await this.reviewOperator.updateReview(id, reqBody);
+      res.status(200).json(book);
+    } catch (err) {
+      console.error(`Failed to update: ${err}`);
+      res.status(404).json({ error: "failed to update the review" });
+    }
+  }
+
+  // Delete an existing review
+  public async deleteReview(req: Request, res: Response): Promise<void> {
+    const id = req.params.id;
+
+    try {
+      await this.reviewOperator.deleteReview(id);
+      res.status(204).send();
+    } catch (err) {
+      console.error(`Failed to delete: ${err}`);
+      res.status(404).json({ error: "failed to delete the review" });
+    }
+  }
 }
 
 // Create router
 function MakeRouter(wireHelper: WireHelper): express.Router {
   const restHandler = new RestHandler(
-    new BookOperator(wireHelper.bookManager())
+    new BookOperator(wireHelper.bookManager()),
+    new ReviewOperator(wireHelper.reviewManager())
   );
 
   const router = express.Router();
 
-  router.get("", restHandler.getBooks.bind(restHandler));
-  router.get("/:id", restHandler.getBook.bind(restHandler));
-  router.post("", restHandler.createBook.bind(restHandler));
-  router.put("/:id", restHandler.updateBook.bind(restHandler));
-  router.delete("/:id", restHandler.deleteBook.bind(restHandler));
+  router.get("/books", restHandler.getBooks.bind(restHandler));
+  router.get("/books/:id", restHandler.getBook.bind(restHandler));
+  router.post("/books", restHandler.createBook.bind(restHandler));
+  router.put("/books/:id", restHandler.updateBook.bind(restHandler));
+  router.delete("/books/:id", restHandler.deleteBook.bind(restHandler));
+  router.get(
+    "/books/:id/reviews",
+    restHandler.getReviewsOfBook.bind(restHandler)
+  );
+  router.get("/reviews/:id", restHandler.getReview.bind(restHandler));
+  router.post("/reviews", restHandler.createReview.bind(restHandler));
+  router.put("/reviews/:id", restHandler.updateReview.bind(restHandler));
+  router.delete("/reviews/:id", restHandler.deleteReview.bind(restHandler));
 
   return router;
 }
@@ -115,6 +196,6 @@ export function InitApp(wireHelper: WireHelper): express.Express {
   });
 
   const r = MakeRouter(wireHelper);
-  app.use("/books", r);
+  app.use("", r);
   return app;
 }

所有更改已合入。让我们用 curl 来试下效果。

curl 测试

创建一个新的书评:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{
        "book_id": 1,
        "author": "John Doe",
        "title": "Great Book",
        "content": "This is a great book!"
      }' \
  http://localhost:3000/reviews

响应如下:

{"book_id":1,"author":"John Doe","title":"Great Book","content":"This is a great book!","created_at":"2024-03-01T15:16:19.823Z","updated_at":"2024-03-01T15:16:19.823Z","id":"65e1f143f1c5f50b36b2ce60","_id":"65e1f143f1c5f50b36b2ce60"}

根据 ID 获取单个书评:

curl -X GET http://localhost:3000/reviews/65e1f143f1c5f50b36b2ce60

结果:

{
  "id": "65e1f143f1c5f50b36b2ce60",
  "book_id": 1,
  "author": "John Doe",
  "title": "Great Book",
  "content": "This is a great book!",
  "created_at": "2024-03-01T15:16:19.823Z",
  "updated_at": "2024-03-01T15:16:19.823Z"
}

列出某本书的所有书评:

curl -X GET http://localhost:3000/books/1/reviews

结果列表:

[
  {
    "id": "65e1f143f1c5f50b36b2ce60",
    "book_id": 1,
    "author": "John Doe",
    "title": "Great Book",
    "content": "This is a great book!",
    "created_at": "2024-03-01T15:16:19.823Z",
    "updated_at": "2024-03-01T15:16:19.823Z"
  },
  {
    "id": "65e1f1c0f1c5f50b36b2ce61",
    "book_id": 1,
    "author": "Carl Smith",
    "title": "Best best Book",
    "content": "This is a great book!",
    "created_at": "2024-03-01T15:18:24.124Z",
    "updated_at": "2024-03-01T15:18:24.124Z"
  }
]

更新已有书评:

curl -X PUT \
  -H "Content-Type: application/json" \
  -d '{
        "content": "I prefer Robert Smith new book",
        "title": "Not that good"
      }' \
  http://localhost:3000/reviews/65e1f143f1c5f50b36b2ce60

结果:

{"content":"I prefer Robert Smith new book","title":"Not that good"}

删除已有书评:

curl -X DELETE http://localhost:3000/reviews/65e1f143f1c5f50b36b2ce60

其返回 code 204 表示一次成功删除。

瞧!你的 API 服务器把 mongoDB 也用上啦。

上页下页