今天做 CAP 项目时,数据库配置经常被我们低估。很多人看到 cds.requires.db,第一反应是这里不过就是写一个 kind,再补一点 credentials,服务能跑起来就算完事。可一旦项目从本地原型走到 SAP BTP,走到 SAP HANA Cloud,走到混合调试、自动部署、CSV 初始数据、连接池、多租户、AI 向量检索这些场景,数据库配置就不再是一个小角落里的 JSON 片段,而是整个 CAP 应用生命周期的地基。

CAP 的数据库设计有一个很有意思的地方,它希望我们把业务模型写在 CDS 里,把服务逻辑写在 Node.js 或 Java 运行时里,而不是一上来就陷进某个数据库方言。我们的领域对象可以叫 Books、Authors、Orders、Incidents,业务语义放在模型层,持久化细节则由 CAP 运行时和构建工具去翻译。也正因为这样,CAP 支持的数据库不是简单并列关系。SQLite 更适合开发和测试,SAP HANA Cloud 是生产场景的主战场,PostgreSQL 也可以进入生产部署,H2 主要服务于 CAP Java 的本地开发。这样的分工,看起来像技术选型,实际更像工程节奏管理。

在本地开发阶段,SQLite 往往是最舒服的选择。它启动快,依赖少,很适合 cds watch 这种内循环开发模式。做一个 CAP Bookshop 原型时,我们并不希望每改一行 CDS 都要连接远程数据库,也不希望每个开发人员都先申请一套 HANA Cloud 实例才能开始写服务。此时安装 SQLite 支持只需要一条命令。

npm add @cap-js/sqlite -D

配置也很轻。内存数据库最适合快速验证,应用启动后表结构和测试数据可以重新装载,关闭后也不会留下脏数据。

{
  "cds": {
    "requires": {
      "db": {
        "kind": "sqlite",
        "credentials": { "url": ":memory:" }
      }
    }
  }
}

这种模式很像我们在办公室搭白板。白板上的流程图画错了就擦掉,字段设计想改就改,没人会把白板当成正式档案。CAP 里的 SQLite 内存库也是这个角色,它让我们专注于领域模型、服务行为、OData 输出和 Fiori Elements 页面是否符合预期,而不是提前背上生产数据库的治理负担。

不过有些时候,内存数据库太轻了。我们可能想保留本地数据,反复调试某个复杂场景,或者排查一次特定的数据迁移问题。这个时候可以切到文件型 SQLite。

{
  "cds": {
    "requires": {
      "db": {
        "kind": "sqlite",
        "credentials": { "url": "db.sqlite" }
      }
    }
  }
}

把模型部署到这个文件里也很直接。

cds deploy --to sqlite:db.sqlite

这里有个很实用的经验,SQLite 文件适合开发人员之间复现问题,但不要让它变成团队事实标准。企业项目里的数据标准,仍然应该回到 CDS 模型、CSV 初始数据、迁移脚本和正式数据库部署流程。SQLite 文件可以帮我们保留现场,却不该替代项目的数据治理。

回到生产环境,SAP HANA Cloud 才是许多 SAP BTP 上 CAP 应用的核心数据库选择。尤其是企业管理软件里常见的主数据、单据、审批状态、库存数量、财务维度、组织结构,这些数据不只是 CRUD 对象,还会参与分析、权限、多租户、扩展和生命周期管理。SAP HANA Cloud 在这里不是一个普通数据库连接目标,而是和 SAP 生态高度贴合的生产级持久化平台。

Node.js 项目里可以安装 HANA 数据库服务包,也可以使用 cds add hana 让工具顺手补齐部署相关配置。

npm add @cap-js/hana
cds add hana

典型生产配置会写成按 profile 生效的形式。

{
  "cds": {
    "requires": {
      "db": {
        "[production]": {
          "kind": "hana",
          "deploy-format": "hdbtable"
        }
      }
    }
  }
}

这里的 [production] 很关键。CAP 项目不是只有一种运行状态。本地开发、混合调试、正式部署对数据库的要求完全不同。我们在开发时可能使用 SQLite,在混合模式下服务跑在本机但连接云端 HANA,在生产环境里应用和数据库都运行在 SAP BTP 上。把配置写进 profile,就是把这些场景的边界提前讲清楚。

混合开发在真实项目里特别有价值。一个团队正在开发采购审批应用,前端是 Fiori Elements,后端是 CAP,数据库是 HANA Cloud。审批规则和供应商主数据已经在云端数据库里,开发人员本地只想调试服务逻辑,不想每次都完整部署应用。这时可以把 HANA 加到 hybrid profile 里。

# Add HANA for hybrid mode
cds add hana --for hybrid

# Deploy to HANA Cloud
cds deploy --to hana

# Run locally with remote HANA
cds watch --profile hybrid

这个模式的好处,是把本地代码热加载和云端真实数据库结合起来。我们改服务逻辑很快,数据又不是凭空造的假数据。做权限、草稿、深层关联、性能排查时,这比纯 SQLite 更接近生产现场。

构建 HANA 部署产物时,CAP 会把 CDS 模型转换成 HANA HDI 相关工件。

cds build --for hana

生成内容通常会落在 gen/db/ 下,其中包括 .hdbtable.hdbview.hdbtabledata.hdiconfig.hdinamespace 等文件。.hdbtable 对应表,.hdbview 对应视图,.hdbtabledata 负责装载 CSV 数据。对于经历过传统 HANA 原生开发的人来说,这些文件并不陌生。CAP 的价值在于,我们不用手写每一个数据库工件,而是让 CDS 成为模型源头,再由构建过程生成目标数据库需要的形式。

说到 HANA,就绕不开它的一些原生能力。现在很多企业应用已经不只是保存结构化表单数据,还要处理文档语义、相似度搜索、知识问答和 RAG 场景。CAP 里的 Vector 类型让这些 AI 相关场景可以进入 CDS 模型。

entity Documents {
  key ID : UUID;
  content : String;
  embedding : Vector(1536);  // For AI embeddings
}

一个更贴近业务的场景,是售后服务知识库。我们的系统里有大量维修记录、客户投诉、工程师处理说明。过去只能靠关键字搜索,用户输入 逆变器过热,系统只能匹配文本中出现这些字的记录。引入 embedding 后,每条记录都可以生成向量,用户输入的自然语言问题也可以生成向量,数据库按向量相似度找出语义相近的事故单。这样,用户不一定非要输入同样的词,也能找到相关解决方案。CAP 数据模型里的 Vector(1536),连接的是 AI 语义能力和企业业务数据之间的桥。

HANA 还有地理空间能力。零售、物流、资产管理、能源巡检这些行业里,位置不是一个备注字段,而是业务判断的关键条件。门店服务半径、仓库配送范围、设备坐标、管线覆盖区域,都可以进入模型。

entity Locations {
  key ID : UUID;
  point : hana.ST_POINT;
  area : hana.ST_GEOMETRY;
}

例如我们做一个备件调拨应用,系统需要根据故障设备坐标找到最近的仓库,并判断配送范围是否覆盖客户现场。用普通字符串保存经纬度当然也能显示在页面上,但很难做空间计算。使用 hana.ST_POINThana.ST_GEOMETRY,位置就不只是文本,而变成数据库可以理解、可以计算、可以索引的空间对象。

生产系统最怕的不是一开始建表,而是上线后的模型演进。加字段通常还好,删除字段、改类型、拆表、改主键就麻烦得多。CAP 对兼容性变化可以自动处理,但遇到不兼容变化时,最好显式启用迁移控制。

@cds.persistence.journal
entity LargeTable {
  key ID : UUID;
  data : String;
}

这会生成 .hdbmigrationtable,让我们对迁移有更精细的控制。现实里,一个已经跑了两年的订单表可能有几千万行数据,不能因为模型改动就粗暴重建。迁移必须考虑历史数据、停机窗口、索引重建、回滚方案和兼容发布。@cds.persistence.journal 的价值就在这里,它提醒我们,数据库不是代码仓库里随便删改的文件,而是承载企业运行状态的资产。

有时 CAP 的数据库无关抽象不够用,我们确实需要写一点原生 SQL。比如大表分区、列式表声明、特殊索引。CAP 提供 @sql.append@sql.prepend 这样的出口。

@sql.append: 'PARTITION BY HASH (ID) PARTITIONS 4'
entity PartitionedData { ... }

@sql.prepend: 'COLUMN TABLE'
entity ColumnTable { ... }

这个能力很好用,但也要克制。CAP 的优势是数据库无关,原生 SQL 一多,项目就会越来越绑定某个数据库。我们的经验是,只有当性能、容量、合规或数据库原生能力确实要求时,才把这些注解放进模型里,并且在代码评审里明确记录原因。一个分区注解背后,最好有真实的数据规模和访问模式支撑,而不是工程师单纯觉得高级。

PostgreSQL 在 CAP 里也有自己的位置。它可以作为生产数据库,尤其在一些非典型 SAP HANA 场景、边缘部署或已有 PostgreSQL 运维体系的团队里,会成为实际选择。安装方式很简单。

npm add @cap-js/postgres
cds add postgres

连接配置可以直接写主机、端口、数据库名、用户名和密码。

{
  "cds": {
    "requires": {
      "db": {
        "kind": "postgres",
        "credentials": {
          "host": "localhost",
          "port": 5432,
          "database": "mydb",
          "user": "postgres",
          "password": "password"
        }
      }
    }
  }
}

本地起一个 PostgreSQL 容器也不复杂。

docker run -d --name postgres \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  postgres:15

部署模型时使用下面的命令。

cds deploy --to postgres

PostgreSQL 的意义,不只是多了一个数据库选项。它说明 CAP 的领域建模和 CQL 查询并不天然锁死在 HANA 上。只要我们把主要业务逻辑留在 CDS、CQL、服务层和标准注解里,数据库就有一定替换空间。当然,进入生产之后仍要诚实面对各数据库之间的差异,函数支持、索引策略、事务隔离、部署机制、扩展能力都要验证,不能只看配置能不能启动。

CAP Java 项目里,H2 常被用于开发阶段。它的角色和 SQLite 有点像,适合轻量测试和本地运行。Maven 依赖可以这样写。

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>

Spring 配置里指定内存库。

# application.yaml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver

Java 团队常常已经有成熟的 Spring Boot 测试习惯,H2 可以自然融入单元测试和集成测试。业务上,我们可以把它理解成一个轻量沙盘。正式战场在 HANA 或 PostgreSQL,本地沙盘则帮助我们快速验证 CQL、事件处理器、服务行为和数据约束。

数据库无关查询是 CAP 的核心舒适区。只要我们使用标准 CQL 和 cds.ql API,很多代码可以跨数据库运行。

// Works on all databases
await SELECT.from(Books).where({ stock: { '>': 0 } });
await INSERT.into(Books).entries({ title: 'New Book' });
await UPDATE(Books, id).set({ stock: 50 });
await DELETE.from(Books, id);

这段代码朴素得像普通 CRUD,但背后有很强的工程意义。我们写的是业务意图,查询库存大于零的书,插入一本新书,更新库存,删除指定记录。至于底层是 SQLite、HANA 还是 PostgreSQL,交给 CAP 数据库服务适配。这样的抽象让团队少写很多方言代码,也让测试环境和生产环境之间更容易保持一致。

标准函数也很重要。字符串拼接、包含判断、前缀后缀检查、大小写转换、裁剪空白、长度计算、截取子串,这些操作在各数据库里都有各自写法。CAP 通过 CQL 标准函数把它们统一起来。像 concat(x, y)contains(x, y)startswith(x, y)endswith(x, y)tolower(x)toupper(x)trim(x)length(x)substring(x, i, n),这些函数看似普通,却能减少大量跨数据库兼容问题。

会话变量则把请求上下文带进数据访问逻辑。

// Available across all databases
cds.context.user;      // Current user
cds.context.tenant;    // Current tenant
cds.context.locale;    // User locale
cds.context.timestamp; // Request timestamp

在企业应用里,当前用户、租户、语言和请求时间并不是边角料。多租户 SaaS 需要 tenant 来隔离数据,审计日志需要 usertimestamp,多语言应用需要 locale 决定文本读取。一个采购订单服务,如果没有这些上下文,就很难处理权限、国际化和合规审计。CAP 把这些上下文放在统一位置,服务逻辑写起来会干净很多。

初始数据是另一个容易被忽略的主题。很多项目一开始只关注表结构,到了联调才发现国家代码、币种、审批状态、产品分类、角色映射这些基础数据没人管。CAP 推荐把 CSV 数据放在清晰的位置。

db/
├── schema.cds
└── data/
    ├── my.bookshop-Books.csv
    ├── my.bookshop-Authors.csv
    └── sap.common-Countries.csv

命名可以使用 <namespace>-<EntityName>.csv,也可以使用 <namespace>.<EntityName>.csv。CSV 内容像下面这样。

ID;title;author_ID;stock;price
1;Wuthering Heights;101;100;12.99
2;Jane Eyre;102;50;10.99

生产初始数据和测试数据最好分开。测试数据可以放到 test/data/

test/
└── data/
    ├── my.bookshop-Books.csv
    └── my.bookshop-Reviews.csv

这个区分非常实际。配置类数据可以进入 db/data,随部署进入生产。测试评论、测试订单、测试库存则放在 test/data,只服务开发和测试。我们做企业系统时,最怕测试数据混进生产,或者生产配置被开发样例覆盖。CAP 的目录约定,其实是在帮助团队形成边界感。

连接池配置关系到运行稳定性。开发机上一个连接也许就够,生产环境里却要考虑并发请求、数据库最大连接数、服务实例数量和超时策略。Node.js 项目可以在 cds.requires.db.pool 里配置。

{
  "cds": {
    "requires": {
      "db": {
        "pool": {
          "min": 0,
          "max": 10,
          "acquireTimeoutMillis": 10000,
          "idleTimeoutMillis": 30000
        }
      }
    }
  }
}

Java 项目常见的是 Hikari 配置。

spring:
  datasource:
    hikari:
      minimum-idle: 5
      maximum-pool-size: 20
      idle-timeout: 30000

连接池不是越大越好。一个 CAP 服务如果部署了多个实例,每个实例最大连接数都设得很激进,总连接数很容易把数据库打满。比较稳妥的做法,是根据数据库限制、应用实例数量、峰值并发和平均查询耗时一起算。比如客户服务系统每天白天会有明显高峰,查询多、写入少,可以适当放宽读请求容量。财务关账场景则可能短时间内有大量批处理写入,连接池和事务时长都要更谨慎。

数据约束应该尽量靠近数据本身。not null 可以阻止缺失关键字段。

entity Books {
  title : String(100) not null;
}

唯一性约束可以通过注解表达。

@assert.unique: { isbn: [isbn] }
entity Books {
  isbn : String(13);
}

外键完整性可以配置为数据库级检查。

{
  "cds": {
    "features": {
      "assert_integrity": "db"
    }
  }
}

业务系统里,约束不是为了让开发人员难受,而是为了防止数据变成烂账。ISBN 不应重复,订单行不应指向不存在的订单头,审批记录不应丢失标题。服务层当然可以校验,但数据库级约束是最后一道闸门。尤其当数据可能来自多个入口,比如 OData 服务、批量导入、后台作业、迁移脚本,底层约束就更重要。

尽量使用 CQL 并不等于完全禁止原生查询。遇到复杂优化、数据库特定函数或历史表结构时,Node.js 里可以直接跑 SQL。

const result = await cds.db.run(
  `SELECT * FROM my_bookshop_Books WHERE stock > ?`,
  [10]
);

Java 里也可以使用 JdbcTemplate。

@Autowired
JdbcTemplate jdbc;

List<Map<String, Object>> result = jdbc.queryForList(
  "SELECT * FROM my_bookshop_Books WHERE stock > ?",
  10
);

但原生 SQL 要像外科手术一样用。它能救场,也能留下长期维护成本。我们一旦写了原生 SQL,就要考虑数据库方言、表名映射、字段命名、安全参数绑定、事务上下文和迁移后的兼容性。上面示例使用参数占位符而不是拼接字符串,这是最低限度的安全习惯。企业应用里,任何拼接用户输入的 SQL 都可能变成审计事故。

profile-based configuration 是 CAP 数据库配置里最应该认真设计的一块。一个项目从开发到上线,数据库配置通常不是一份。

{
  "cds": {
    "requires": {
      "db": {
        "[development]": {
          "kind": "sqlite",
          "credentials": { "url": ":memory:" }
        },
        "[hybrid]": {
          "kind": "hana"
        },
        "[production]": {
          "kind": "hana"
        }
      }
    }
  }
}

开发模式默认可以直接 cds watch

# Development (default)
cds watch

# Hybrid
cds watch --profile hybrid

# Production
NODE_ENV=production cds serve

这套配置背后的思想,是把环境差异显式写出来,而不是靠口口相传。开发人员看到 [development] 就知道本地内存库会被使用,看到 [hybrid] 就知道服务本地跑、数据库远程连,看到 [production] 就知道生产数据库配置开始接管。对于团队协作来说,这比在 README 里写一长段注意事项更可靠。

把这些内容放在一起看,CAP 数据库配置其实分三层。最上层是业务建模,我们用 CDS 定义实体、关联、约束和类型。中间层是数据库服务,CAP 把 CQL、CSV、schema evolution、profile 和连接池组织起来。底层才是具体数据库,SQLite、SAP HANA Cloud、PostgreSQL、H2 各自承担不同角色。成熟的 CAP 项目,不会在这三层之间乱跳。能用 CDS 表达的,就不急着写原生 SQL。能用 profile 管理的,就不硬编码环境判断。能用 CSV 管理的基础数据,就不手工点数据库控制台。

我更愿意把 CAP 数据库配置看成一套项目纪律。SQLite 让我们跑得快,HANA 让我们站得稳,PostgreSQL 提供更多部署可能,H2 服务 Java 本地测试。CQL 让业务查询保持干净,CSV 让初始数据进入版本管理,连接池让运行时资源可控,约束让数据质量不完全依赖代码自觉,profile 则让每个环境各归其位。

一个企业系统能不能长期维护,很多时候不取决于第一次演示有多漂亮,而取决于半年后还能不能安全加字段,一年后还能不能平滑迁移,两年后还能不能解释每一份配置为什么存在。CAP 给了我们相当完整的数据库配置框架,真正的工程能力,是把这些机制放在合适的位置,不滥用,也不偷懒。数据库配置写得清楚,模型演进才有余地,服务部署才有把握,后面的 AI 能力、分析能力和扩展能力也才有地方落脚。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐