This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new f75951052 fix(studio): persist cleared optional instance and registry
fields (#4466)
f75951052 is described below
commit f75951052c4ba3290ce4459ef032f6d48a27e0fd
Author: 0 <[email protected]>
AuthorDate: Mon Sep 21 12:19:36 2026 +0800
fix(studio): persist cleared optional instance and registry fields (#4466)
Both updates replace every editable field of the row, but MyBatis-Plus
updateById omits null entity fields, so clearing an optional field silently
kept
its previous value: an instance's admin credential reference survived being
removed, and a NameServer registry entry kept an omitted k8s namespace, k8s
id or
description.
Assign those columns explicitly when the request omits them —
MybatisPlusInstanceRepository.save for the credential reference, and a new
NameserverRegistryService.clearOmittedOptionalColumns for the three registry
columns. Both paths carry a red/green test asserting the column is written
NULL.
Maintainer edit on top of the contribution: the two new test methods in
MybatisPlusInstanceRepositoryTest were renamed to end with Test, matching
the
project convention and the sibling methods this PR added to
NameserverRegistryServiceTest.
---
.../nameserver/NameserverRegistryService.java | 29 ++++++++++
.../instance/MybatisPlusInstanceRepository.java | 9 ++++
.../nameserver/NameserverRegistryServiceTest.java | 63 ++++++++++++++++++++++
.../MybatisPlusInstanceRepositoryTest.java | 30 +++++++++++
4 files changed, 131 insertions(+)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryService.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryService.java
index 615499813..908cb4095 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryService.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.cluster.nameserver;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.apache.rocketmq.studio.persistence.entity.RmqNameserver;
@@ -90,6 +91,7 @@ public class NameserverRegistryService {
// The unique index is the final guard against concurrent rename
collisions.
throw duplicateName(name);
}
+ clearOmittedOptionalColumns(entity);
RmqNameserver stored = nameserverMapper.selectById(entity.getId());
if (stored == null) {
// The row vanished between the update and the reload; do not
convert null to a VO.
@@ -141,4 +143,31 @@ public class NameserverRegistryService {
.gmtModified(entity.getGmtModified())
.build();
}
+
+ /**
+ * The registry update replaces every editable field of the entry, but
MyBatis-Plus
+ * {@code updateById} omits null entity fields. An omitted k8s namespace,
k8s id or
+ * description would therefore silently keep its previous value even
though the request
+ * submitted no value for it; assign those columns explicitly so the
stored entry matches
+ * what the request asked for.
+ */
+ private void clearOmittedOptionalColumns(RmqNameserver entity) {
+ UpdateWrapper<RmqNameserver> cleared = new UpdateWrapper<>();
+ boolean anyCleared = false;
+ if (entity.getK8sNamespace() == null) {
+ cleared.set("k8s_namespace", null);
+ anyCleared = true;
+ }
+ if (entity.getK8sId() == null) {
+ cleared.set("k8s_id", null);
+ anyCleared = true;
+ }
+ if (entity.getDescription() == null) {
+ cleared.set("description", null);
+ anyCleared = true;
+ }
+ if (anyCleared) {
+ nameserverMapper.update(null, cleared.eq("id", entity.getId()));
+ }
+ }
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
index 63b3e4d05..f63f9b4e7 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
@@ -18,6 +18,7 @@
package org.apache.rocketmq.studio.instance;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
import org.apache.rocketmq.studio.common.exception.BusinessException;
@@ -123,6 +124,14 @@ public class MybatisPlusInstanceRepository implements
InstanceRepository {
throw new BusinessException(409,
"Instance update was not applied: " + entity.getId());
}
+ if (instance.getAdminCredentialRef() == null) {
+ // updateById omits null entity fields, so a cleared reference
has to be
+ // assigned explicitly; otherwise the stored reference
survives an update that
+ // removed it.
+ instanceMapper.update(null, new UpdateWrapper<RmqInstance>()
+ .eq("id", entity.getId())
+ .set("admin_credential_ref", null));
+ }
} else {
instanceMapper.insert(entity);
instance.setId(entity.getId());
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryServiceTest.java
index e253677b1..29146acf5 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryServiceTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.rocketmq.studio.cluster.nameserver;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.apache.rocketmq.studio.persistence.entity.RmqNameserver;
import org.apache.rocketmq.studio.persistence.mapper.RmqNameserverMapper;
@@ -34,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -339,6 +341,67 @@ class NameserverRegistryServiceTest {
.hasMessageContaining("deleted concurrently");
}
+ @Test
+ void updateShouldClearOptionalColumnsThatTheRequestOmitsTest() {
+ // The Studio form submits a blanked optional field as an absent one,
and updateById
+ // skips null entity fields, so the cleared columns must be assigned
explicitly.
+ RmqNameserver existing = new RmqNameserver();
+ existing.setId(1L);
+ existing.setName("rocketmq1");
+ existing.setNamesrvAddr("rocketmq1-nameserver.svc:9876");
+ existing.setK8sNamespace("rocketmq1");
+ existing.setK8sId("k8s-1");
+ existing.setDescription("community chart cluster");
+
when(nameserverMapper.selectById(1L)).thenReturn(existing).thenReturn(existing);
+ when(nameserverMapper.selectCount(any())).thenReturn(0L);
+
when(nameserverMapper.updateById(any(RmqNameserver.class))).thenReturn(1);
+ when(nameserverMapper.update(isNull(),
any(UpdateWrapper.class))).thenReturn(1);
+
+ NameserverRegistryVO updated =
service.update(UpdateNameserverRegistryDTO.builder()
+ .id(1L)
+ .name("rocketmq1")
+ .namesrvAddr("rocketmq1-nameserver.svc:9876")
+ .build());
+
+ @SuppressWarnings("rawtypes")
+ ArgumentCaptor<UpdateWrapper> captor =
ArgumentCaptor.forClass(UpdateWrapper.class);
+ verify(nameserverMapper).update(isNull(), captor.capture());
+ assertThat(captor.getValue().getSqlSet())
+ .contains("k8s_namespace")
+ .contains("k8s_id")
+ .contains("description");
+
assertThat(captor.getValue().getParamNameValuePairs()).containsValue(null);
+ assertThat(existing.getK8sNamespace()).isNull();
+ assertThat(existing.getK8sId()).isNull();
+ assertThat(updated.getDescription()).isNull();
+ }
+
+ @Test
+ void updateShouldWriteOptionalColumnsSubmittedByTheRequestTest() {
+ RmqNameserver existing = new RmqNameserver();
+ existing.setId(1L);
+ existing.setK8sNamespace("old-namespace");
+ existing.setK8sId("old-k8s");
+ existing.setDescription("old description");
+
when(nameserverMapper.selectById(1L)).thenReturn(existing).thenReturn(existing);
+ when(nameserverMapper.selectCount(any())).thenReturn(0L);
+
when(nameserverMapper.updateById(any(RmqNameserver.class))).thenReturn(1);
+
+ service.update(UpdateNameserverRegistryDTO.builder()
+ .id(1L)
+ .name("rocketmq1")
+ .namesrvAddr("x:9876")
+ .k8sNamespace("new-namespace")
+ .k8sId("new-k8s")
+ .description("new description")
+ .build());
+
+ verify(nameserverMapper, never()).update(any(), any());
+ assertThat(existing.getK8sNamespace()).isEqualTo("new-namespace");
+ assertThat(existing.getK8sId()).isEqualTo("new-k8s");
+ assertThat(existing.getDescription()).isEqualTo("new description");
+ }
+
@Test
void updateShouldThrowWhenEntryVanishesBeforeReloadTest() {
RmqNameserver existing = new RmqNameserver();
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
index 40cbe0f14..c01d64b74 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepositoryTest.java
@@ -18,6 +18,7 @@
package org.apache.rocketmq.studio.instance;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
import org.apache.rocketmq.studio.common.exception.BusinessException;
@@ -39,6 +40,7 @@ import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -236,6 +238,34 @@ class MybatisPlusInstanceRepositoryTest {
assertThat(repository.deleteById(2L)).isFalse();
}
+ @Test
+ void saveShouldClearTheAdminCredentialRefWhenTheUpdateRemovesItTest() {
+ // updateById omits null entity fields, so a cleared reference has to
be assigned
+ // explicitly or the stored value survives the update.
+ InstanceVO vo = vo(5L, "instance-proxy-2", InstanceType.PROXY_CLUSTER);
+ vo.setAdminCredentialRef(null);
+ when(instanceMapper.updateById(any(RmqInstance.class))).thenReturn(1);
+ when(instanceMapper.update(isNull(),
any(UpdateWrapper.class))).thenReturn(1);
+
+ repository.save(vo);
+
+ @SuppressWarnings("rawtypes")
+ ArgumentCaptor<UpdateWrapper> captor =
ArgumentCaptor.forClass(UpdateWrapper.class);
+ verify(instanceMapper).update(isNull(), captor.capture());
+
assertThat(captor.getValue().getSqlSet()).contains("admin_credential_ref");
+
assertThat(captor.getValue().getParamNameValuePairs()).containsValue(null);
+ }
+
+ @Test
+ void saveShouldKeepTheAdminCredentialRefWhenTheUpdateCarriesOneTest() {
+ InstanceVO vo = vo(5L, "instance-proxy-2", InstanceType.PROXY_CLUSTER);
+ when(instanceMapper.updateById(any(RmqInstance.class))).thenReturn(1);
+
+ repository.save(vo);
+
+ verify(instanceMapper, never()).update(any(), any());
+ }
+
private RmqInstance entity(Long id, String name, InstanceType type) {
RmqInstance entity = new RmqInstance();
entity.setId(id);