在 Mendix 中实现这个自动 ID 生成逻辑,最佳实践是使用实体的 “提交前” (Before Commit) 事件处理微流。这样做可以确保无论通过何种方式创建对象(例如,通过页面新建、API导入等),这个ID生成逻辑都会被触发,保证了数据的一致性。
下面是详细的步骤和微流实现说明。
前期准备
- 实体 (Entity):假设你有一个实体,我们称之为
MyObject。 - ID 属性:在
MyObject实体中,创建一个名为ID的属性,其类型(Type)必须是字符串(String)。 - 启用创建日期:确保
MyObject实体在属性中勾选了系统成员createdDate。这在Mendix中是默认启用的。
步骤一:创建 “提交前” (Before Commit) 微流
- 双击你的实体
MyObject,打开其属性对话框。 - 切换到 “事件处理程序” (Event handlers) 标签页。
- 点击 “提交前” (Before Commit) 事件的 “选择…”(Select…) 按钮,然后选择 “新建”(New) 来创建一个新的微流。
- 为微流命名,一个好的命名规范是
BCO_MyObject_GenerateID(BCO 代表 Before Commit)。 - 点击“确定”,Mendix会自动为你创建一个微流,并传入一个
MyObject类型的参数。
步骤二:设计微流逻辑
这个微流的目标是生成ID并赋值给传入的 $MyObject 对象。以下是微流中的活动(Activity)顺序和配置。
微流概览图
[Start] -> (Create YearString) -> (Retrieve Objects) -> (Aggregate List) -> (Create SequenceNumber) -> (Create FormattedSequence) -> (Change MyObject) -> [End]
1. 创建变量:获取当前年份 (Create Year Variable)
- 活动类型: 创建变量 (Create Variable)
- 数据类型: 字符串 (String)
- 值/表达式:
formatDateTime([%CurrentDateTime%], 'yyyy') - 输出变量名:
YearString
说明: 这个活动使用
formatDateTime函数从系统变量[%CurrentDateTime%]中提取出四位数的年份字符串,例如 “2023”。
2. 检索对象:获取当年已创建的对象总数 (Retrieve Objects)
- 活动类型: 检索 (Retrieve)
- 来源: 从数据库 (From Database)
- 实体:
MyObject - 范围: 全部 (All)
- XPath 约束:
[year-from-dateTime(createdDate) = toInteger($YearString)] - 输出: 列表 (List),命名为
MyObjectList_ThisYear
说明: 这是关键的一步。
year-from-dateTime(createdDate): 这是一个 XPath 函数,用于从每个对象的createdDate属性中提取年份(整数类型)。toInteger($YearString): 我们将前面创建的年份字符串(如 “2023”)转换为整数,以便进行比较。- 这个XPath约束会筛选出所有
createdDate在当年的MyObject对象。
3. 聚合列表:计算对象数量 (Aggregate List)
- 活动类型: 聚合列表 (Aggregate List) 或 列表操作 (List Operation) (在较新版Mendix中)
- 输入列表:
$MyObjectList_ThisYear - 函数: 计数 (Count)
- 输出变量类型: 整数 (Integer)
- 输出变量名:
CountThisYear
说明: 这个活动计算上一步检索到的列表中的对象数量。如果今年还没有创建任何对象,
$CountThisYear的值将是0。
4. 创建变量:计算新序号 (Create Sequence Number Variable)
- 活动类型: 创建变量 (Create Variable)
- 数据类型: 整数 (Integer)
- 值/表达式:
$CountThisYear + 1 - 输出变量名:
SequenceNumber
说明: 将当年的对象总数加 1,得到新的序号。例如,如果已有3个对象,新序号就是
4。
5. 创建变量:格式化序号(补零) (Create Formatted Sequence Variable)
- 活动类型: 创建变量 (Create Variable)
- 数据类型: 字符串 (String)
- 值/表达式:
substring(toString(10000 + $SequenceNumber), 1) - 输出变量名:
FormattedSequence
说明: 这是 Mendix 中一个常用的小技巧,用于给数字左侧补零。
10000 + $SequenceNumber: 假设$SequenceNumber是12,结果就是10012。如果是123,结果就是10123。toString(...): 将数字转换为字符串,例如"10012"。substring(..., 1): 从字符串的第二个字符(索引为1)开始截取到末尾。"10012"就变成了"0012"。"10123"就变成了"0123"。- 这样,无论序号是几位数,我们都能得到一个固定的四位数、前面带零的字符串。
6. 更改对象:设置最终ID (Change Object)
- 活动类型: 更改对象 (Change Object)
- 对象:
$MyObject(微流的输入参数) - 提交: 否 (No)。因为这是 “提交前” 微流,Mendix 会在微流执行完毕后自动提交对象。
- 刷新在客户端: 是 (Yes)
- 设置成员:
- 点击 “新建” (New)。
- 成员:
ID - 值/表达式:
$YearString + $FormattedSequence
说明: 将年份字符串(如 “2023”)和格式化后的序号字符串(如 “0001”)拼接起来,形成最终的ID(“20230001”),并将其赋值给当前正在创建的
$MyObject对象的ID属性。
重要考虑:并发问题(Race Condition)
当系统并发量很高时,可能会出现两个用户在同一毫秒内几乎同时创建对象的情况。他们执行这个微流时,可能会查询到相同的 $CountThisYear,从而生成了重复的ID。
解决方案:
-
数据库唯一约束:最简单且强烈推荐的方法。在
MyObject实体的 “验证规则” (Validation Rules) 标签页中,为ID属性添加一个 “唯一” (Unique) 约束。- 优点:数据库层面保证了ID的唯一性。如果出现并发导致ID重复,第二个尝试提交的事务会失败,Mendix会抛出一个错误。你可以捕获这个错误并提示用户“操作失败,请重试”。
- 缺点:用户可能会看到错误提示。但在大多数场景下,这种并发冲突的概率极低,这是一个可接受的、健壮的解决方案。
-
使用锁机制或专门的序号生成器实体:对于极其严苛的系统,可以创建一个单独的“序号”实体来管理每年的计数器,并在生成ID时对该实体记录进行加锁。这种方法更复杂,只在上述唯一约束方案无法满足需求时才考虑。
通过以上步骤,你就成功地实现了一个健壮、自动化的ID生成器。
Mendix 自动生成 ID 微流实现 Q&A
问题 1:为什么选择使用“提交前” (Before Commit) 事件,而不是“创建后” (After Create) 或自定义的按钮微流?
回答:
这是为了保证 数据的一致性和逻辑的健壮性。
- “提交前” (Before Commit):这个事件在对象数据被写入数据库之前触发。它是最理想的位置,因为:
- 通用性:无论对象是通过页面新建、API 调用、还是其他微流创建的,这个事件都会被触发。你只需在一个地方定义逻辑,就能覆盖所有创建场景。
- 原子性:ID 的生成和对象的创建在同一个数据库事务中完成。如果ID生成失败,整个对象的创建也会失败,不会产生没有ID的“脏数据”。
- “创建后” (After Create):这个事件在对象被实例化(在内存中创建)后立即触发,但此时对象还 未提交到数据库。如果你在这个微流里去数据库查询已有的对象数量,是查不到当前这个新对象的,这会导致计数逻辑变得复杂且容易出错。
- 按钮微流:如果把逻辑放在一个“保存”按钮的微流里,那么只有通过点击这个按钮创建的对象才会有ID。如果将来有其他方式创建对象(如数据导入),你就必须复制粘贴这段逻辑,这违反了“不要重复自己”(DRY)的原则,难以维护。
结论:“提交前”事件是实现这类“创建时必须完成的业务逻辑”的最佳实践。
问题 2:微流中用于给序号补零的表达式 substring(toString(10000 + $SequenceNumber), 1) 是如何工作的?
回答:
这是一个在 Mendix 中非常常用且高效的技巧,用于给数字左侧补零,使其达到固定长度(这里是4位)。
让我们用一个例子来分解它,假设新序号 $SequenceNumber 是 35:
10000 + $SequenceNumber:计算10000 + 35,结果是整数10035。这个10000的作用是确保结果总是一个五位数。toString(...):将整数10035转换为字符串"10035"。substring(..., 1):从字符串的第二个字符(索引为1,Mendix中索引从0开始)开始截取,一直到字符串末尾。对于"10035",截取后得到的结果就是"0035"。
如果 $SequenceNumber 是 123,过程就是 10123 -> "10123" -> "0123"。
如果 $SequenceNumber 是 1,过程就是 10001 -> "10001" -> "0001"。
通过这种方式,无论原始序号是几位数,我们都能得到一个格式统一的四位字符串。
问题 3:如果系统并发量很大,两个用户在同一时刻创建对象,会不会生成重复的 ID?如何解决?
回答:
会的,这存在“竞态条件”(Race Condition)风险。 两个并发的微流可能会同时从数据库中查询到相同的“当年对象总数”,然后计算出完全相同的 SequenceNumber,最终生成重复的ID。
最佳解决方案:设置数据库唯一约束。
- 打开你的实体 (
MyObject) 的属性。 - 切换到 “验证规则” (Validation Rules) 标签页。
- 点击“新建”,规则类型选择 “唯一” (Unique),然后选择
ID属性。 - 设置一个错误消息,例如:“ID发生冲突,请重新保存。”
工作原理:
即使两个微流生成了相同的ID,当它们尝试向数据库提交数据时,数据库层面的唯一约束会阻止第二个事务成功提交。Mendix 会捕获这个数据库错误,并将其作为验证失败呈现给用户。虽然用户可能需要重试一次,但这从根本上保证了数据的唯一性和准确性,是解决此类并发问题的最可靠、最简单的方法。
问题 4:如果我删除了一个当年的对象,它的序号会被后续创建的新对象重用吗?
回答:
不会。 我们的微流逻辑是基于“当前数据库中已存在的对象数量”来计算新序号的。
例如:
- 今年已创建3个对象,ID分别为
20230001,20230002,20230003。 - 你删除了
20230002。现在数据库中只有2个今年的对象了。 - 此时,你再创建一个新对象。微流会查询到当前有2个对象,计算出的新序号是
2 + 1 = 3,生成的ID将是20230003。
哦,等等,这里会产生重复!这是一个很好的问题,它暴露了 Count + 1 逻辑的一个缺陷。
更优化的方案:获取最大序号
为了避免删除后ID重复的问题,我们应该获取当年的最大序号,然后加1。微流需要做如下调整:
- 检索 (Retrieve):和原来一样,获取当年所有对象列表
MyObjectList_ThisYear。 - (新)决策 (Decision):检查
MyObjectList_ThisYear是否为空 ($MyObjectList_ThisYear = empty)。- 如果为空 (true):说明是今年的第一个对象,直接将序号设置为1。
- 如果不为空 (false):执行下一步。
- (新)列表操作 (List Operation):
- 活动类型: 列表操作 (List Operation)
- 输入列表:
$MyObjectList_ThisYear - 操作: 查找 (Find)
- 查找方式: 最大值 (Maximum)
- 成员:
ID(是的,直接对字符串ID找最大值) - 输出变量:
MyObject_WithMaxID
- (新)创建变量 (Create Variable):
- 值:
toInt(substring($MyObject_WithMaxID/ID, 4)) - 说明: 截取最大ID的后四位序号部分,并转换为整数。
- 输出变量:
MaxSequence
- 值:
- 后续逻辑: 新的序号就是
$MaxSequence + 1。
这个改进后的逻辑确保了序号总是递增的,即使中间有数据被删除,也不会造成ID重复。
问题 5:如果我的应用中每年会产生数百万条记录,每次都去数据库检索并计数,性能会不会很差?
回答:
可能会。 如果没有适当的优化,对一个包含数百万记录的表执行 count 操作会很慢。
解决方案:为 createdDate 属性添加数据库索引。
- 在你的 Mendix Studio Pro 中,打开你的实体 (
MyObject)。 - 切换到 “索引” (Indexes) 标签页。
- 点击“新建”,将
createdDate属性添加进去。 - 重新部署你的应用,Mendix 会在数据库中为这个字段创建索引。
作用: 索引会极大地加快数据库基于 createdDate 进行查询和筛选的速度。微流中的 XPath 约束 [year-from-dateTime(createdDate) = ...] 将会利用这个索引,使得检索性能从“全表扫描”提升为“索引查找”,即使在海量数据下也能保持高效。

