优化数据查询: Cloud SQL与Prisma整合处理复杂数据关系

优化数据查询: Cloud SQL与Prisma整合处理复杂数据关系

在过去的两年里,我们的应用一直使用Google Firebase这个NoSQL数据库,而对于小型应用来说,Firebase 确实是一个很不错的选择。然而,随着项目功能的不断扩展,开始遇到了一些局限性。最主要的问题是:

  • Firebase的索引数量有限,最多只能有500个索引。
  • 每当新增一个Filter或查询条件时,就需要额外新增一个索引,导致索引数量逐渐达到上限。
  • Firebase无法进行连表查询

这对于日益复杂的查询需求成为了最大的痛点。考虑到整个项目都是建立在Google Cloud Platform (GCP)之上,决定在不脱离GCP的前提下寻找解决方案。

最终,选择了Cloud SQL作为解决方案。对于复杂的功能和查询需求,将数据处理转移到了Cloud SQL,并使用ORM框架Prisma来处理数据的映射和操作。而对于一些简单的GET请求,仍然保留在Firebase数据库中。

为了实现与Cloud SQL的通信,前端通过调用Cloud Functions Http onCall来触发后台接口,实现与Cloud SQL的数据交互。

1. 数据库选型

Cloud SQL 有3种类型的database可供选择,最初我们优先考虑的是MySQL,相对其他database,MySQL读写速度更快、市场使用率高。

但是最终我们选择了PostgreSQL,处于以下几点

  1. 项目最初使用的是Firebase database,业务模型中有大量的List类型,MySQL不支持List类型,虽然可以使用JSON类型,但是在查询上会相当繁琐
  2. PostgreSQL在社区的使用率上已经逐渐超过MySQL,社区的活跃度也是高于MySQL
  3. PostgreSQL内置List类型,可以和我们项目原有Firebase List类型完美契合,不需要额外工作

2. 本地连接CloudSQL数据库

本地连接Cloud SQL我们就可以更方便的操作数据库(如果已经做了配置可以忽略这一小节)

  • 根据Google Cloud Document 完成以下步骤

    • Install and initialize the gcloud(安装和初始化gcloud)

    • Create your credential file (创建证书文件)

      arduino 复制代码
      gcloud auth application-default login

接下来到Cloud SQL Overview 查看实例连接名

  • 启动 Cloud SQL Auth 代理, INSTANCE_CONNECTION_NAME 就是上图中的Connection name

    • 对于 Linux (Mac)环境,使用以下命令启动 Cloud SQL Auth 代理:

      bash 复制代码
      ./cloud-sql-proxy INSTANCE_CONNECTION_NAME
    • 在 Windows 中的 PowerShell 中,使用以下命令启动 Cloud SQL Auth 代理:

      sql 复制代码
      .\cloud-sql-proxy.exe INSTANCE_CONNECTION_NAME
    • 系统会显示类似以下内容的消息:

      csharp 复制代码
      Listening on 127.0.0.1:5432 forINSTANCE_CONNECTION_NAME
      Ready for new connections

如果是MySQL数据库则是3306端口,这时我们已经连接上Cloud SQL

想要看在图形界面上查看数据库中的数据,可以中Navicat/ DateGrip进行连接,在当前页面下载完对应的驱动然后进行Test Connection即可

3. 通过代码连接数据库

当我们在初始化数据库,可以直接使用PostgreSQL 语句进行插入数据,从时间和内存占用率上来看,纯PostgreSQL 语句比使用ORM 批量插入时间和内存占有都更少

js 复制代码
const connectionString = 'postgresql://USER:PASSWORD@HOST:PORT/DATABASE';
const client = new Client({ connectionString })

async insertDataBase() {
    await client.connect();
    let count = 0;
    await Promise.all(data.map(async (item) => {
      var id = item['id'];
			var logoURL = item['logoURL'];
      const query = {
        text: `INSERT INTO "Companies" ("id", "logoURL") VALUES($1, $2)`,
        values: [
          id ?? null,
          logoURL ?? null,
        ]
      }

      await client.query(
        query,
      );

    }));
    client.end()

  }

需要注意的是,PostgresSQL推荐使用下划线命名列名foo_bar 如果我们使用的是驼峰命名则在查询时需要对字段加上双引号INSERT INTO "Companies" ("id", "logoURL") VALUES($1, $2)

4. 使用Prisma ORM框架

为了使用Prisma框架与数据库进行连接,我们需要在目录结构下创建prisma文件夹,同时在该文件夹下创建schema.prisma文件

js 复制代码
datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id           Int      @id @default(autoincrement(uuid()))
  createdAt    DateTime @default(now())
  email        String   @unique
  name         String?
	twitterLink  String[]
}

4.1 基础配置

上面的代码中可以看到定义了一个datasource

  • url: 数据库的链接,格式为:postgresql://USER:PASSWORD@HOST:PORT/DATABASE

    • 项目中使用到Cloud SQL,所以在链接尾部需要添加?host=/cloudsql/INSTANCE_CONNECTION_NAME

      • INSTANCE_CONNECTION_NAME 是上文中第二点提到的Connection Name
  • client: 是固定格式

  • model: 我们可以在该文件定义多个model,model就是数据库的表,User为User表

    • id为Int类型,同时是默认自增且唯一

    • name为String类型,同时可为空

    • twitterLink为String[], 字符串数组

      • 在做新增/更新时需要注意如果是字符串数组类型的字段,一定不能为null/undefined,需要赋一个空数组[]

4.2 初始化客户端

做完基础配置后,我们运行以下指令,让我们的客户端代码可以使用Prisma进行CRUD

yarn risma generate

通过运行后的提示,可以看到在客户端中我们可以使用new PrismaClient() 来获取Prisma实例,后续通过Prisma实例内置方法进行CRUD

4.3 同步Model到数据库生成对应的表

当写完Model后,我们想同步到数据库中,可以运行以下命令来初始化Prisma

csharp 复制代码
yarn prisma migrate dev -- name init --create-only

运行后我们会看到数据库中自动生成了一张_prisma_migrations

然后我们接着执行以下指令,将我们定义好的UserModel 生成一张User表

yanr prisma deploy

运行后,就可以看到数据库多出了一张User表

5. 数据库操作封装

Prisma的文档可以说是对开发者相当友好,具体的CRUD文档中写的很清晰这里就不多赘述,接下来根据实际的应用场景对CRUD进行函数封装,这样团队的其他成员可以方便的调用

5.1获取页码函数

  • pageNumber 传入的页面
  • pageSize 当前页的大小
  • take 从第几页开始获取
  • skip 数据间隔多少
js 复制代码
/**
   * @param {number} pageNumber 
   * @param {number} pageSize 
   * @returns 
   */
  getPagination(pageNumber, pageSize) {
    const page = pageNumber || 1;
    const limit = pageSize || 20;
    const skip = (page - 1) * limit;
    const take = limit;
    return { take, skip };
  }

5.2 处理请求入参数

该函数更具入参的类型,判断合适的prisma查询条件

  • condition 请求的参数

    js 复制代码
    conditions: {
          priority: 10,
          tags: ['B2B', 'Education'],
          address: [
            { "country": null, "province": null, "city": "Nanjing" }
            { city: "San Francisco", state: "CA" },
            { "city": "Xiamen", "country": "China", "province": "Fujian" },
            { 'country': 'India' },
            { "city": "Los Angeles", "province": "California", "country": "United States", }
            // ... 可以添加更多 JSON 对象
          ]
        },
  • rangeField 类型为长度为2的数组,例如金额区间、时间区间。

  • ListField 数据库中定义的数组类型String[] int []

  • addressField Json类型,这里主要对地址做了处理

js 复制代码
/**
   * 
   * @param {{}} conditions 
   * @param {[]} rangeField 
   * @param {string []} ListField 
   * @param {string []} addressField 
   * @returns 
   */
  async handleConditions(conditions, rangeField, ListField, addressField) {
    const where = {};
    for (const key in conditions) {
      if (Object.prototype.hasOwnProperty.call(conditions, key)) {
        const value = conditions[key];
        if (addressField.includes(key)) {
          let addressFilter = [];
          // The key must be add quotes
          // {"city": "San Jose", "country": "United States", "province": "California"}
          for (let i = 0; i < value.length; i++) {
            const address = value[i];
            if (address['city']) {
              addressFilter.push({
                [key]: {
                  path: ['city'],
                  equals: address.city
                }
              })
            }
            if (address['country']) {
              addressFilter.push({
                [key]: {
                  path: ['country'],
                  equals: address.country
                }
              })
            }
            if (address['province']) {
              addressFilter.push({
                [key]: {
                  path: ['province'],
                  equals: address.province
                }
              })
            }
          }
          // Prisma sentence OR/AND, OR like ||, AND like &&
          where['OR'] = addressFilter
        } else if (Array.isArray(value)) {
          // if rangeField is DateTime, value's type is string and must be ISO8601String  e.g.: 1969-07-20T20:18:04.000Z
          if (rangeField.includes(key)) {
            where[key] = {
              gte: value[0],
              lte: value[1],
            };
          }
          else if (ListField.includes(key)) {
            where[key] = { hasSome: value };
          }
          else {
            // for database field not array type, but use array search
            where[key] = { in: value };
          }
        } else {
          where[key] = { equals: value };
        }
      }
    }
    return where;
  }

5.3 获取分页数据

通过上面的基础函数,我们就可以进行查询分页数据

  • onDataProcess,对特殊字段处理
js 复制代码
/**
   * @param {num} pageNumber 
   * @param {num} pageSize 
   * @param {object} conditions 
   * @param {Prisma.CompaniesDelegate<ExtArgs>} collection
   * @param {object} orderByField 
   * @param {[]} rangeField 
   * @param {string []} ListField 
   * @param {string []} addressField 
   * @param {function} onDataProcess 
   * @returns 
   */
  async fetchDataWithPagination(pageNumber, pageSize, conditions, collection, orderByField, rangeField, ListField, addressField, onDataProcess) {
    const { take, skip } = this.getPagination(pageNumber, pageSize);

    const query = await this.handleConditions(conditions, rangeField, ListField, addressField);
    const data = await collection.findMany({
      where: query,
      orderBy: orderByField,
      take,
      skip,
    });

    const totalCount = await collection.count({ where: query });
    const totalPages = Math.ceil(totalCount / pageSize);

    const result = onDataProcess?.(data) ?? data;

    return {
      data: result,
      total: totalCount,
      totalPages,
      currentPage: pageNumber,
    };
  }

完整代码请查看github

总结

希望可以帮助到使用Firebase 以及Cloud SQL的开发者,欢迎交流

相关推荐
hlsd#22 分钟前
go mod 依赖管理
开发语言·后端·golang
四喜花露水24 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
陈大爷(有低保)26 分钟前
三层架构和MVC以及它们的融合
后端·mvc
亦世凡华、26 分钟前
【启程Golang之旅】从零开始构建可扩展的微服务架构
开发语言·经验分享·后端·golang
河西石头27 分钟前
一步一步从asp.net core mvc中访问asp.net core WebApi
后端·asp.net·mvc·.net core访问api·httpclient的使用
前端Hardy33 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
2401_8574396939 分钟前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧66640 分钟前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
阿华的代码王国1 小时前
【SpringMVC】——Cookie和Session机制
java·后端·spring·cookie·session·会话
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript