diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java index 3942cc6ad2..795ed1f145 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultTbUserSettingsService.java @@ -185,9 +185,9 @@ public class DefaultTbUserSettingsService implements TbUserSettingsService { Map dashboardTitles = new HashMap<>(); uniqueIds.forEach(id -> { - var dashboardInfo = dashboardService.findDashboardInfoById(tenantId, new DashboardId(id)); - if (dashboardInfo != null && StringUtils.isNotEmpty(dashboardInfo.getTitle())) { - dashboardTitles.put(id, dashboardInfo.getTitle()); + var title = dashboardService.findDashboardTitleById(tenantId, new DashboardId(id)); + if (StringUtils.isNotEmpty(title)) { + dashboardTitles.put(id, title); } } ); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d96e50ca7f..e040e0bc5e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -474,6 +474,10 @@ cache: userSettings: timeToLiveInMinutes: "${CACHE_SPECS_USER_SETTINGS_TTL:1440}" maxSize: "${CACHE_SPECS_USER_SETTINGS_MAX_SIZE:100000}" + dashboardTitles: + timeToLiveInMinutes: "${CACHE_SPECS_DASHBOARD_TITLES_TTL:1440}" + maxSize: "${CACHE_SPECS_DASHBOARD_TITLES_MAX_SIZE:100000}" + #Disable this because it is not required. spring.data.redis.repositories.enabled: false diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index fd971fd72a..680cf19822 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Ignore; @@ -1104,6 +1105,30 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { Assert.assertEquals(savedDashboard2.getId().getId(), starred.getId()); Assert.assertEquals(savedDashboard2.getTitle(), starred.getTitle()); + //TEST renaming in the cache. + savedDashboard1.setTitle(RandomStringUtils.randomAlphanumeric(10)); + savedDashboard1 = doPost("/api/dashboard", savedDashboard1, Dashboard.class); + savedDashboard2.setTitle(RandomStringUtils.randomAlphanumeric(10)); + savedDashboard2 = doPost("/api/dashboard", savedDashboard2, Dashboard.class); + + newSettings = doGet("/api/user/dashboards/" + savedDashboard1.getId().getId() + "/unstar", UserDashboardsInfo.class); + Assert.assertNotNull(newSettings); + Assert.assertNotNull(newSettings.getLast()); + Assert.assertEquals(2, newSettings.getLast().size()); + lastVisited = newSettings.getLast().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), lastVisited.getTitle()); + Assert.assertTrue(lastVisited.isStarred()); + lastVisited = newSettings.getLast().get(1); + Assert.assertEquals(savedDashboard1.getId().getId(), lastVisited.getId()); + Assert.assertEquals(savedDashboard1.getTitle(), lastVisited.getTitle()); + Assert.assertFalse(lastVisited.isStarred()); + Assert.assertNotNull(retrievedSettings.getStarred()); + Assert.assertEquals(1, newSettings.getStarred().size()); + starred = newSettings.getStarred().get(0); + Assert.assertEquals(savedDashboard2.getId().getId(), starred.getId()); + Assert.assertEquals(savedDashboard2.getTitle(), starred.getTitle()); + doDelete("/api/dashboard/" + savedDashboard1.getId().getId().toString()).andExpect(status().isOk()); newSettings = doGet("/api/user/dashboards", UserDashboardsInfo.class); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index c6c87c07bf..c9974bba69 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -36,7 +36,7 @@ public interface DashboardService extends EntityDaoService { DashboardInfo findDashboardInfoById(TenantId tenantId, DashboardId dashboardId); -// String findDashboardTitleById(TenantId tenantId, DashboardId dashboardId); + String findDashboardTitleById(TenantId tenantId, DashboardId dashboardId); ListenableFuture findDashboardInfoByIdAsync(TenantId tenantId, DashboardId dashboardId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index eedf925d61..b09d92bd19 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -42,4 +42,5 @@ public class CacheConstants { public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes"; public static final String VERSION_CONTROL_TASK_CACHE = "versionControlTask"; public static final String USER_SETTINGS_CACHE = "userSettings"; + public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles"; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index e0d1777112..cb6a5050e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.dashboard; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; @@ -77,4 +79,5 @@ public interface DashboardInfoDao extends Dao { DashboardInfo findFirstByTenantIdAndName(UUID tenantId, String name); + String findTitleById(UUID tenantId, UUID dashboardId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 96d56148eb..d1198d4d7a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -19,8 +19,12 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; @@ -36,6 +40,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -43,6 +48,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.user.UserSettingsEvictEvent; import java.util.List; import java.util.Optional; @@ -70,6 +76,25 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Autowired private DataValidator dashboardValidator; + @Autowired + protected TbTransactionalCache cache; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + protected void publishEvictEvent(DashboardTitleEvictEvent event) { + if (TransactionSynchronizationManager.isActualTransactionActive()) { + eventPublisher.publishEvent(event); + } else { + handleEvictEvent(event); + } + } + + @TransactionalEventListener(classes = DashboardTitleEvictEvent.class) + public void handleEvictEvent(DashboardTitleEvictEvent event) { + cache.evict(event.getKey()); + } + @Override public Dashboard findDashboardById(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing findDashboardById [{}]", dashboardId); @@ -91,6 +116,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return dashboardInfoDao.findById(tenantId, dashboardId.getId()); } + @Override + public String findDashboardTitleById(TenantId tenantId, DashboardId dashboardId) { + return cache.getAndPutInTransaction(dashboardId, + () -> dashboardInfoDao.findTitleById(tenantId.getId(), dashboardId.getId()), true); + } + @Override public ListenableFuture findDashboardInfoByIdAsync(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing findDashboardInfoByIdAsync [{}]", dashboardId); @@ -103,8 +134,13 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb log.trace("Executing saveDashboard [{}]", dashboard); dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); try { - return dashboardDao.save(dashboard.getTenantId(), dashboard); + var saved = dashboardDao.save(dashboard.getTenantId(), dashboard); + publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); + return saved; } catch (Exception e) { + if (dashboard.getId() != null) { + publishEvictEvent(new DashboardTitleEvictEvent(dashboard.getId())); + } checkConstraintViolation(e, "dashboard_external_id_unq_key", "Dashboard with such external id already exists!"); throw e; } @@ -170,6 +206,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb deleteEntityRelations(tenantId, dashboardId); try { dashboardDao.removeById(tenantId, dashboardId.getId()); + publishEvictEvent(new DashboardTitleEvictEvent(dashboardId)); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitleEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitleEvictEvent.java new file mode 100644 index 0000000000..1e4e985988 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitleEvictEvent.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed 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.thingsboard.server.dao.dashboard; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; + +@Data +public class DashboardTitleEvictEvent { + private final DashboardId key; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesCaffeineCache.java new file mode 100644 index 0000000000..fca9a552a3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed 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.thingsboard.server.dao.dashboard; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.DashboardId; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("DashboardTitlesCache") +public class DashboardTitlesCaffeineCache extends CaffeineTbTransactionalCache { + + public DashboardTitlesCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.DASHBOARD_TITLES_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesRedisCache.java new file mode 100644 index 0000000000..990a175b71 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardTitlesRedisCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed 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.thingsboard.server.dao.dashboard; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.DashboardId; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("DashboardTitlesCache") +public class DashboardTitlesRedisCache extends RedisTbTransactionalCache { + + public DashboardTitlesRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.DASHBOARD_TITLES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 0ede2325ac..735cffa5f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -72,4 +72,6 @@ public interface DashboardInfoRepository extends JpaRepository