This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new ffa7e7a941 [type:fix] Update parent and descendant tags
transactionally (#7255)
ffa7e7a941 is described below
commit ffa7e7a941d6eb24f42e0a3530b07bef63f30122
Author: Liming Deng <[email protected]>
AuthorDate: Wed Sep 30 11:45:26 2026 +0800
[type:fix] Update parent and descendant tags transactionally (#7255)
* fix(admin): update tag trees transactionally
* fix(admin): reject cyclic tag updates and roll back descendants
---
.../shenyu/admin/service/impl/TagServiceImpl.java | 11 ++-
.../admin/service/TagUpdateTransactionTest.java | 106 +++++++++++++++++++++
2 files changed, 114 insertions(+), 3 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/TagServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/TagServiceImpl.java
index 7a373675f5..963d0e1bff 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/TagServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/TagServiceImpl.java
@@ -36,6 +36,8 @@ import
org.springframework.transaction.annotation.Transactional;
import java.sql.Timestamp;
import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -89,6 +91,7 @@ public class TagServiceImpl implements TagService {
}
@Override
+ @Transactional(rollbackFor = Exception.class)
public int update(final TagDTO tagDTO) {
TagDO before = tagMapper.selectByPrimaryKey(tagDTO.getId());
Assert.notNull(before, "the updated tag is not found");
@@ -182,7 +185,7 @@ public class TagServiceImpl implements TagService {
relationMap.put(tagDO.getParentTagId(), list);
}
});
- recurseUpdateTag(allDataMap, relationMap, tagDTO.getId());
+ recurseUpdateTag(allDataMap, relationMap, tagDTO.getId(), new
HashSet<>());
}
/**
@@ -191,8 +194,10 @@ public class TagServiceImpl implements TagService {
* @param allData allData
* @param relationMap relationMap
* @param id id
+ * @param visited tags already visited during this update
*/
- private void recurseUpdateTag(final Map<String, TagDO> allData, final
Map<String, List<String>> relationMap, final String id) {
+ private void recurseUpdateTag(final Map<String, TagDO> allData, final
Map<String, List<String>> relationMap, final String id, final Set<String>
visited) {
+ Assert.isTrue(visited.add(id), "Cyclic tag hierarchy detected at tag:
" + id);
if (CollectionUtils.isEmpty(relationMap.get(id))) {
return;
}
@@ -201,7 +206,7 @@ public class TagServiceImpl implements TagService {
TagDO tagDO = allData.get(tagId);
tagDO.setExt(buildExtParamByParentTag(allData.get(id)));
tagMapper.updateByPrimaryKey(tagDO);
- recurseUpdateTag(allData, relationMap, tagId);
+ recurseUpdateTag(allData, relationMap, tagId, visited);
});
}
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagUpdateTransactionTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagUpdateTransactionTest.java
new file mode 100644
index 0000000000..a974042007
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/TagUpdateTransactionTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.shenyu.admin.service;
+
+import jakarta.annotation.Resource;
+import org.apache.shenyu.admin.AbstractSpringIntegrationTest;
+import org.apache.shenyu.admin.model.dto.TagDTO;
+import org.apache.shenyu.admin.exception.ValidFailException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Verify recursive tag updates are committed or rolled back together.
+ */
+public class TagUpdateTransactionTest extends AbstractSpringIntegrationTest {
+
+ @Resource
+ private TagService tagService;
+
+ @Resource
+ private JdbcTemplate jdbcTemplate;
+
+ @BeforeEach
+ public void setup() {
+ jdbcTemplate.update("INSERT INTO tag (id, tag_name, tag_desc,
parent_tag_id, ext) VALUES ('update-parent', 'old', '', '0', '{}')");
+ jdbcTemplate.update("INSERT INTO tag (id, tag_name, tag_desc,
parent_tag_id, ext) VALUES ('update-child', 'child', '', 'update-parent',
'{}')");
+ }
+
+ @AfterEach
+ public void cleanup() {
+ jdbcTemplate.update("DELETE FROM tag WHERE id IN ('update-child',
'update-parent')");
+ }
+
+ @Test
+ public void testParentFailureRollsBackDescendantUpdates() {
+ assertThrows(DataIntegrityViolationException.class, () ->
tagService.update(update("x".repeat(129))));
+ assertEquals("old", jdbcTemplate.queryForObject("SELECT tag_name FROM
tag WHERE id = 'update-parent'", String.class));
+ assertEquals("{}", childExt());
+ }
+
+ @Test
+ public void testExistingCycleIsRejectedWithoutCommittingChanges() {
+ jdbcTemplate.update("UPDATE tag SET parent_tag_id = 'update-child'
WHERE id = 'update-parent'");
+ TagDTO dto = update("new-name");
+ dto.setParentTagId("update-child");
+ assertThrows(ValidFailException.class, () -> tagService.update(dto));
+ assertEquals("old", jdbcTemplate.queryForObject("SELECT tag_name FROM
tag WHERE id = 'update-parent'", String.class));
+ assertEquals("{}", childExt());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"update-parent", "update-child"})
+ public void testSelfReferenceAndDescendantReparentingRollBack(final String
parentId) {
+ TagDTO dto = update("new-name");
+ dto.setParentTagId(parentId);
+ assertThrows(ValidFailException.class, () -> tagService.update(dto));
+ assertEquals("old", jdbcTemplate.queryForObject("SELECT tag_name FROM
tag WHERE id = 'update-parent'", String.class));
+ assertEquals("0", jdbcTemplate.queryForObject("SELECT parent_tag_id
FROM tag WHERE id = 'update-parent'", String.class));
+ assertEquals("{}", childExt());
+ }
+
+ @Test
+ public void testSuccessfulUpdateCommitsParentAndDescendants() {
+ assertEquals(1, tagService.update(update("new-name")));
+ assertEquals("new-name", jdbcTemplate.queryForObject("SELECT tag_name
FROM tag WHERE id = 'update-parent'", String.class));
+ assertNotEquals("{}", childExt());
+ }
+
+ private String childExt() {
+ return jdbcTemplate.queryForObject("SELECT ext FROM tag WHERE id =
'update-child'", String.class);
+ }
+
+ private TagDTO update(final String name) {
+ TagDTO dto = new TagDTO();
+ dto.setId("update-parent");
+ dto.setParentTagId("0");
+ dto.setName(name);
+ dto.setTagDesc("updated");
+ return dto;
+ }
+}