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;
+    }
+}

Reply via email to