重构:使用 MyBatis-Flex 迁移应用 ORM
将应用自管表的 JdbcClient 数据访问迁移为实体、Mapper、构造器查询和必要的显式 SQL。 保留 AgentScope 自管表及原有业务语义,并补充事务、查询与数据库集成测试。
This commit is contained in:
57
server/src/main/resources/mapper/AgentEventMapper.xml
Normal file
57
server/src/main/resources/mapper/AgentEventMapper.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.AgentEventMapper">
|
||||
|
||||
<!-- 显式结果映射避免 payload 列与实体 payloadJson 属性名称不同而丢失事件负载。 -->
|
||||
<resultMap id="agentEventResultMap" type="tech.easyflow.manuagent.entity.AgentEventEntity">
|
||||
<id property="id" column="id"/>
|
||||
<result property="projectId" column="project_id"
|
||||
typeHandler="tech.easyflow.manuagent.typehandler.UuidTypeHandler"/>
|
||||
<result property="runId" column="run_id"
|
||||
typeHandler="tech.easyflow.manuagent.typehandler.UuidTypeHandler"/>
|
||||
<result property="eventType" column="event_type"/>
|
||||
<result property="eventId" column="event_id"/>
|
||||
<result property="payloadJson" column="payload"
|
||||
typeHandler="tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler"/>
|
||||
<result property="createdAt" column="created_at"/>
|
||||
</resultMap>
|
||||
|
||||
<!--
|
||||
PostgreSQL INSERT ... RETURNING 同时完成写入和序号读取,不使用“先插入、再查最大值”
|
||||
这种在并发场景下会取错事件的实现。affectData 保留正确的事务与缓存语义。
|
||||
-->
|
||||
<select id="insertReturning" resultMap="agentEventResultMap" affectData="true" flushCache="true">
|
||||
INSERT INTO app.agent_event(project_id, run_id, event_type, event_id, payload)
|
||||
VALUES (
|
||||
#{event.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{event.runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{event.eventType},
|
||||
#{event.eventId},
|
||||
#{event.payloadJson, jdbcType=OTHER,
|
||||
typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler})
|
||||
<!-- 与迁移前 JDBC 返回字段一致;event_id 已完成持久化,但无需再次回传给业务层。 -->
|
||||
RETURNING id, project_id, run_id, event_type, payload, created_at
|
||||
</select>
|
||||
|
||||
<!-- PostgreSQL JSONB 运算仅封装在数据库适配层,业务服务不感知方言细节。 -->
|
||||
<select id="selectLatestStartedPhase" resultType="string">
|
||||
SELECT payload ->> 'phase'
|
||||
FROM app.agent_event
|
||||
WHERE run_id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND event_type = 'RUN_STARTED'
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectLatestMaterialResponseJson" resultType="string">
|
||||
SELECT payload::text
|
||||
FROM app.agent_event
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND event_type = 'ASK_RESPONDED'
|
||||
AND jsonb_typeof(payload -> 'decisions') = 'array'
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
</mapper>
|
||||
58
server/src/main/resources/mapper/AgentRunMapper.xml
Normal file
58
server/src/main/resources/mapper/AgentRunMapper.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.AgentRunMapper">
|
||||
|
||||
<!-- 以下更新均将“当前状态”写进 WHERE,更新行数就是状态机竞争结果。 -->
|
||||
<update id="completeWaiting">
|
||||
UPDATE app.agent_run
|
||||
SET status = 'COMPLETED', pending_interrupt = NULL, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND status = 'WAITING_INPUT'
|
||||
</update>
|
||||
|
||||
<update id="interruptRunning">
|
||||
UPDATE app.agent_run
|
||||
SET status = 'INTERRUPTED', pending_interrupt = NULL,
|
||||
error_code = 'USER_STOPPED', error_message = '用户已停止运行',
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
</update>
|
||||
|
||||
<update id="waitForInput">
|
||||
UPDATE app.agent_run
|
||||
SET status = 'WAITING_INPUT',
|
||||
pending_interrupt = #{interruptJson, jdbcType=OTHER,
|
||||
typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
</update>
|
||||
|
||||
<update id="completeRunning">
|
||||
UPDATE app.agent_run
|
||||
SET status = 'COMPLETED', pending_interrupt = NULL,
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
</update>
|
||||
|
||||
<update id="failRunning">
|
||||
UPDATE app.agent_run
|
||||
SET status = 'FAILED', pending_interrupt = NULL,
|
||||
error_code = 'AGENT_RUN_FAILED', error_message = #{message},
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
</update>
|
||||
|
||||
<update id="interruptRunningAfterRestart">
|
||||
UPDATE app.agent_run
|
||||
SET status = 'INTERRUPTED', pending_interrupt = NULL,
|
||||
error_code = 'PROCESS_RESTARTED', error_message = '服务重启,运行已中断',
|
||||
ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE status = 'RUNNING'
|
||||
</update>
|
||||
</mapper>
|
||||
37
server/src/main/resources/mapper/ArtifactMapper.xml
Normal file
37
server/src/main/resources/mapper/ArtifactMapper.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ArtifactMapper">
|
||||
|
||||
<!--
|
||||
PostgreSQL 的 INSERT ... RETURNING 属于会修改数据的查询语句。
|
||||
affectData 与 flushCache 确保 MyBatis 按 DML 事务语义处理并清理一级缓存。
|
||||
-->
|
||||
<select id="upsert"
|
||||
resultType="tech.easyflow.manuagent.entity.ArtifactEntity"
|
||||
affectData="true"
|
||||
flushCache="true">
|
||||
INSERT INTO app.artifact(
|
||||
id, project_id, run_id, kind, name, relative_path, mime_type,
|
||||
size_bytes, sha256, metadata_json)
|
||||
VALUES (
|
||||
#{artifact.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{artifact.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{artifact.runId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{artifact.kind}, #{artifact.name}, #{artifact.relativePath}, #{artifact.mimeType},
|
||||
#{artifact.sizeBytes}, #{artifact.sha256},
|
||||
#{artifact.metadataJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler})
|
||||
ON CONFLICT (project_id, relative_path) DO UPDATE SET
|
||||
run_id = EXCLUDED.run_id,
|
||||
kind = EXCLUDED.kind,
|
||||
name = EXCLUDED.name,
|
||||
mime_type = EXCLUDED.mime_type,
|
||||
size_bytes = EXCLUDED.size_bytes,
|
||||
sha256 = EXCLUDED.sha256,
|
||||
metadata_json = EXCLUDED.metadata_json,
|
||||
published_at = CURRENT_TIMESTAMP
|
||||
<!-- 发布接口只需要产物视图字段,下载字段由独立下载查询按需读取。 -->
|
||||
RETURNING id, project_id, run_id, kind, name, size_bytes, metadata_json, published_at
|
||||
</select>
|
||||
</mapper>
|
||||
18
server/src/main/resources/mapper/ModelAssignmentMapper.xml
Normal file
18
server/src/main/resources/mapper/ModelAssignmentMapper.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ModelAssignmentMapper">
|
||||
|
||||
<!-- 角色为主键,单语句 upsert 避免并发设置默认模型时出现先查后写竞态。 -->
|
||||
<insert id="upsert">
|
||||
INSERT INTO app.model_assignment(role, model_config_id, assigned_by)
|
||||
VALUES (
|
||||
#{assignment.role},
|
||||
#{assignment.modelConfigId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{assignment.assignedBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler})
|
||||
ON CONFLICT (role) DO UPDATE SET
|
||||
model_config_id = EXCLUDED.model_config_id,
|
||||
assigned_by = EXCLUDED.assigned_by,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
</insert>
|
||||
</mapper>
|
||||
63
server/src/main/resources/mapper/ModelConfigMapper.xml
Normal file
63
server/src/main/resources/mapper/ModelConfigMapper.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ModelConfigMapper">
|
||||
|
||||
<!--
|
||||
JSONB 参数必须显式使用 JsonbStringTypeHandler。这样即使 Lambda Wrapper 在 Spring 初始化前
|
||||
触发了 MyBatis-Flex 的全局 TableInfo 缓存,模型写入仍不会退化为 VARCHAR 参数绑定。
|
||||
-->
|
||||
<insert id="insertModel">
|
||||
INSERT INTO app.model_config(
|
||||
id, name, provider, base_url, model_id,
|
||||
api_key_ciphertext, api_key_hint, key_version,
|
||||
config_json, capabilities_json, enabled, is_default, created_by)
|
||||
VALUES (
|
||||
#{model.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{model.name},
|
||||
#{model.provider},
|
||||
#{model.baseUrl},
|
||||
#{model.modelId},
|
||||
#{model.apiKeyCiphertext},
|
||||
#{model.apiKeyHint},
|
||||
#{model.keyVersion},
|
||||
#{model.configJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
#{model.capabilitiesJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
COALESCE(#{model.enabled}, TRUE),
|
||||
COALESCE(#{model.defaultModel}, FALSE),
|
||||
#{model.createdBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler})
|
||||
</insert>
|
||||
|
||||
<!--
|
||||
API Key 为空表示保留已有密钥;更新时间统一由数据库生成,避免应用时钟和数据库时钟混用。
|
||||
-->
|
||||
<update id="updateModel">
|
||||
UPDATE app.model_config
|
||||
SET name = #{model.name},
|
||||
base_url = #{model.baseUrl},
|
||||
model_id = #{model.modelId},
|
||||
config_json = #{model.configJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
capabilities_json = #{model.capabilitiesJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
<if test="model.apiKeyCiphertext != null">
|
||||
api_key_ciphertext = #{model.apiKeyCiphertext},
|
||||
api_key_hint = #{model.apiKeyHint},
|
||||
key_version = #{model.keyVersion},
|
||||
</if>
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{model.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
</update>
|
||||
|
||||
<!-- 以下两条语句保持迁移前的执行顺序和条件,不额外引入模型启用状态判断。 -->
|
||||
<update id="clearDefault">
|
||||
UPDATE app.model_config
|
||||
SET is_default = FALSE
|
||||
WHERE is_default
|
||||
</update>
|
||||
|
||||
<update id="setDefault">
|
||||
UPDATE app.model_config
|
||||
SET is_default = TRUE,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
</update>
|
||||
</mapper>
|
||||
42
server/src/main/resources/mapper/ProjectMapper.xml
Normal file
42
server/src/main/resources/mapper/ProjectMapper.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ProjectMapper">
|
||||
|
||||
<!-- 项目删除前必须先阻止仍在运行的任务。 -->
|
||||
<select id="hasRunningRun" resultType="boolean">
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM app.agent_run
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND status = 'RUNNING'
|
||||
)
|
||||
</select>
|
||||
|
||||
<!-- 以下删除顺序与外键依赖顺序一致,并由 ProjectService 的 Spring 事务统一提交或回滚。 -->
|
||||
<delete id="deleteEvents">
|
||||
DELETE FROM app.agent_event
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
<delete id="deleteArtifacts">
|
||||
DELETE FROM app.artifact
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
<delete id="deletePlans">
|
||||
DELETE FROM app.project_plan
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
<delete id="deleteFiles">
|
||||
DELETE FROM app.project_file
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
<delete id="deleteRuns">
|
||||
DELETE FROM app.agent_run
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
</delete>
|
||||
|
||||
<update id="updateStatus">
|
||||
UPDATE app.project
|
||||
SET status = #{status}, version = version + 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
</update>
|
||||
</mapper>
|
||||
50
server/src/main/resources/mapper/ProjectPlanMapper.xml
Normal file
50
server/src/main/resources/mapper/ProjectPlanMapper.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.ProjectPlanMapper">
|
||||
|
||||
<!--
|
||||
版本号计算与写入保持在同一条 PostgreSQL 语句内;唯一约束继续作为并发冲突的最终保护。
|
||||
-->
|
||||
<select id="insertNextDraft"
|
||||
resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity"
|
||||
affectData="true"
|
||||
flushCache="true">
|
||||
INSERT INTO app.project_plan(id, project_id, plan_version, status, plan_json, created_by)
|
||||
SELECT
|
||||
#{plan.id, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
#{plan.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
COALESCE(MAX(plan_version), 0) + 1,
|
||||
'DRAFT',
|
||||
#{plan.planJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
#{plan.createdBy, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
FROM app.project_plan
|
||||
WHERE project_id = #{plan.projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
RETURNING id, project_id, plan_version, status, plan_json, confirmed_at, created_at
|
||||
</select>
|
||||
|
||||
<select id="selectCurrent" resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity">
|
||||
SELECT id, project_id, plan_version, status, plan_json, confirmed_at, created_at
|
||||
FROM app.project_plan
|
||||
WHERE project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
ORDER BY CASE status WHEN 'CONFIRMED' THEN 0 ELSE 1 END, plan_version DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 条件更新和 RETURNING 在同一语句中完成,避免确认状态检查与写入之间出现竞态。 -->
|
||||
<select id="confirmDraft"
|
||||
resultType="tech.easyflow.manuagent.entity.ProjectPlanEntity"
|
||||
affectData="true"
|
||||
flushCache="true">
|
||||
UPDATE app.project_plan
|
||||
SET status = 'CONFIRMED',
|
||||
plan_json = #{planJson, jdbcType=OTHER, typeHandler=tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler},
|
||||
confirmed_by = #{userId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler},
|
||||
confirmed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = #{planId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND project_id = #{projectId, typeHandler=tech.easyflow.manuagent.typehandler.UuidTypeHandler}
|
||||
AND status = 'DRAFT'
|
||||
RETURNING id, project_id, plan_version, status, plan_json, confirmed_at, created_at
|
||||
</select>
|
||||
</mapper>
|
||||
35
server/src/main/resources/mapper/SkillConfigMapper.xml
Normal file
35
server/src/main/resources/mapper/SkillConfigMapper.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="tech.easyflow.manuagent.mapper.SkillConfigMapper">
|
||||
|
||||
<!-- AgentScope 表严格只读;应用只在 app.skill_config 保存启停、来源和校验状态。 -->
|
||||
<sql id="skillViewColumns">
|
||||
s.name, s.description, c.version, c.source_type, c.enabled, c.read_only,
|
||||
c.validation_status, c.validation_message, c.updated_at
|
||||
</sql>
|
||||
|
||||
<select id="selectViews" resultType="tech.easyflow.manuagent.mapper.SkillViewRow">
|
||||
SELECT <include refid="skillViewColumns"/>
|
||||
FROM agentscope.agentscope_skills s
|
||||
JOIN app.skill_config c ON c.skill_name = s.name
|
||||
ORDER BY c.source_type, s.name
|
||||
</select>
|
||||
|
||||
<select id="selectView" resultType="tech.easyflow.manuagent.mapper.SkillViewRow">
|
||||
SELECT <include refid="skillViewColumns"/>
|
||||
FROM agentscope.agentscope_skills s
|
||||
JOIN app.skill_config c ON c.skill_name = s.name
|
||||
WHERE s.name = #{name}
|
||||
</select>
|
||||
|
||||
<!-- 保持迁移前 JDBC SQL 的过滤条件和数据库时间戳语义。 -->
|
||||
<update id="updateEnabled">
|
||||
UPDATE app.skill_config
|
||||
SET enabled = #{enabled},
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE skill_name = #{name}
|
||||
AND validation_status = 'VALID'
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user