Sails 数据源(Datastore)实战指南:配置、无模型访问与底层数据库操作
Sails 数据源Datastore实战指南配置、无模型访问与底层数据库操作【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails**Datastore数据源**是 Sails 应用访问数据库的入口抽象它代表应用所配置的一个具体数据源通常对应一个真实的数据库实例。本文以 docs/reference/waterline/datastores/datastores.md 为主线完整讲解数据源的配置方式、Sails 启动时自动注册的数据源实例、如何脱离模型直接访问底层数据库以及.manager、.driver、.leaseConnection()、.sendNativeQuery()、.transaction()五个核心 API 的用法与源码印证。读完本文你将能够为应用配置单库或多库环境并在需要原生 SQL、事务或连接池复用等场景下直接操控底层数据库。什么是 DatastoreDatastores代表为应用配置的数据源。一个 datastore 通常对应一个特定的数据库例如本地 MySQL 服务器中运行的数据库公司数据中心里远程部署的 PostgreSQL 数据库云服务商托管的远程 MongoDB 数据库。在 Sails 中所有数据库访问最终都经由 datastore 完成。每个 模型 都与某一个 datastore 建立绑定关系因此每当调用模型的内置方法如.find()、.create()时模型都会隐式地与其配置的 datastore 通信开发者在大多数场景下无需关心底层连接细节。配置 Datastores数据源在sails.config.datastores中配置。在标准 Sails 应用中对应的物理文件是config/datastores.js。该文件是一组告诉 Sails 在执行内置模型方法如.find()、.create()时到哪里读取或保存数据的配置主要用于配置开发数据库以及被个别模型使用的独立数据库详见 docs/anatomy/config/datastores.js.md。Sails 应用默认自带一个隐式implicitdatastore所有模型默认都会使用它。对很多应用来说这一个默认数据源已经足够但如果你的应用需要同时操作多个数据库可以为额外的、具名的 datastore 添加配置例如legacyProductDb// config/datastores.js module.exports.datastores { // 隐式默认数据源所有未显式指定 datastore 的模型都会使用它 default: { adapter: sails-mysql, url: mysql://rootlocalhost:3306/my_app_db, }, // 额外的具名数据源供特定模型或代码显式引用 legacyProductDb: { adapter: sails-mysql, url: mysql://rootlocalhost:3306/legacy_products, }, };配置好具名数据源后可以让某个模型单独绑定它在模型的datastore属性中声明也可以在运行时通过sails.getDatastore(legacyProductDb)直接获取该数据源实例。关于各配置项的详细说明可参阅 sails.config.models 与 sails.config.connections 相关文档。无模型使用 Datastore注册的数据源实例虽然模型会自动与其 datastore 通信但有时我们需要脱离任何特定模型直接与数据源对话。为此当应用启动lift时Sails 会为每个配置的数据源自动实例化一个对象称为注册的数据源实例registered datastore instances。在运行时获取某个数据源实例有两种方式全局方法sails.getDatastore()// 获取默认数据源 var datastore sails.getDatastore(); // 获取具名数据源 var legacyDatastore sails.getDatastore(legacyProductDb);模型方法.getDatastore()获取某个特定模型所绑定的数据源var datastore Pet.getDatastore();注意.getDatastore()是同步方法无需await、Promise 链或传统 Node 回调。获取到的注册数据源实例对外暴露了自己的方法与属性例如.leaseConnection()和.manager它们提供了直接与底层数据库对话的便捷途径。本小节文档的其余页面正是对这些数据源方法/属性的详细说明下面逐一展开。.manager底层连接管理器.manager是当前数据源的实时连接管理器live connection manager。根据所使用适配器的不同它可能代表一个连接池connection pool单个连接或者一个预配置的客户端库实例的引用。用法datastore.manager;示例直连 MongoDB 集合由于sails-mongo暴露的连接管理器实际上就是 Mongo 客户端driver的db实例可以直接把它当作 Mongo 的db使用// sails-mongo 的连接管理器就是 Mongo 客户端的 db 实例可以当作 db 使用 var db Pet.getDatastore().manager; // 现在可以做任何我们能用 Mongo db 实例做的事 var rawMongoCollection db.collection(Pet.tableName);这里使用Pet.tableName而非硬编码表名可以保证即使在模型表名改变时代码仍然正确。该属性的完整说明见 .manager。.driver通用的底层驱动.driver是当前数据源的通用、无状态、底层驱动如果适配器支持的话。⚠️ 该属性不保证对所有数据库适配器都存在。如果数据源底层适配器不支持标准化的驱动接口那么.driver将不存在。用法datastore.driver;示例动态连接任意数据库想象你在构建一个类似 phpMyAdmin 的结构化数据可视化工具可能需要动态地连接各种不同的数据库。此时.driver的通用性就派上用场了// 获取我们数据库例如 MySQL的通用无状态驱动 var Driver sails.getDatastore().driver; // 用它创建我们自己的动态连接管理器例如连接池 var managerReport await Driver.createManager({ connectionString: req.param(connectionUrl) }); var manager managerReport.manager; var db; try { db (await Driver.getConnection({ manager: managerReport.manager })).connection; } catch (err) { await Driver.destroyManager({ manager: managerReport.manager }); throw err; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 在这里做一些操作…… // 例如 // await Driver.sendNativeQuery({ // connection: db, // nativeQuery: ... // }); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 最后在继续之前拆除动态连接管理器 // 同时也会释放上面获取的活动连接 await Driver.destroyManager({ manager: managerReport.manager }); return res.ok();可以看到.driver提供了createManager、getConnection、sendNativeQuery、destroyManager等标准驱动接口方法适合需要完全掌控连接生命周期的高级场景。详见 .driver。.leaseConnection()租用连接执行多条查询.leaseConnection()从数据源租用一个新的连接用于在同一条连接上执行多条查询即让during函数中提供的逻辑可以复用这条数据库连接。用法await datastore.leaseConnection(during);或者var result await datastore.leaseConnection(during);参数参数类型说明1during((function))一个过程参数procedural parameter。当连接就绪后 Sails 会自动调用它并传入下方表格中的参数。during函数收到的参数参数类型说明1db((ref))新租用的数据库连接。关于用它做什么可参见.usingConnection()。注意在 Sails 1.1.0 之前.leaseConnection()的推荐用法是让during代码在结束时调用回调proceed。只要你的during函数签名中不包含第二个参数这个回调就不再必要——直接用async/await或返回 Promise 即可。返回值类型说明((Ref?))during函数返回的可选结果数据。例如你在during中写了return foo;那么这里就是foo。错误名称类型抛出时机UsageError((Error))传入了无效参数时AdapterError((Error))数据库适配器内部出错时Error((Error))发生其他意外情况时Sails 与 Waterline 中错误的协商方式可参考 Concepts Models and ORM Errors。示例租用连接执行两条查询后归还从默认数据源租用一个数据库连接用它发送两条查询再将其释放回连接池var inventory await sails.getDatastore() .leaseConnection(async (db) { var location await Location.findOne({ id: inputs.locationId }) .usingConnection(db); if (!location) { let err new Error(Cannot find location with that id (inputs.locationId)); err.code E_NO_SUCH_LOCATION; throw err; } // 获取该位置的所有产品 var productOfferings await ProductOffering.find({ location: inputs.locationId }) .populate(productType) .usingConnection(db); return productOfferings; }) .intercept(E_NO_SUCH_LOCATION, notFound); // 完成我们对数据库连接所做的操作都成功了。 // 现在可以继续执行业务逻辑。关键点在于Location.findOne(...)与ProductOffering.find(...)都通过.usingConnection(db)显式指定使用租来的这条连接从而保证两条查询落在同一条连接上。完整文档见 .leaseConnection()。.sendNativeQuery()执行原生 SQL.sendNativeQuery()使用当前数据源执行原生 SQL 查询。⚠️.sendNativeQuery()仅对配置了 SQL 数据库例如 MySQL、SQL Server、PostgreSQL的 Sails/Waterline 数据源可用。具体的 SQL 语法与结果格式因数据库而异需要查阅你所使用的底层数据库适配器的文档。用法var rawResult await datastore.sendNativeQuery(sql, valuesToEscape);参数参数类型说明1sql((string))以对应数据库方言书写的 SQL 字符串。支持$1、$2等模板语法见下方示例。如果你使用了自定义表名或列名务必直接引用这些物理名称而不是模型标识符和属性名。2valuesToEscape((array?))需要被 SQL 转义并注入sql的动态、不可信字符串数组。如果没有动态值需要注入省略该参数或传入空数组即可。返回值类型说明((Ref?))数据库适配器返回的原始结果如果有。具体格式取决于传入的 SQL 查询以及所使用的适配器/方言。错误名称类型抛出时机UsageError((Error))传入了无效参数时AdapterError((Error))数据库适配器内部出错时Error((Error))发生其他意外情况时示例参数化查询下面是一个通用示例适用于几乎所有关系型数据库。请记住用法与结果数据会随 SQL 查询以及适配器/方言的不同而变化。Sails/Waterline 的标准 MySQL 适配器 使用mysqlNPM 包PostgreSQL 适配器 使用pg包。// 构建我们的 SQL 查询模板 var NAMES_OF_PETS_SQL SELECT pet.name FROM pet WHERE pet.species_label $1 OR pet.species_label $2; // 发送给数据库 var rawResult await sails.getDatastore().sendNativeQuery(NAMES_OF_PETS_SQL, [ dog, cat ]); sails.log(rawResult); // (结果格式取决于传入的 SQL 查询以及所使用的适配器/方言) // 然后解析原始结果做你想做的事 return exits.success();示例中$1、$2是占位符实际值由valuesToEscape数组按顺序注入由适配器负责 SQL 转义从而避免 SQL 注入。自定义表名/列名的处理你书写的 SQL 应引用表名和列名而不是模型标识符和属性名。如果你的模型定义了自定义表名或属性定义了自定义列名请确保在原生 SQL 中使用这些自定义名称。如果担心自定义表名/列名散落在代码中难以维护可以借助 Waterline 模型暴露的tableName与columnName引用动态构建 SQL 模板var NAMES_OF_PETS_SQL SELECT ${Pet.tableName}.${Pet.schema.name.columnName} FROM ${Pet.tableName} WHERE ${Pet.tableName}.${Pet.schema.speciesLabel.columnName} $1 OR ${Pet.tableName}.${Pet.schema.speciesLabel.columnName} $2 ;但请注意返回结果中的自定义列名问题仍需要自己处理。.sendNativeQuery()返回的rawResult本质上与数据库强相关、绑定在物理层因此它会继承你在物理层设置的各种复杂性包括模型定义中的自定义表名/列名。注意事项该方法只适用于 SQL 数据库。如果使用的是 MongoDB 等其他数据库请通过.manager获取原始 MongoDB 客户端或通过.driver获取静态底层数据库库如mysql、pg等。取决于所使用的适配器valuesToEscape可能会被原地修改mutated。这是出于性能考虑做出的刻意设计可能在 Sails 未来的大版本中改变。因此如果你把某个变量传给valuesToEscape且后续还要用到该变量请先克隆一份再传入。完整文档见 .sendNativeQuery()。.transaction()数据库事务.transaction()获取一个预配置的、延迟执行deferred的对象它挂接在sails-mysql或sails-postgresql适配器以及相应的 driver上用于在事务中执行一组操作。用法await datastore.transaction(during);或者var result await datastore.transaction(during);参数参数类型说明1during((function))见下方 during参数 表格。during函数收到的参数参数类型说明1db((ref))租用的事务性数据库连接。参见.usingConnection()。注意在 Sails 1.1.0 之前.transaction()的推荐用法是让during代码在结束时调用回调proceed。只要during函数签名中不包含第二个参数就不再需要该回调。返回值类型说明((Ref?))during函数返回的可选结果数据。例如在during中写了return foo;则这里就是foo。错误名称类型抛出时机UsageError((Error))传入了无效参数时AdapterError((Error))数据库适配器内部出错时Error((Error))发生其他意外情况时示例账户转账将指定金额从一个用户余额中减去并加到另一个用户余额中// 例如在某个 action 中 var flaverr require(flaverr); await sails.getDatastore() .transaction(async (db) { var myAccount await BankAccount.findOne({ owner: this.req.session.userId }) .usingConnection(db); if (!myAccount) { throw new Error(Consistency violation: Database is corrupted-- logged in user record has gone missing); } var recipientAccount await BankAccount.findOne({ owner: inputs.recipientId }).usingConnection(db) if (!recipientAccount) { throw flaverr(E_NO_SUCH_RECIPIENT, new Error(There is no recipient with that id)); } // 计算从当前登录用户余额中扣除后的新余额 var myNewBalance myAccount.balance - inputs.amount; // 如果这会让当前用户余额为负则中止 // 事务会自动回滚 if (myNewBalance 0) { throw flaverr(E_INSUFFICIENT_FUNDS, new Error(Insufficient funds)); } // 更新当前用户账户 await BankAccount.update({ owner: this.req.session.userId }) .set({ balance: myNewBalance }) .usingConnection(db); // 更新收款人账户 await BankAccount.update({ owner: inputs.recipientId }) .set({ balance: recipientAccount.balance inputs.amount }) .usingConnection(db); }) .intercept(E_INSUFFICIENT_FUNDS, ()badRequest) .intercept(E_NO_SUCH_RECIPIENT, ()notFound);事务机制保证只要during内任何一步抛出错误例如余额不足整个事务都会自动回滚两个账户的余额不会出现只扣不增或只增不扣的不一致状态。通过.intercept()可以把自定义错误码优雅地映射为badRequest、notFound等 Sails 响应。提示上面的示例仅为演示用途在实际项目中这类增减/递减逻辑还应包含行级锁row-level locking以避免并发场景下的竞态条件。完整文档见 .transaction()。源码印证数据源 API 在 Sails 内部的实际使用Sails 自身的 Blueprint 路由实现就大量使用了本文介绍的数据源 API。以 lib/hooks/blueprints/actions/create.js 和 lib/hooks/blueprints/actions/update.js 为例其中都有如下形式的调用注释// Model.getDatastore().transaction(function during(db, proceed){ ... })在 lib/hooks/blueprints/actions 目录下create.js、update.js、destroy.js、add.js、remove.js、replace.js等多个内置 action 都引用了getDatastore这表明Sails 在框架内部正是通过Model.getDatastore()获取数据源实例再在其上执行事务等底层操作。你在自定义代码中通过sails.getDatastore()/Model.getDatastore()获得的正是同一类数据源实例API 完全一致。总结与最佳实践场景推荐 API对应文档获取默认/具名数据源实例sails.getDatastore(name?)sails.getDatastore()获取某模型绑定的数据源Model.getDatastore().getDatastore()访问底层连接池/客户端datastore.manager.manager动态创建连接管理器连接池datastore.driver.driver同一条连接上执行多条查询datastore.leaseConnection(during).leaseConnection()执行原生 SQL仅 SQL 数据库datastore.sendNativeQuery(sql, values).sendNativeQuery()事务性多步数据库操作datastore.transaction(during).transaction()实践要点绝大多数场景无需直接操作数据源——模型方法会自动与绑定数据源通信多数据库应用通过在config/datastores.js中配置具名数据源并用sails.getDatastore(name)获取需要事务、原生 SQL 或连接复用时记得配合.usingConnection()让查询显式使用租来的连接使用.sendNativeQuery()时务必通过$1、$2占位符配合valuesToEscape注入动态值防止 SQL 注入并注意valuesToEscape可能被适配器原地修改只有 SQL 数据源支持.sendNativeQuery()与.transaction()MongoDB 等场景请使用.manager或.driver。【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考