点击此处查看详细分析解答
account组件UT测试报错“NOT NULL constraint failed: t_manager_account.Password”问题分析与解决方案
根据您提供的详细错误日志和上下文信息,我们对 account 组件在执行单元测试(UT)时出现的 NOT NULL constraint failed: t_manager_account.Password 错误进行了全面分析。以下将从问题定位、根本原因、修复方案以及后续预防措施四个方面进行系统性解答。
问题定位
您执行的命令:
bingo test -ut -jit -r artifactory-920bs
导致多个测试用例失败,例如:
TestAccount.test_when_set_history_password_0_to_5_should_change_diff_password_success
TestAccount.test_when_set_history_password_2_to_1_should_change_same_password_success
TestAccount.test_when_set_password_in_history_password_should_set_fail
这些用例均在执行到文件:
account_collection.lua:447
时报错:
/opt/bmc/libmc/lualib/database/row.lua:138: NOT NULL constraint failed: t_manager_account.Password
报错调用栈分析(关键路径):
account_collection.lua:447 → error(ret)
account_collection.lua:441 → account_in_server:password_validator(...)
account_collection.lua:434 → 创建 account_in_db 对象时未包含 Password 字段
该错误发生在尝试向数据库表 t_manager_account 插入新用户记录时,Password 字段为 nil 或空值,但此字段设置了 NOT NULL 约束,因此 SQLite 拒绝插入并抛出异常。
根本原因分析
1. 数据库约束机制说明
❖ NOT NULL constraint failed
- 这是 SQLite 数据库的一个完整性约束错误。
- 当某字段被定义为
notAllowNull = true 或为主键时,不允许插入空值(nil 或 "")。
- 来源知识库确认:
notAllowNull 是一个用于 MDS 模型中的属性配置,表示该字段是否允许为空。若设置为 true,则对应数据库字段添加 NOT NULL 约束。
—— 来自文档 “Lua开发框架”
❖ t_manager_account 表中 Password 字段不可为空
- 实体
t_network_ipv6 的案例中明确指出存在 NOT NULL constraint failed: Id;
- 类比可推知,
t_manager_account.Password 字段也应配置了 notAllowNull = true。
- 一旦尝试插入
Password = nil 或未设置该字段,即触发此错误。
2. 代码逻辑缺陷分析
查看 account_collection.lua 的相关代码片段(第 434 行):
local account_in_db = self.m_table_account({
Id = account_info.id,
UserName = account_info.name,
RoleId = account_info.role_id
})
关键问题暴露:
- 创建
account_in_db 对象时,传入的字段不包含 Password!
- 尽管后续调用了
password_validator 方法进行密码校验,但在调用前,对象已经被部分构造,且保存操作可能隐式触发。
- 当代码执行到
error(ret) 时(第 447 行),已经发生了 account_in_db 的插入或持久化操作,而此时 Password 仍未赋值 —— 导致数据库约束失败。
特别注意:
错误发生在 pcall 异常捕获结构内部,但在 error(ret) 前,account_in_db:delete() 被调用,但未保证删除成功或回滚整个事务。如果数据库操作已在外部提交,则即使删除也无法避免约束冲突。
此外,第 446 行尝试删除 history_password 表项,但 Password 的约束失败发生在主表 t_manager_account,因此前置删除操作无效。
3. 测试场景还原
以 test_when_set_history_password_0_to_5_should_change_diff_password_success 为例:
- 测试期望:设置历史密码检查数为 5,允许用户使用不同密码。
- 实际流程:
- 调用
new_account() 创建账户;
- 在
new_count_to_db_and_mdb 中创建 DB 对象,缺少 Password 参数;
- 执行密码策略验证(
password_validator),发生异常;
- 捕获异常并
error(ret),引发栈回溯;
- 此时数据库已尝试插入一条无
Password 值的记录 → 触发 NOT NULL constraint failed。
解决方案
修正代码:确保对象创建时字段完整或延迟持久化
方案一:【推荐】延迟数据库插入,直到所有校验通过
核心思想:不要在密码校验前创建数据库对象,避免中途失败造成非法状态。
修改 new_count_to_db_and_mdb 函数逻辑,在完成所有验证 后再 执行数据库插入。
function AccountCollection:new_count_to_db_and_mdb(ctx, account_info, account_class, is_ipmi_or_snmp, is_password_validator)
log:info("Start to add new account to db and mdb")
local account_type = account_info.oem and enum.AccountType.OEM:value() or enum.AccountType.Local:value()
-- Step 1: 先进行密码校验(若需要)
if not is_ipmi_or_snmp and is_password_validator then
local ok, ret = pcall(function()
-- 注意:此处 account_in_server 尚未与 DB 绑定
local mock_account = { id = account_info.id, UserName = account_info.name }
local validator = self.password_validator_collection:get_validator(account_type)
-- 调用验证逻辑(不依赖 DB 持久化)
validator:validate(ctx, account_info.name, account_info.password, true, false)
end)
if not ok then
log:error('Password validation failed: ' .. tostring(ret))
error(ret)
end
end
-- Step 2: 校验通过后,才插入数据库
local account_in_db = self.m_table_account({
Id = account_info.id,
UserName = account_info.name,
RoleId = account_info.role_id,
Password = account_info.password or "" -- 明确赋值,防 nil
})
local account_in_server = account_class.new(self.db, account_in_db,
self.password_validator_collection:get_validator(account_type), self.ipmi_channel_config)
-- 成功返回
return account_in_server
end
方案二:若必须前置创建对象,则预设默认 Password 值
如果架构要求必须提前创建 account_in_db,则必须为 Password 提供 非空默认值,如空字符串或占位符。
local account_in_db = self.m_table_account({
Id = account_info.id,
UserName = account_info.name,
RoleId = account_info.role_id,
Password = account_info.password or "" -- 添加此行
})
注意:此方式治标不治本,仍存在业务逻辑与数据持久化耦合的风险,仅作为临时兼容手段。
附加建议:增强异常处理机制
使用 pcall 或 xpcall 包裹整个数据库操作,并实现事务性回滚语义:
local ok, result = pcall(function()
-- 包括 account_in_db 创建、密码校验、mdb绑定等
end)
if not ok then
-- 清理部分写入的数据
if account_in_db and account_in_db.exists then
account_in_db:delete()
end
error(result)
end
预防措施与最佳实践
| 措施 |
说明 |
遵循 ORM 最佳实践 |
在调用 db.Table() 或 self.m_table_xxx() 创建对象前,确保所有 NOT NULL 字段都有值。 |
参考文档:ORM数据库操作说明 |
所有数据库操作都应考虑异常捕获(pcall),防止中断流程。 |
MDS模型配置检查 |
确保 t_manager_account 表中 Password 字段是否真的 必须非空。若允许初始为空(如待首次登录设置),应设为可空(notAllowNull=false)。 |
启用调试日志 |
在 row.lua 和 object.lua 添加日志输出,查看具体插入数据内容。 |
单元测试隔离 |
使用 mock 数据库或内存数据库(如 :memory:),避免状态污染。 |
关联知识补充(来自上下文)
1. NOT NULL Constraint 相关实体
Not Null Constraint:数据库规则,要求字段必须有值。
t_network_ipv6:曾因 Id 字段为空导致 NOT NULL constraint failed。
object.lua → row.lua 调用链中处理对象保存,是错误源头之一。
db.Table():创建数据库对象,若字段缺失则违反约束。
2. account 组件职责
- 负责用户管理、密码策略、历史密码防重用等(见
/search_result/3)。
- 密码策略相关参数包括
Custom_SamePwdCheckCount、History Password Count 等。
最终结论
当前错误本质是:在未完成密码校验的前提下,过早地创建了包含 NOT NULL 字段的数据库对象,且未赋值。
推荐修复方式:将数据库持久化操作延迟至所有校验通过之后执行,避免“半成品”对象写入数据库。
后续验证建议
- 修改代码后重新运行 UT:
bingo test -ut -jit -r artifactory-920bs
- 验证所有 previously failed test cases 是否变为
ok。
- 检查日志中是否仍有
NOT NULL constraint failed 报错。
- 可在
account_collection.lua 插入调试日志,打印 account_info.password 值是否为 nil。
只要确保在创建数据库对象时,Password 字段已赋有效值(或延迟创建),该问题即可彻底解决。