Ottoman v2.0 介绍:Node.js 和 Couchbase 的 ODM





0/5 (0投票)
在本文中,我们将提供对为什么以及何时应该为您的下一个 Node.js 项目考虑 Ottoman 的基本理解。
代表 Couchbase 团队,我非常激动地宣布 Ottoman 2.0 正式可用。
Ottoman 是一个用于 Couchbase 和 Node.js 的对象文档映射器 (ODM) 库,它将存储在 Couchbase 中的 JSON 文档映射到原生的 JavaScript 对象。Ottoman 由 Couchbase Node.js SDK 提供支持,并内置了对 JavaScript 和 TypeScript 的支持。
典型的 Web 应用程序由前端、后端和数据存储组成。Ottoman 是您后端的一个组件,充当客户端应用程序框架和您的数据存储 Couchbase 之间的连接器。
为什么您需要为 Couchbase 使用 ODM
大多数客户端-服务器应用程序都需要某种抽象来管理数据访问。有些人可能会觉得“数据访问”这个词很简单,并可能很快将其误认为是基本 CRUD(创建、读取、更新和删除)操作,但这并不完全准确。
现代应用程序中的数据管理范围从简单的数据访问,到转换、验证和建模以满足不同用户和系统的需求。您可能会决定选择一个多语言数据库及其相关的数据访问模式,但无论哪种方式,数据的质量都必须干净且合法。
如果您来自关系数据库背景,您可能已经熟悉这些数据库自带的模式和约束,它们声称可以确保数据完整性。然而,在使用像 Couchbase 这样数据结构灵活的 NoSQL 数据库时,这可能会带来挑战。
在这种情况下,您可能会觉得必须构建自己的“模式管理器”库,该库需要定义模式、构建数据模型、验证数据、确保约束、管理关系等等。构建这样的东西可能会很快失控。这类系统不仅难以维护和扩展,而且很可能出错且耗时,导致交付延迟和数据质量下降。
找到一个已经包含上述所有功能的库至关重要。这就是像 Ottoman 这样的 ODM 让一切变得轻松。
Ottoman 如何简化 Node.js 开发
为了了解 Ottoman 如何帮助您的开发团队,让我们更深入地了解 Ottoman 2.0 的新功能。
本文中的示例基于 travel-sample 数据集,仅用于说明目的。此外,本文假设用户对 Couchbase 7.0 中的作用域和集合 有一定的基本了解。
模式和数据模型
Couchbase Server 7.0 中的 JSON 文档可以组织到作用域和集合中,使用户能够构建多租户微服务应用程序。
在 Ottoman 中,数据模型指示文档存储在哪个作用域和集合中,并提供了许多访问这些文档的方法。而模式则定义了文档的形状。
下面的代码示例定义了一个航空公司模式,该模式有五个字段,并指定了一些约束和默认值。例如,country 字段只能是 United States 和 Canada,并且在创建文档时是必需的。而 Capacity 是一个数字,如果指定,最大值为 1000。
默认情况下,模式是“严格”的,这意味着 Ottoman 被指示确保保存在数据库中的文档必须符合定义的模式结构,并且在保存时会忽略任何附加定义的字段。此 strict 选项可以通过 使用模式选项 进行覆盖。
const airlineSchema - new Schema({
id: { type: String }.
type: { type: String, required: true, default: 'airline' },
name: { type: String, required: true },
country: { type: String, required: true, enum: ['United States', 'Canada' },
capacity: { type: Number, ,ax: 1000 }
})
要使用 `airlineSchema`,您需要创建一个模型
const airlineModel = model('Airline', airlineSchema, { scopeName: 'default' })
第一个参数是模型的名称,如果未使用 模型选项 进行覆盖,它也是集合的名称。
Ottoman 文档与存储在 Couchbase 中的文档是一对一映射的。每个 文档 都是其模型的实例。
const airlineDocument = new airlineModel({
id: "10",
type: "airline",
name: "MILE-AIR",
country:"United States"
})
通过调用 `airlineModel` 上的 save 方法,您将在 Couchbase 数据库的 `_default` 作用域下的 Airline 集合中创建一个文档。
时间戳
timestamp 模式选项 指示 Ottoman 在创建文档时自动添加 `createdAt` 和 `updatedAt` 日期时间,默认值为当前日期和时间。每次更新文档时,`updatedAt` 时间戳也会被更新。
以下是将时间戳选项添加到航空公司模式的示例
const airlineSchema = new Schema({
id: { type: String },
type: { type: String, required: true, default: 'airline' },
name: { type: String, required: true },
country: { type: String, required: true, enum: ['United States', 'Canada'] },
capacity: { type: Number, max: 1000 }
}, {timestamps: true })
(上图)这一步本质上扩展了模式以隐式添加两个新字段。如果需要,您也可以显式调用它们并覆盖它们的名称,如下所示
不可变
将字段指定为“不可变的”会保留原始值并防止对指定字段的任何修改。在时间戳模式定义中,每次更新文档时,`created_at` 和 `updated_at` 字段都会被更新。
理想情况下,您不希望修改 `created_at` 字段。因为它用于跟踪文档的创建时间。这就是 immutable 选项发挥作用的地方。
const airlineSchema = new Schema({
id: { type: String },
type: { type: String, required: true, default: 'airline' },
name: { type: String, required: true },
country: { type: String, required: true, enum: ['United States', 'Canada'] },
capacity: { type: Number, max: 1000 } ,
created_at: { type: Date, default: new Date(), immutable: true},
updated_at: { type: Date, default: new Date() }
}, { timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' } })
有时您希望一个不可变字段能够被更新。特别是在创建文档且不可变字段没有默认值,或者由于其他原因无法避免更新时。在这种情况下,您可以在 mutation 操作中传递一个附加选项,`new : true`。
这一步假设 `airlineSchema` 有一个不可变字段 contact,并且您正在对 `airlineModel` 使用 `findOneandUpdate` 模型操作。
await airlineModel.findOneAndUpdate(
{ contact: { $like: '123456' } },
{ contact: '45678' },
{ new: true}
)
钩子 (Hooks)
Hooks 在 Ottoman 中是中间件异步函数,您可以编写并注册这些函数,以便在预定义事件触发之前(pre-hooks)或之后(post-hooks)进行操作。
Hooks 必须在模型定义之前定义,因此在工作流程中与模式定义一起定义 hooks 始终是最佳实践。
前面我们提到 `updated_at` 字段在变异时会被更新。内部,这是通过一个监听更新事件的 pre-hook 实现的。
airlineSchema.pre('update', (doc) => {
doc[updatedAt] = typeof currentTime === 'function'
? currentTime()
: currentTime
return doc
})
通常,hook 将事件名称作为第一个参数,后面跟着一个最终会被调用的回调函数。Hooks 可以注册用于验证、保存、更新和删除事件。
您可以考虑注册 hook 的一些用例包括
- 日志记录
- 清理资源
- 发送通知
- 更新其他相关文档
插件
使用 Ottoman 的关键优势之一是敏捷开发,因为您无需重复自己。相反,您最终会构建和使用可插拔的组件,这些组件不仅节省了时间和精力,而且能生成易于调试和维护的代码。
Plugins 通过允许您组件化某些功能来扩展 hooks 的行为,使您可以构建一次并在多个模式上使用它们。
此时,假设您所有的模式都有一个名为 `name` 的字段,并且您希望每次保存文档时将该字段的值转换为小写。这完全可以通过在 `save` 事件上使用 pre-hook 来实现,但您还要求将其应用于所有模式。这种情况就是插件可以发挥作用的地方。
在这种情况下,您正在定义一个“lowercase”插件,它将字段名 associated 的值转换为小写
const lowercase = (schema) => {
schema.pre('save', (doc) => {
doc['name'] = doc['name'].toLowerCase()
return doc
}]
}
然后,您指示模式使用该插件
airlineSchema.plugin(lowercase)
您还可以全局注册插件,以便在项目之间使用它们。
自定义模式类型
Ottoman 开箱即用地提供了一些 默认模式类型,如 string、number、boolean、date、array 等。然而,有时您可能希望构建一个可重用且定义良好的 复杂的自定义模式类型。
例如,假设您想将 `website_url` 添加到 `airlineSchema`。您有什么选择?唯一可用的选择是 string。选择 string 没有问题,但是唯一需要注意的是,不能保证 `website_url` 是格式正确的。这是一个典型的应该使用自定义模式类型的场景。
创建自定义模式类型是一个三步过程
1. 定义自定义模式类型
class LinkType extends IOttomanType {
constructor(name) {
super(name, 'Link')
}
cast(value) {
if(!isLink(String(value))) {
throw new ValidationError('Field ${this.name} only allows Link')
}
return String(value)
}
}
function isLink(value) {
const regExp = new RegExp(
/[-a-zA-Z0-9@:%._\+~#-]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&amo;//=]*)?/gi
)
return regExp.test(value)
}
函数 `isLink` 包含验证 URL 是否格式正确的逻辑。
2. 注册自定义模式类型
var LinkTypeFactor = (name) => new LinkType(name)
registerType(LinkType.name, LinkTypeFactory)
3. 使用自定义模式类型
const airlineSchema = new Schema({
id: { type: String },
type: { type: String, required: true, default: 'airline' },
name: { type: String, required: true },
country: { type: String, required: true, enum: ['United States', 'Canada'] },
capacity: { type: Number, max: 1000 },
created_at: { type: Date, default: new Date(), immutable: true},
updated_at: { type: Date, default: new Date() },
website_url: { type: LinkType }
}, { timestampts : { createdAt: 'created_at', updatedAt: 'update_at' } })
现在,每次创建或更新文档时,都会验证 `website_url` 是否为格式正确的 URL。
自定义验证器
有时,检查字段的完整性超出了使用 default、min、max 或 schema types 等基本约束的范围。例如,您可能想将字段类型声明为 array,但您也可能想限制 array 的长度。这时就应该使用 自定义验证器。
让我们扩展 `airlineSchema` 以接受电话号码数组,但不接受超过两个电话号码。首先,您需要使用 `addValidators` 方法注册验证器,如下所示
addValidators({
phoneLength: (value) => {
if(value.length > 2) {
throw new Error('Only two phone numbers are allowed at the maximum.')
}
}
})
const airlineSchema = new Schema({
id: { type: String },
type: { type: String, required: true, default:'airline' },
name: { type: String, required: true },
country: { type: String, required: true, enum: ['United States', 'Canada'] },
capacity: { type: Number, max: 1000 },
created_at: { type: Date, default: new Date(), immutable: true },
updated_at:{ type: Date, default: new Date() },
website_url: { type: LinkType } ,
phone_number: [{type: String, valudator: 'phoneLength' }]
}, { timestamps : { createdAt: 'created_at', updatedAt: 'updated_at' } })
参考
数据建模——在 NoSQL 数据库领域有时被称为“文档设计”——是数据管理的重要组成部分。在关系数据库中,数据存储在表中,表之间的关系由外键(也称为参照键)管理。
在 Couchbase 中,相似类型的数据存储在同一个集合中,它们使用文档键(或简称“键”)引用其他文档。当文档以这种方式设计时,Ottoman 不仅提供了在模式设计期间引用它们的方法,还通过 populate 方法 自动填充它们。
为了更好地理解这个特性,我们将通过定义一个 `routeSchema` 和一个 `routeModel` 来创建一个路由。路由有一个 `airline` 字段,它使用 `ref` 关键字引用一个 airline 模型。
const routeSchema = new Schema({
id: { type: String } ,
airline: { type: String, ref: 'Airline' },
destination_airport: { type: String },
source_airport: { type: String },
stop: { type: Number },
type: { type: String }
})
下面我们创建一个引用 airline ID 10 的路由文档。
const routeDocument = new routeModel({
id: "route1",
type: "route",
source_airport: "LAX",
destination_airport: "DFW",
stops: 0,
airline: "10"
})
通过调用 `routeModel` 上的 save 方法,您将在 Couchbase 数据库的 `_default` 作用域下的 `Route` 集合中创建一个文档。
最后,我们检索文档并填充文档。
const laxRouteDocument = await routeModel.findOne( { source_airport: 'LAX' } )
await laxRouteDocument._populate('airline')
我们检索的路由文档嵌入了 airline 数据,如下所示
{
"id": "route1",
"type": "route",
"source_airport ": "LAX",
"destination_airport": "DFW",
"stops": "0",
"airline": {
"id": "10",
"type": "airline",
"name": "MILE-AIR",
"country": "United States"
}
}
查询生成器
Ottoman 拥有非常丰富的 API,可以处理 Couchbase 和 N1QL 查询语言 支持的许多复杂操作。
Query Builder 在后台为您创建 N1QL 语句。使用 Query Builder 时,您有三种模式选项
索引
Indexes 在从 Couchbase 数据库访问文档方面起着重要作用。
没有正确的索引会导致性能下降。因此,提前了解您的文档设计(即数据模型)以及您将对它们执行的查询非常重要。构建索引是处理数据时的关键步骤。Ottoman 提供三种可以与您的模式关联的索引类型。
索引类型 #1: N1QL
N1QL 索引是 Ottoman 使用的默认索引类型,有时也称为 GSI 或全局二级索引。在引导过程中,Ottoman 会自动创建几个二级索引,确保一些基本操作开箱即用且高效。这是最推荐的索引类型。
airlineSchema.index.findByName = {
by: 'name',
type: 'n1q1'
}
索引类型 #2: Refdoc
refdoc 索引类型管理需要保证唯一性的某些要求。在考虑 refdoc 索引之前,有几件重要的事情需要您注意
- Refdoc 索引由 Ottoman 严格管理,而不是由 Couchbase 数据库管理。这意味着任何在 Ottoman 之外对文档所做的更改都将导致 refdoc 索引不同步。
- Refdoc 索引会创建一个额外的二进制文档,其中包含对正在索引的文档键的引用。简而言之,您将为每个使用 refdoc 索引创建的文档看到一个额外的文档。
例如,假设您希望确保 `airlineSchema` 中的 `website_url` 是唯一的。您将创建一个 refdoc 索引,如下所示
airlineSchema.index.findByUrl = {
by: website_url,
type: 'refdoc'
}
refdoc 索引的一般最佳实践是谨慎使用,并通过 Ottoman 严格处理数据变异。
索引类型 #3: Views
此索引类型已弃用,很快将被删除。此索引类型仅出于向后兼容性而存在。强烈不鼓励使用此索引。
精益
Ottoman 提供了许多模型方法来从集合中检索文档。这些方法包括 `find`、`findById`、`findOne` 等。
`find` 方法是最受欢迎的,它根据指定的搜索条件返回多个文档。一次从集合中检索大量文档会带来性能开销,而处理少量文档时可能看不到这种开销。开销主要是因为所有指定的模型方法都返回 Ottoman Document 类的实例,该类包含大量 Ottoman 内部状态变更跟踪信息。
启用 lean 选项 告诉 Ottoman 跳过实例化完整的 Ottoman 文档,而是只返回普通的 JavaScript 对象 (POJO)。
虽然这可能会提高性能,但它会牺牲 Ottoman 的内置功能,如更改跟踪、验证、hooks 以及 save、remove 等典型的模型方法。因此,建议谨慎并充分了解情况后再使用 `lean`。
const result = await airlineMode.find( { name: { $like: 'american' } }, { lean: true } )
上面示例中的 `result` 对象将包含所有 name 类似于 `american` 的 airline 文档,但所有这些文档都将是纯 JavaScript 对象(即,您将失去与 Ottoman 文档关联的所有魔力)。
模型方法
Ottoman 的模型附带了 几个辅助方法,用于不同的目的。以下是一些最常用的模型方法
find: `find` 是一个通用方法,它在后台使用查询服务,并用于根据提供的过滤器条件从 Couchbase 集合中检索一个或多个文档。高效使用 `find` 需要创建适当的二级索引(即 N1QL 索引)。
以下 find 操作返回所有 country 为 United States 的 airline 文档,并且忽略大小写。
const docs = await airlineModel.find({ country: "United States" }, { ignoreCase: true})
findOneAndUpdate: `find` 基于传递给它的过滤器条件查找单个文档,并使用提供的新值更新该文档。传递 `upsert : true` 选项可确保在找不到匹配文档时创建一个新文档。
await airlineModel.findOneAndUpdate({ id: "10" }}. country: "Canada" }, { upsert: true })
批量操作:有时您可能需要一次变异多个文档。Ottoman 提供了三种模型方法来帮助解决这个问题,它们都在后台使用查询服务
- createMany:一次创建多个文档
- removeMany:一次删除多个文档
- updateMany:一次更新多个文档
只要没有发生错误,所有批量操作的响应状态都将是“Success”,否则将是“Failure”。
调试
正如您已经看到的,有相当多的模型操作在底层使用 N1QL 查询语言。优化 N1QL 查询和构建正确的索引是开发的重要组成部分。
为了实现这一点,了解这些模型操作使用的是哪种 N1QL 查询很重要。 Ottoman 启用了调试,它会在开发控制台中打印 N1QL 语句,开发人员可以有效地使用这些语句来分析和 使用 UI 创建索引 。
Ottoman 与 Node.js SDK 有何不同?
虽然 Ottoman 由 Node.js SDK 提供支持,但值得注意的是,Ottoman 具有某些仅对您可用的功能。在选择其中一个而非另一个时,您可能需要考虑以下功能。
使用 Ottoman 的其他好处
我希望您已经感到兴奋并准备好使用 Ottoman 编写您的第一个应用程序。以下是我们客户偏爱 Ottoman 的一些主要原因
适应性
您不需要专门的技能,只需要了解 JavaScript 或 TypeScript 即可。
可负担性
享受开源的所有好处。没有供应商锁定,降低资本支出,没有专有许可证等。
可支持性和可持续性
将扫描和修补软件以解决安全漏洞的负担留给我们。获得与服务器版本同步的持续软件更新,并从我们的支持团队和庞大的开发人员社区获得全面支持。
敏捷性
成为您所在领域的领导者:快速及时地构建和交付应用程序。无需从头开始构建数据层。将您的时间花在解决业务问题上,而不是编码。编写易于维护和阅读的小代码块。即使经过多次迭代,您的代码看起来也会相似,因为它非常简单。
数据质量
使用模式、验证器、约束和其他可用模块确保数据质量。通过 Ottoman 的“管道”生成无 bug 的代码,这是深思熟虑和精心设计的对重复性设计和开发挑战的观察的结果。Ottoman 解决了许多常见问题,如果手动编码,这些问题可能非常困难且容易出错。
立即开始使用 Ottoman
现在您已经对为什么以及何时应该为您的下一个 Node.js 项目考虑 Ottoman 有了基本的了解,是时候动手实践了!
以下是一些有用的链接,可以帮助您入门
- 阅读 Ottoman 文档
- 为 Ottoman repo 贡献代码
- 在 Couchbase 论坛上提问(或回答!)问题
- 阅读关于 Ottoman 1.0 的博文
- 收听关于 Ottoman 的播客
- 观看 2020 年 Couchbase Connect 关于 Ottoman 的演讲
准备好亲自试用 Ottoman 了吗?
从这里的示例项目开始