本节主要讨论kong插件中如何与数据库进行交互。
kong支持的数据库有postgreSql和Cassandra这两种数据库。由于本人没有接触过Cassandra数据库,所以本文所使用的数据库为postgreSql。
kong在持久化层提供了一个抽象,允许开发者操作自定义实体。这些实体对象称为DAO(Data Access Object)。
一、kong.db对象
kong网关中核心组件的实体是service、route、consumer和plugin。所有这些实体都可以通过kong.db.*全局单例对象访问,代码清单见代码:
local services_dao= kong.db.services
local routes_dao = kong.db.routes
local consumers_dao = kong.db.consumers
local plugins_dao = kong.db.plugins
用户自定义插件中的实体也可以通过kong.db.*全局单例对象访问。
二、DAO对象API
DAO对象负责操作存储在数据库中的表数据。这些数据与kong的实体对象一一对应。kong底层支持的数据库都遵循一套相同的接口。
插入service实体或者插件实体对象的操作非常简单,例如:
local inserted_service,err = kong.db.services:insert({
name = "mockbin",
url = "http://mockbin.org",
})
local inserted_plugin, err = kong.db.plugins:insert({
name = "key-auth",
service_id = {id = inserted_service.id},
})
kong自带的key-auth插件是一个很好的例子来展示DAO如何被使用的,key-auth插件。
三、自定义实体
用户如果想要在插件中使用自定义实体,首先需要根据情况定义一个或者多个实体。数据格式为Lua Table,其中描述了自定义实体的相关信息,包括实体的字段名称、数据类型等。实体的配置项与插件配置中的配置项有些类似,但实体的配置项多了一些额外的元信息,例如实体的主键。实体配置项在daos模块中定义。下面是kong的key-auth插件的daos模块定义:
local typedefs = require "kong.db.schema.typedefs"
-- 该插件会自动生成一个名为keyauth_credentials的DAO对象
return {
{
ttl = true, --用于过期自动删除:
primary_key = { "id" },
name = "keyauth_credentials", --数据库中对应的表
endpoint_key = "key",
cache_key = { "key" },
workspaceable = true, --根据命名空间进行隔离(支持多租户)
admin_api_name = "key-auths",
admin_api_nested_name = "key-auth",
fields = {
{
-- 主键
id = typedefs.uuid
},
{
-- 创建时间的时间戳,精确到秒
created_at = typedefs.auto_timestamp_s
},
{
-- 消费者ID作为外键
consumer = {
type = "foreign",
reference = "consumers",
required = true,
on_delete = "cascade",
},
},
{
-- 唯一的键值
key = {
type = "string",
required = false,
unique = true,
auto = true
},
},
{ tags = typedefs.tags },
},
},
}
主要的属性描述信息如表1所示:
名称 | 类型 | 是否必填 | 描述 |
---|---|---|---|
name | string | 是 | DAO对象的名称(kong.db.[name]) |
primary_key | table | 是 | 实体主键,大多数情况下kong插件实体时使用UUID作为主键,主键名为id。插件实体也支持复合主键。 |
endpoint_key | string | 否 | 在Admin API中作为备用标识符的字段,例如将endpoint_key设置为key,对于某个实体id=123,key="foo",则/keyauth_credentials/123和/keyauth_credentials/foo这两条URL是等价的。 |
cache_key | table | 否 | 用于生成cache_key所需的字段 |
generate_admin_api | boolean | 否 | 是否自动生成Admin API,默认情况下会为所有DAO对象生成Admin API,包括自定义的DAO对象。如果想要为DAO对象创建完全自定义的Admin API,或者想要完全禁用自动生成功能,可以将该项设置为false |
admin_api_name | string | 否 | 当启用generate_admin_api功能时,系统默认会使用name属性自动生成Admin API端点 |
admin_api_nested_name | string | 否 | 与admin_api_name属性类似 |
fields | table | 是 | 实体中每个字段的描述信息 |
我们在定义实体对象时,可以通过添加属性对每个实体对象的字段做约束。在使用DAO实体插入或者更新这些实体时,会先检查信息,如果输入的内容不符合规范,则返回错误。typedefs变量中包含大量实用的类型定义和别名字段,例如主键字段中最常用的typedefs.uuid属性或者created_at字段中较为常用的typedefs.auto_timestamp_s属性。表2罗列了一些常用的字段属性。
名称 | 类型 | 是否必填 | 描述 |
---|---|---|---|
type | string | 是 | 字段类型,支持一下标量类型:string、integer、number、boolean;还支持一下复合类型:array、 recode、set;除此之外,还可以取foreign:表示外键关系; |
default | any | 否 | 字段默认值。当该值为空时,会自动填充Lua语言中的默认值,而非数据库中的默认值。 |
required | boolean | 否 | 字段是否必填,当设置为true时,输入是缺少该字段会抛出错误。 |
unique | boolean | 否 | 字段是否唯一,当设置为true时,如果另一个实体已经存在会抛出错误。 |
auto | boolean | 否 | 是否自动填充字段,当type==“uuid”时,该字段会填充UUID;当type="string"时,该字段会填充随机字符串。如果字段名为created_at或者update_at,该字段将在插入或更新实体时填充当前时间。 |
reference | string | 否 | 当type是foreign类型时,该地段时必填的,且字段值必须时已经存在的schema名称。 |
on_delete | string | 否 | 当type是foreign类型时,该地段是必填的。该字段只是了当实体关联的外键对象被删除时执行什么操作。其有三个可选值,cascade(删除所有从属实体)、null(将从属实体的外键字段值设置为null)、restrict(执行操作出现错误) |
更多的字段属性可以参见:typedefs.lua。
四、DAO实体API
1、查询操作
查询操作模板语句:
local entity, err, err_t = kong.db.<name>:select(primary_key)
该语句会在数据库中查询对象并返回,可能会出现三种情况。
- 通过主键找到对应的实体,实体对象以Table格式返回。
- 查询过程中发生了错误,比如数据库连接丢失,此时entity参数返回nil,err参数返回描述错误原因的字符串,err_t参数也返回错误信息,但数据格式是Table。
- 没有找到对应的实体,也没有发生错误,entity参数返回nil,不返回错误信息。
2、遍历操作
遍历操作模板语句:
for entity, err on kong.db.<name>:each(entities_per_page) do
if err then
...
end
...
end
该方法可以创建分页请求,遍历数据库中的所有实体,entities_per_page参数控制每页返回的实体数,默认为100。每次迭代时,entity参数都会返回一个新对象。当参数错误时,err参数会填充错误内容。推荐读者在执行遍历操作时先检查错误信息,然后再执行自己逻辑。
3、插入操作
插入操作模板语句:
local entidy, err, err_t = kong.db.<name>:insert(<value>)
执行插入操作后,返回值包括三种情况:
- entity:返回插入实体的副本或者nil。
- err:返回错误信息,类型为字符串。
- err_t:返回错误信息,类型为Table。
4、更新操作
更新操作模板语句:
local entity,err,err_t = kong.db.<name>:update(primary_key, <values>)
执行更新操作的前提时系统可以找到与主键对应的实体值。执行更新操作后,返回值包括三中情况:
- entity:返回插入实体的副本或者nil。
- err:返回错误信息,类型为字符串。
- err_t:返回错误信息,类型为Table。
5、更新或插入操作
更新或者插入操作模板语句:
local entity, err, err_t = kong.db.<name>:upsert(primaty_key,<values>)
更新或插入操作融合了插入和更新两个操作。当primary_key对应的实体存在时,其类似于更新操作;当primary_key对应的实体不存在时,其类似于插入操作。
6、删除操作
删除操作模板语句:
local ok, err,err_t = kong.db.<name>:delete(primary_key)
在执行删除操作后,
返回值包括三中情况:
- ok:表示删除指定实体成功。
- err:返回错误信息,类型为字符串。
- err_t:返回错误信息,类型为Table。
五、migration模块
开发者在定义完实体后,还需要创建migration模块。kong启动时会根据用户自定义的migration模块创建数据库表。
migration模块的文件目录结构如下:
<plugin_name>
├── migrations
│ ├── init.lua
│ └── 000_base_header_plugin.lua
└── schema.lua
init.lua脚本的作用,根据kong官方文档的说明:
If there is no
init.lua
file inside already, you should create one. This is where all the migrations for your plugin will be referenced.如果migration目录中没有init.lua文件,则你需要创建一个。这是您的插件的所有迁移将被引用的地方。
kong框架执行插件数据库脚本是从init.lua脚本开始的,根据kong官方文档的说明,init.lua脚本为kong指明哪些数据库脚本需要被执行。例如,acl插件migration模块中的init.lua脚本:
return {
"000_base_acl",
"002_130_to_140",
"003_200_to_210",
"004_212_to_213",
}
该脚本指明acl插件的数据库脚本为000_base_acl.lua、002_130_to_140.lua、003_200_to_210.lua和004_212_to_213.lua共四个脚本文件。
当需要对插件进行迭代升级时,切记不能已经发布的迁移文件中进行修改,必须重新创建新的迁移文件(编写迁移文件做好做到可重复执行)。虽然没有严格的规则来命名迁移文件,但有一个惯例,即第一个文件以000为前缀,下一个文件以001为前缀,以此类推。
迁移文件的语法规范示例:
-- `<plugin_name>/migrations/000_base_my_plugin.lua`
return {
postgres = {
up = [[
CREATE TABLE IF NOT EXISTS "my_plugin_table" (
"id" UUID PRIMARY KEY,
"created_at" TIMESTAMP WITHOUT TIME ZONE,
"col1" TEXT
);
DO $$
BEGIN
CREATE INDEX IF NOT EXISTS "my_plugin_table_col1"
ON "my_plugin_table" ("col1");
EXCEPTION WHEN UNDEFINED_COLUMN THEN
-- Do nothing, accept existing state
END$$;
]],
}
}
-- `<plugin_name>/migrations/001_100_to_110.lua`
return {
postgres = {
up = [[
DO $$
BEGIN
ALTER TABLE IF EXISTS ONLY "my_plugin_table" ADD "cache_key" TEXT UNIQUE;
EXCEPTION WHEN DUPLICATE_COLUMN THEN
-- Do nothing, accept existing state
END;
$;
]],
teardown = function(connector, helpers)
assert(connector:connect_migrations())
assert(connector:query([[
DO $$
BEGIN
ALTER TABLE IF EXISTS ONLY "my_plugin_table" DROP "col1";
EXCEPTION WHEN UNDEFINED_COLUMN THEN
-- Do nothing, accept existing state
END$$;
]])
end,
}
}
每个策略都包含两端内容:up和teardown。
- up是原始SQL语句的可选字符串。在执行kong migrations up时执行这些语句。
- teardown是一个可选的Lua函数,它接受一个connector参数。connector参数可以调用查询方法来执行SQL查询。迁移完成后会触发teardown。
我们建议所有非破坏性操作,例如创建新表和添加新记录,都在up阶段完成。我们建议在teardown阶段上执行破坏性操作,例如删除数据、更改行类型和插入新数据。
所有SQL语句的编写都应该使它们尽可能具有可重入性。例如,使用DROP TABLE IF EXISTS而不是DROP TABLE,使用CREATE INDEX IF NOT EXIST而不是CREATE INDEX,等等。如果迁移由于某种原因失败,那么修复问题后重新执行指令即可。
参考资料:
- 《Kong网关 --入门、实战与进阶》
- Kong官方文档