Nodejs 多租户架构构建指南

2025-06-10

Nodejs 多租户架构构建指南

各位开发者,大家好!祝大家一切顺利。今天,我很高兴能够深入探讨多租户架构,并分享我构建一个完全采用这种方法的后端应用程序的见解和经验。

让我们生动地描绘一下:将您的软件想象成一个巨大的公寓大楼。在这座虚拟大厦中,每个用户或组织都充当租户,居住在其中一间公寓里。他们和平共处在同一个建筑结构中,这个结构代表着您软件的后端。然而,他们各自拥有独特的私密空间——可以将它们视为存储在独立数据库中的数据庇护所。主要目标是什么?保护他们的数据,确保其安全,并执行严格的边界,以防止任何未经授权的人窥探他们的数字“公寓”。这就像精心策划一个庞大、互联的数字公寓大楼的管理!

在本篇博文中,我们将揭秘多租户架构的复杂性,即使是初学者也能轻松上手。那就让我们一起踏上这段启迪之旅吧!

在我们开始之前,我想特别感谢这个出色的Medium博客,它是我探索的灵感源泉。

面临的挑战

  1. 每个租户必须有其专用的数据库。
  2. 管理员应该具有停用特定租户的能力。
  3. 将用户密码存储在主数据库中是绝对不行的。

当然,还有其他要求,但在本博客的范围内,我们将重点关注这些要求。

高层概述

多租户架构概述

如您所见,我们有四个客户端,每个客户端都有自己的数据库。它们都与同一个后端交互,后端执行其功能并将它们连接到各自的数据库。为了管理所有这些,我们需要一个超级管理员数据库来跟踪所有用户,并存储您可能需要的其他详细信息,例如价格信息或租户可以访问的模块。

现在我们已经做好了准备,让我们深入研究令人兴奋的部分——代码!

在这个例子中,我将使用 Node.js、Express 和 MongoDB 作为数据库。当然,你也可以根据自己的技术栈调整这种方法。一旦你掌握了概念,就会发现它非常简单。

附言:博客写完后,我发现它变得过于技术化,有些人可能不喜欢,他们只会来了解大概内容或工作原理。所以我最后添加了一个部分,概述了应用程序。你可以用它来了解一下它的概念。

代码

初始化

为了启动我们的项目,让我们创建一个目录。我将其命名为“多租户”,但您可以随意选择一个适合您项目的名称。

mkdir multi-tenant
Enter fullscreen mode Exit fullscreen mode

此后我们初始化 npm 项目

npm init -y
Enter fullscreen mode Exit fullscreen mode

现在,让我们安装必要的第三方包。

npm i cookie-parser express jsonwebtoken lru-cache mongoose
Enter fullscreen mode Exit fullscreen mode

也可以随意初始化 Git 存储库,但对于此演示,我不会介绍该步骤。

此外,我将使用 ES6 模块而不是 CommonJS。请耐心等待,因为我同时负责前端和后端,并且我希望尽可能保持一致。要启用 ES6 模块,您可以在 package.json 中添加 "type": "module" 或使用 .mjs 文件扩展名。

目录结构

在深入代码之前,我先解释一下我是如何将后端项目组织到不同的目录中,以保持代码库的井然有序。本篇博文将遵循以下结构:

  1. 控制器 - 控制器:这是 API 入口点所在的位置,主要用于请求验证。
  2. 服务 - 后端的核心业务逻辑就在这里。它包括数据操作、API 调用和数据库查询构建。
  3. 存储库 - 这些与数据库交互。服务在构建查询后调用存储库,存储库处理数据库交互并返回结果。
  4. Utils - 在这个目录中,我存储了不与客户端交互但协助后端的辅助函数,例如密码哈希、JWT 管理等。
  5. 中间件 - 此目录包含在访问 API 之前执行的代码。我们可以在这里编写逻辑来确定要使用的数据库并验证租户。这种分离有助于将我们的主要后端逻辑与连接逻辑分开。
  6. 服务器 - 它保存了我们服务器的配置文件,例如 Express 设置和潜在的 CORS 配置(本演示中未使用但推荐使用)。
  7. 路由 - 此目录中的代码定义了客户端可以访问的路由和端点。
  8. 模式——在这里,我们存储数据库模式。
.
├── controllers
├── middleware
├── repositories
├── routes
├── schema
├── server
├── services
└── utils
Enter fullscreen mode Exit fullscreen mode

我们的目录看起来是这样的。

现在,请记住,我不会在本演示中深入讲解如何分离管理员和租户逻辑,但您可以通过在所有目录中创建单独的管理员和租户目录来相应地组织逻辑来实现。在本演示中,我们将重点介绍架构的设置。

创建我们的服务器

我们首先创建服务器的入口点,即 index.js 文件。它将初始化 Express 应用。Express 应用的实际初始化将在服务器目录中处理,因为 index.js 不需要担心 Express 的设置;它的作用是引用所有初始化函数,例如连接数据库、初始化中间件、设置 Redis 等等。

这是server/express.config.js文件:

import express from "express";

const ExpressConfig = () => {
  const app = express();
  app.use(express.json());
  app.set("trust proxy", true);
  return app;
};
export default ExpressConfig;

Enter fullscreen mode Exit fullscreen mode

现在,让我们在我们的中使用此配置index.js,它将我们的服务器绑定到一个端口:

import ExpressConfig from "./server/express.config.js";

const app = ExpressConfig();


const PORT = 5000;

app.listen(PORT, async () => {
  console.log(`Multi Tenant Backend running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

需要注意的是,在本演示中,我没有使用环境变量来定义端口。但是,在您的生产项目中,强烈建议使用环境变量。这允许您PORT根据环境动态设置变量和其他配置选项,从而使您的应用程序更加灵活和安全。

初始中间件设置

现在,让我们深入研究如何为后端设置初始中间件。目前,我们将实现必要的中间件,当我们需要编写验证和确定数据库连接的逻辑时,我们将重新讨论本节。

我们首先创建middleware/index.js文件

import cors from "cors";
import cookieParser from "cookie-parser";

export default function (app: Application) {

  app.use(cookieParser());

}
Enter fullscreen mode Exit fullscreen mode

在本演示中,我们尽量简化。我们使用 cookie-parser 将令牌作为 HTTP 专用 Cookie 进行处理。在生产代码中,您可以扩展此文件以包含其他中间件,例如定义 CORS 策略、速率限制、设置请求上下文(用于记录请求-响应周期的文件)等等。您可以根据项目的具体需求和安全考虑随意定制它。

数据库模式设置

现在,让我们定义数据的模式。在这个演示中,我将尽量简化,涵盖基本元素,但您可以随意修改以满足您的特定需求。值得注意的是,我不会在mongoose.model这里注册我的集合。我会在开始设置数据库连接时注册它们。这样做的原因是为了避免为租户注册超级管理员模式,反之亦然。我们希望将这些模式分开。

让我们从超级管理员收藏开始。

租户催收

我想要构建的第一个集合是租户集合。在这个集合中,您可以存储与租户相关的元数据,例如他们购买的模块、用户限制以及租户是否处于启用或禁用状态。为简单起见,我仅存储名称和数据库 URI。以下是代码schema/tenant.js

import {  Schema } from "mongoose";

const tenantSchema = new Schema(
  {
    dbUri: { type: String, required: true },
    name: { type: String, unique: true, required: true },
  }
);

export default tenantSchema;
Enter fullscreen mode Exit fullscreen mode

租户用户集合

第二个超级管理员集合存储了我们应用程序的所有用户,无论他们属于哪个租户。同样,为了简单起见,我将保持其基本结构。您可以扩展它,添加用户角色或是否为租户管理员等字段。以下是代码schema/tenantUser.js

import { Schema,Types } from "mongoose";

const tenantUserSchema = new Schema(
 {
   email: String,
   tenantId:{
       type: Types.ObjectId,
       ref: "tenants",
    }
  }
);
export default tenantUserSchema;
Enter fullscreen mode Exit fullscreen mode

现在租户数据库将具有 -

用户集合

第三个集合是租户集合。它将存储与特定租户用户相关的数据。为了演示,我保留了基本的集合,但您可以根据需要在架构中添加相关字段。以下是代码schema/users.js

import { Schema } from "mongoose";


const usersSchema= new Schema(
  {
    email: { type: String, unique: true, required: true },
    password: { type: String },
  }
);
export default usersSchema;
Enter fullscreen mode Exit fullscreen mode

有了这些数据库架构定义,您现在对项目结构和数据的组织方式已经有了大致的了解。您可以随意调整和扩展这些架构,以满足您项目的特定需求。

存储库

在本节中,我们将定义一些简单的函数,它们接受查询并使用该查询执行数据库操作。请注意,这些函数的第一个参数始终是数据库连接对象。由于我们在多租户设置中处理多个数据库连接,因此我们需要向存储库提供适当的数据库连接,以便它能够对正确的数据库执行操作。

让我们从repositories/tenant.js文件开始:

import mongoose from "mongoose";

const mainSchemaName = "tenants";

const getTenantsRepo = async (
  adminDbConnection,
  findQuery = {},
  selectQuery = {}
) => {
  const data = await adminDbConnection
    .model(mainSchemaName)
    .find(findQuery)
    .select(selectQuery)
    .lean();
  return data;
};

const getATenantRepo = async (
  adminDbConnection,
  findQuery = {},
  selectQuery = {}
) => {
  const data = await adminDbConnection
    .model(mainSchemaName)
    .findOne(findQuery)
    .select(selectQuery)
    .lean();
  return data;
};

// This function is part of a service
// that involves many database calls, 
// so we'll use transactions here.
const addATenantRepo = async (
  adminDbConnection,
  tenantData,
  session = null
) => {
  const sessionOption = {};
  if (session) sessionOption.session = session;
  const data = await adminDbConnection
    .model(mainSchemaName)
    .create([tenantData], sessionOption);

  return data[0];
};

const updateATenant = async (
  adminDbConnection,
  findQuery = {},
  updateQuery = {},
) => {

  const data = await adminDbConnection
    .model(mainSchemaName)
    .updateOne(findQuery, updateQuery);
  return data;
};

export { getTenantsRepo, getATenantRepo, addATenantRepo, updateATenant };
Enter fullscreen mode Exit fullscreen mode

现在,让我们继续查看repositories/tenantUser.js文件:


const mainSchemaName = "tenantusers";

// This function is part of a service 
// with transactions.
const addATenantUserRepo = async (
  dbConn,
  userData,
  session = null
) => {
  const sessionOption = {};
  if (session) {
    sessionOption.session = session;
  }
  const data = await dbConn
    .model(mainSchemaName)
    .create([userData], sessionOption);
  return data[0];
};

const getATenantUserRepo = async (
  dbConn,
  findQuery,
  selectQuery = {}
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .findOne(findQuery)
    .select(selectQuery)
    .lean();

  return data;
};

const updateATenantUserRepo = async (
  dbConn,
  findQuery,
  updateQuery
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .updateOne(findQuery, updateQuery);
  return data;
};

export { addATenantUserRepo, getATenantUserRepo, updateATenantUserRepo };
Enter fullscreen mode Exit fullscreen mode

最后,我们有该repositories/users.js文件:


const mainSchemaName = "users"

// This function is part of the service 
// with transactions.
const addAUserRepo = async (
  dbConn,
  userData,
  session = null
) => {
  const sessionOption = {};
  if (session) sessionOption.session = session;
  const data = await dbConn
    .model(mainSchemaName)
    .create([userData], sessionOption);
  return data[0];
};

const getAUserRepo = async (
  dbConn,
  findQuery = {},
  selectQuery = {}
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .findOne(findQuery)
    .select(selectQuery)
    .lean();
  return data;
};

const updateUserRepo = async (
  dbConn,
  findQuery,
  updateQuery
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .updateOne(findQuery, updateQuery);
  return data;
};

const getUsersRepo = async (
  dbConn,
  findQuery = {},
  selectQuery = {}
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .find(findQuery)
    .select(selectQuery)
    .lean();

  return data;
};


export {
  addAUserRepo,
  getAUserRepo,
  updateUserRepo,
  getUsersRepo,
};
Enter fullscreen mode Exit fullscreen mode

主要连接设置

在本节中,我们将重点介绍如何设置管理所有数据库连接的逻辑,并利用最佳数据库结构(例如 LRU(最近最少使用)缓存)来高效管理这些连接。虽然我们在此处同时定义了管理员连接和租户连接的初始化函数,但为了更好地组织,您应该考虑将逻辑分离。另外,请注意,我们在此处将模型注册到数据库对象上。

utils/initDBConnection.js让我们从文件开始

import mongoose, { Connection } from "mongoose";
import TenantSchema from "../schema/tenant.js";
import TenantUserSchema from "../schema/tenantUser.js";
import UserSchema from "../schema/user.js";

const clientOption = {
  socketTimeoutMS: 30000,
  useNewUrlParser: true,
  useUnifiedTopology: true,
};

// Log MongoDB queries
mongoose.set("debug", true);

const initAdminDbConnection = async (
  DB_URL
) => {
  try {
    const db = mongoose.createConnection(DB_URL, clientOption);

    db.on("error", (err) =>
      console.log("Admin db error: ", err)
    );

    db.once("open", () => {
      console.log("Admin client MongoDB Connection ok!");
    });

    await db.model("tenants", TenantSchema);
    await db.model(
      "tenantusers",
      TenantUserSchema
    );

    return db;
  } catch (error) {
      return error;
  }
};

const initTenantDBConnection = async (
  DB_URL,
  dbName
) => {
  try {
    const db = mongoose.createConnection(DB_URL, clientOption);

    db.on("error", (err) =>
      console.log(`Tenant ${dbName} db error: `, err)
    );

    db.once("open", () => {
      console.log(
        `Tenant connection for ${dbName} MongoDB Connection ok!`
      );
    });

    await db.model("users", UserSchema);

    return db;
  } catch (error) {
    return error;
  }
};
export { initAdminDbConnection, initTenantDBConnection }

Enter fullscreen mode Exit fullscreen mode

现在,让我们在文件中定义 LRU 缓存管理器的代码utils/lruCacheManager.js

import { LRUCache } from "lru-cache";
import { Connection } from "mongoose";

const cacheOptions = {
  max: 5000,
  maxAge: 1000 * 60 * 60,
};

const connectionCache = new LRUCache(cacheOptions);

const setCacheConnection = (tenantId, dbConn): void => {
  console.log("Setting connection cache for ", tenantId);
  connectionCache.set(tenantId, dbConn);
};

const getCacheConnection = (tenantId) => {
  return connectionCache.get(tenantId);
};

const getCacheValuesArr = () => {
  return connectionCache.values();
};

export { setCacheConnection, getCacheConnection, getCacheValuesArr };

Enter fullscreen mode Exit fullscreen mode

现在,让我们编写应用程序的核心文件——连接管理器文件!该文件包含初始化数据库和管理集合的逻辑,以便我们的应用程序可以使用它们。以下是代码utils/connectionManager.js

import mongoose from "mongoose";

import { initAdminDbConnection, initTenantDBConnection } from "./initDBConnection.js";
import {
  getATenantRepo,
  getTenantsRepo,
} from "../repositories/tenant.js";
import {
  getCacheConnection,
  getCacheValuesArr,
  setCacheConnection,
} from "./lruCacheManager.js";

let adminDbConnection;

// This function will be called at the start
// of our server. Its purpose is to initialize the admin database
// and the database connections for all of the tenants.
export const connectAllDb = async () => {
  const ADMIN_DB_URI = `your admin db uri`;

  adminDbConnection = await initAdminDbConnection(ADMIN_DB_URI);

  const allTenants = await getTenantsRepo(
    adminDbConnection,
    { name: 1, dbUri: 1, _id: 1 }
  );

  for (const tenant of allTenants) {
    const tenantConnection = await initTenantDBConnection(
      tenant.dbUri,
      tenant.name
    );

    setCacheConnection(tenant._id.toString(), tenantConnection);
  }
};

export const getConnectionForTenant = async (
  tenantId
) => {
  console.log(`Getting connection from cache for ${tenantId}`);
  let connection = getCacheConnection(tenantId);
  if (!connection) {
    console.log(`Connection cache miss for ${tenantId}`);

    const tenantData = await getATenantRepo(
        adminDbConnection,
        { _id: tenantId },
        { dbUri: 1, name: 1 }
      )

    if (tenantData) {
      connection = await initTenantDBConnection(
        tenantData.dbUri,
        tenantData.name
      );
      if (!connection) return null;

      console.log("Connection cache added for ", tenantData.name);

    } else {
      console.log(
        "No connection data for tenant with ID",
        tenantId
      );
      return null;
    }
  }

  return connection;
};

export const getAdminConnection = () => {
  console.log("Getting adminDbConnection");
  return adminDbConnection;
};

const gracefulShutdown = async () => {
  console.log("Closing all database connections...");

  const connectionArr = getCacheValuesArr();

  // Close all tenant database connections from the cache
  for (const connection of connectionArr) {
      await connection.close();
      console.log("Tenant database connection closed.");
  }

  // Close the admin database connection if it exists
  if (adminDbConnection) {
    await adminDbConnection.close();
    console.log("Admin database connection closed.");
  }

  console.log("All database connections closed. Yay!");
};

let isShutdownInProgress = false;

// Listen for termination signals
["SIGINT", "SIGTERM", "SIGQUIT", "SIGUSR2"].forEach((signal) => {
  process.on(signal, async () => {
    if (!isShutdownInProgress) {
      console.log(`Received ${signal}, gracefully shutting down...`);
      isShutdownInProgress = true;
      await gracefulShutdown();
      process.exit(0);
    }
  });
});

Enter fullscreen mode Exit fullscreen mode

哈!我们的应用程序的主要部分到此结束。希望您能理解这段代码,它不仅仅是一些管理数据库连接的函数,但它仍然是我们应用程序的核心。

让我们定义一些实用函数,它们稍后会在我们的服务中发挥作用。我们可以将此文件命名为 misc.js,其中包含各种函数,因此以下是代码:utils/misc.js

import jwt from "jsonwebtoken";

const signJWT = (data) => {

  return jwt.sign(data, "random secret");

};

const verifyJWT = (
  payload
) => {
    return jwt.verify(payload, "random secret");
};

// define in your env file
const saltRounds = 10

const generateHash = async (input) => {
  try {
    const hash = await bcrypt.hash(input, Number(saltRounds));
    return hash;
  } catch (error) {
    console.error("Error generating hash:", error);
    throw error;
  }
};

const comparePassword = async (plainPassword, hash) => {
  try {
    const match = await bcrypt.compare(plainPassword, hash);
    return match;
  } catch (error) {
    console.error("Error comparing password:", error);
    throw error;
  }
};

export {
  signJWT,
  verifyJWT,
  generateHash,
  comparePassword,
};

Enter fullscreen mode Exit fullscreen mode

服务

在本节中,我们将深入研究应用程序的核心业务逻辑。

那么,让我们从最紧张的服务文件开始吧!services/tenant.js文件。

import mongoose from "mongoose";
import {
  addATenantRepo,
 } from "../repositories/tenant.js";
import { addATenantUserRepo } from "../repositories/tenantUser.js";
import { setCacheConnection } from "../utils/lruCacheManager.js";

import { addAUserRepo } from "../repositories/user.js";
import { initAdminDbConnection, initTenantDBConnection } from "../utils/initDBConnection.js";

const addATenantService = async (
  dbConn,
  tenantData
) => {
  const session = await dbConn.startSession();
  session.startTransaction();
  try {
    const data = await addATenantRepo(
      dbConn,
      { ...tenantData },
      session
    );

    let userData;
    if (data._id) {
      userData = await addATenantUserRepo(
        dbConn,
        {
          tenantId: data._id,
          email: tenantData.email,
        },
        session
      );

      const tenantDbConnection = await initTenantDBConnection(
        data.dbUri,
        data.name
      );

      await addAUserRepo(
        tenantDbConnection,
        {
          _id: userData._id,
          email: tenantData.email,
        },
        session
      );

      await session.commitTransaction();
      session.endSession();

      setCacheConnection(data._id.toString(), tenantDbConnection);
    }

    return {
      success: true,
      statusCode: 201,
      message: `Tenant added successfully`,
      responseObject: { tenantId: data._id, userId: userData?._id },
    };
  } catch (error) {
    await session.abortTransaction();
    session.endSession();
    throw error;
  }
};

export { addATenantService };

Enter fullscreen mode Exit fullscreen mode

addATenantService 函数负责向系统添加新租户。它遵循以下步骤:

  1. 将租户数据添加到超级管理员租户集合。
  2. 将用户详细信息添加到超级管理员租户用户集合。
  3. 将用户链接到租户用户集合中的租户,保持超级管理员和租户数据库之间的一致性。

现在,让我们转到services/auth.js包含身份验证相关逻辑的文件:

import { signJWT } from "../utils/misc.js";

const loginService = async (
  userData
) => {
  if (!userData || !userData|| !userData._id || !userData.tenantId)
    return {
      success: false,
      statusCode: 401,
      message: `No user with the given credentials`,
      responseObject: {
        incorrectField: "email",
      },
    };

  // Do some password matching

  const accessToken = signJWT(
    {
      userId: userData._id.toString(),
      tenantId: userData.tenantId.toString(),
    }
  );

  return {
    success: true,
    statusCode: 200,
    message: `Logged In Successfully`,
    responseObject: {
      accessToken,
      userId: userData._id.toString(),
      tenantId: userData.tenantId.toString(),
    },
  };
};

Enter fullscreen mode Exit fullscreen mode

loginService 函数处理用户登录并生成访问令牌,该令牌对我们的应用程序至关重要,因为它包含 userId 和 tenantId。此令牌将用于验证请求并根据 tenantId 确定合适的数据库连接。在本演示中,我没有编写密码匹配的代码,但您可以根据需要编写自己的逻辑。

服务部分到此结束。现在让我们继续讨论控制器和路由部分。

控制器和路由

我已经讲完了“服务”部分,现在让我们深入探讨“控制器”和“路由”。在本节中,我将提供设置控制器和路由所需的代码。由于代码简单易懂,我不会深入讲解。但是,有效地组织代码至关重要,因此我将提供一个基本结构。

这是我们的controllers/index.js文件代码

import { loginService } from "../services/auth.js"
import { addATenantService } from "../services/tenant.js"

export function loginController = async (req,res)=>{
  const serviceFnResponse = await loginService(req.body);

  res.status(serviceFnResponse.code).json({...serviceFnResponse});
}

export function addATenantController = async (req,res)=>{
  const serviceFnResponse = await addATenantService(req.body);

  res.status(serviceFnResponse.code).json({...serviceFnResponse});
}
Enter fullscreen mode Exit fullscreen mode

在这些控制器中,你应该添加代码来验证请求主体,以确保数据的完整性和安全性。正确的输入验证是构建健壮应用程序的关键步骤。

现在,让我们在文件中定义应用程序的路由routes/index.js

import { Router } from "express"
import { loginController, addATenantController } from "../controllers/index.js"

const router = Router()

router.post("/add",addATenantController);
router.post("/login",loginController);

export default router;
Enter fullscreen mode Exit fullscreen mode

此代码创建一个 Express 路由器并定义应用程序的路由。在实际场景中,您可能会有更多路由,每个路由都映射到特定的控制器。为了便于维护,请考虑将路由组织到单独的文件中,每个资源或功能一个文件。

通过这种方式构建代码,您可以维护一个干净、有序的项目,从而更容易在将来添加新功能或扩展现有功能。

中间件

现在,让我们深入研究应用程序的引擎——中间件。这段代码将处理繁重的工作,例如为每个请求确定数据库连接和租户。我们从这个middleware/databaseResolver.js文件开始:

import { getConnectionForTenant } from "../utils/connectionManager.js";
import { verifyJWT } from "../utils/misc.js";

export const databaseResolver = async (req, _, next) => {
  const urlArr = req.url.split("/");

  // Skip database resolution for login route
  if (urlArr.includes("login")) return next();

  const token = req.headers.jwt;
  // Handle the logic for null checking and authorization
  const payloadData = verifyJWT(token);
  // Handle the expiry logic, etc.
  const dbConnection = getConnectionForTenant(payload.tenantId);

  // Here, we are directly populating the req object, but you can use
  // custom context managers in your application
  req.dbConnection = dbConnection;
  next();
};
Enter fullscreen mode Exit fullscreen mode

我们定义了基本的中间件逻辑,该逻辑根据请求的租户信息解析适当的数据库连接。对于登录路由,中间件也会跳过此过程。

接下来,让我们配置应用程序中使用的中间件。创建一个名为的新文件server/middleware.config.js

import { databaseResolver } from "../middleware/databaseResolver.js"

export default function(app){
  app.use(databaseResolver);
}
Enter fullscreen mode Exit fullscreen mode

您可能想知道,既然代码相对较短,为什么我们要为中间件使用单独的文件。但是,在生产级应用程序中,您可能会有更复杂的中间件需求,例如请求清理、日志记录等。拥有单独的中间件配置文件可以让您有效地管理和组织这些需求。
例如,请求清理中间件、日志记录中间件等。

该文件设置应用程序的路由,在实际场景中,您将拥有按资源或功能组织的多条路由。

server/route.config.js

import router from "routes/index.js"

export default function(app){
  app.use('/api',router);
}
Enter fullscreen mode Exit fullscreen mode

现在你需要做的只是在 index.js 文件中引入所有配置

import ExpressConfig from "./server/express.config.js";
import MiddlewareConfig from "./server/middleware.config.js";
import RouteConfig from "./server/route.config.js";

const app = ExpressConfig();

MiddlewareConfig(app)
RouteConfig(app)

const PORT = 5000;

app.listen(PORT, async () => {
  console.log(`Multi Tenant Backend running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

通过以这种方式构建代码,您可以维护一个干净且有条理的 index.js 文件,从而使其更容易理解和扩展您的应用程序。

大功告成!恭喜您构建了多租户应用的基础。这个强大的结构可以作为创建复杂软件即服务 (SaaS) 应用的基础。如果您觉得技术细节有点难以理解,别担心。我们将使用图表来概述该应用的工作原理,并让我们回顾一下应用中的各个组成部分。

应用程序概述

应用程序概述

  1. 客户端请求:当用户与您的平台交互(例如登录或访问数据)时,他们的请求会被发送到您的服务器。此请求会附带一个称为 JSON Web Token (JWT) 的特殊密钥,类似于秘密密码。

  2. 中间件:请求首先进入一个名为“中间件”的检查点。它就像俱乐部里的保镖。如果请求是登录,就会快速通过。否则,它会进入下一步。

  3. 数据库解析器:真正的魔法来了。数据库解析器会查看 JWT 密码,并确定用户所属的公司(租户)。这就像将邮件分类到不同的邮箱一样。

  4. 数据库连接:一旦解析器知道是哪个租户,它就会打开正确的门——租户的数据库。这就像每个租户在一个巨大的图书馆里都有自己的房间,只有他们才能访问自己的书。

  5. 控制器:打开正确的房间(数据库)后,控制器将接管一切。它就像一位图书管理员,帮你找到所需的书籍。控制器会判断用户想要做什么,并从正确的房间(数据库)获取信息。

  6. 服务:在控制器内部,名为“服务”的特殊助手负责执行繁重的工作。它们执行的任务包括检查用户密码是否正确或获取数据。这些服务就像专业的图书管理员,知道每本书的存放位置。

  7. 数据库交互:服务与数据库交互,向其提问并获取答案。例如,它们可能会询问“这是正确的密码吗?”或“请提供此用户的数据”。每个租户的数据都是独立保存的,因此不会发生混淆。

  8. 响应生成:服务端一旦获得所有答案,就会创建响应。这就像整理一份包含所有必要信息的报告,其中包含“成功!”或“抱歉,信息不完整!”之类的消息。

  9. 控制器响应:控制器收到此报告并准备发送。这就像把它放进一个信封里,信封上写着用户的地址。

  10. 客户端响应:最终,响应会被发送回用户的设备。这就像通过邮件收到报告一样。用户会看到消息以及他们请求的任何数据,然后就可以继续使用您的平台。

简单来说,您的多租户应用程序就像一个拥有众多房间(数据库)的大型图书馆。当用户提出请求时,应用程序会确保他们进入正确的房间获取正确的信息。这样,每个租户的数据都保持私密和安全,就像图书馆不同房间存放的不同书籍一样。

该系统非常适合运行云服务,其中每个租户在共享同一平台的同时拥有自己的空间。

如果您对应用程序的具体方面还有其他疑问或需要进一步了解,请随时提出!如果您有任何改进建议,也请告诉我,我很乐意听取。

鏂囩珷鏉ユ簮锛�https://dev.to/rampa2510/guide-to-building-multi-tenant-architecture-in-nodejs-40og
PREV
5 个免费且完美的无头 CMS,适合 JS 开发者
NEXT
React:如何使用下拉菜单动态排序对象数组(使用 React Hooks)