Commit b67f454ba04110e338e23cd37e2a438d889d7c78
1 parent
0d7adb73
Handle Device service transactional methods exceptions (fix exception handling f…
…or devices with same name)
Showing
2 changed files
with
163 additions
and
97 deletions
@@ -30,7 +30,6 @@ import org.springframework.cache.annotation.Cacheable; | @@ -30,7 +30,6 @@ import org.springframework.cache.annotation.Cacheable; | ||
30 | import org.springframework.cache.annotation.Caching; | 30 | import org.springframework.cache.annotation.Caching; |
31 | import org.springframework.context.annotation.Lazy; | 31 | import org.springframework.context.annotation.Lazy; |
32 | import org.springframework.stereotype.Service; | 32 | import org.springframework.stereotype.Service; |
33 | -import org.springframework.transaction.annotation.Transactional; | ||
34 | import org.springframework.util.CollectionUtils; | 33 | import org.springframework.util.CollectionUtils; |
35 | import org.springframework.util.StringUtils; | 34 | import org.springframework.util.StringUtils; |
36 | import org.thingsboard.common.util.JacksonUtil; | 35 | import org.thingsboard.common.util.JacksonUtil; |
@@ -82,6 +81,7 @@ import org.thingsboard.server.dao.service.DataValidator; | @@ -82,6 +81,7 @@ import org.thingsboard.server.dao.service.DataValidator; | ||
82 | import org.thingsboard.server.dao.service.PaginatedRemover; | 81 | import org.thingsboard.server.dao.service.PaginatedRemover; |
83 | import org.thingsboard.server.dao.tenant.TbTenantProfileCache; | 82 | import org.thingsboard.server.dao.tenant.TbTenantProfileCache; |
84 | import org.thingsboard.server.dao.tenant.TenantDao; | 83 | import org.thingsboard.server.dao.tenant.TenantDao; |
84 | +import org.thingsboard.server.dao.tx.TransactionHandler; | ||
85 | 85 | ||
86 | import javax.annotation.Nullable; | 86 | import javax.annotation.Nullable; |
87 | import java.util.ArrayList; | 87 | import java.util.ArrayList; |
@@ -141,6 +141,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | @@ -141,6 +141,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | ||
141 | @Autowired | 141 | @Autowired |
142 | private OtaPackageService otaPackageService; | 142 | private OtaPackageService otaPackageService; |
143 | 143 | ||
144 | + @Autowired | ||
145 | + private TransactionHandler transactionHandler; | ||
146 | + | ||
144 | @Override | 147 | @Override |
145 | public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { | 148 | public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { |
146 | log.trace("Executing findDeviceInfoById [{}]", deviceId); | 149 | log.trace("Executing findDeviceInfoById [{}]", deviceId); |
@@ -184,10 +187,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | @@ -184,10 +187,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | ||
184 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), | 187 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), |
185 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") | 188 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") |
186 | }) | 189 | }) |
187 | - @Transactional | ||
188 | @Override | 190 | @Override |
189 | public Device saveDeviceWithAccessToken(Device device, String accessToken) { | 191 | public Device saveDeviceWithAccessToken(Device device, String accessToken) { |
190 | - return doSaveDevice(device, accessToken, true); | 192 | + try { |
193 | + return transactionHandler.runInTransaction(() -> doSaveDevice(device, accessToken, true)); | ||
194 | + } catch (Exception t) { | ||
195 | + throw handleDeviceSaveException(device, t); | ||
196 | + } | ||
191 | } | 197 | } |
192 | 198 | ||
193 | @Caching(evict= { | 199 | @Caching(evict= { |
@@ -212,26 +218,32 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | @@ -212,26 +218,32 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | ||
212 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), | 218 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}"), |
213 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") | 219 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.id}") |
214 | }) | 220 | }) |
215 | - @Transactional | ||
216 | @Override | 221 | @Override |
217 | - public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) { | ||
218 | - if (device.getId() == null) { | ||
219 | - Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName()); | ||
220 | - device = deviceWithName == null ? device : deviceWithName.updateDevice(device); | ||
221 | - } | ||
222 | - Device savedDevice = this.saveDeviceWithoutCredentials(device, true); | ||
223 | - deviceCredentials.setDeviceId(savedDevice.getId()); | ||
224 | - if (device.getId() == null) { | ||
225 | - deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); | ||
226 | - } else { | ||
227 | - DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), savedDevice.getId()); | ||
228 | - if (foundDeviceCredentials == null) { | ||
229 | - deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); | ||
230 | - } else { | ||
231 | - deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials); | ||
232 | - } | 222 | + public Device saveDeviceWithCredentials(Device toSave, DeviceCredentials deviceCredentials) { |
223 | + try { | ||
224 | + return transactionHandler.runInTransaction(() -> { | ||
225 | + Device device = toSave; | ||
226 | + if (device.getId() == null) { | ||
227 | + Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName()); | ||
228 | + device = deviceWithName == null ? device : deviceWithName.updateDevice(device); | ||
229 | + } | ||
230 | + Device savedDevice = this.saveDeviceWithoutCredentials(device, true); | ||
231 | + deviceCredentials.setDeviceId(savedDevice.getId()); | ||
232 | + if (device.getId() == null) { | ||
233 | + deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); | ||
234 | + } else { | ||
235 | + DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), savedDevice.getId()); | ||
236 | + if (foundDeviceCredentials == null) { | ||
237 | + deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); | ||
238 | + } else { | ||
239 | + deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials); | ||
240 | + } | ||
241 | + } | ||
242 | + return savedDevice; | ||
243 | + }); | ||
244 | + } catch (Exception t) { | ||
245 | + throw handleDeviceSaveException(toSave, t); | ||
233 | } | 246 | } |
234 | - return savedDevice; | ||
235 | } | 247 | } |
236 | 248 | ||
237 | private Device doSaveDevice(Device device, String accessToken, boolean doValidate) { | 249 | private Device doSaveDevice(Device device, String accessToken, boolean doValidate) { |
@@ -270,15 +282,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | @@ -270,15 +282,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | ||
270 | device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData())); | 282 | device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData())); |
271 | return deviceDao.save(device.getTenantId(), device); | 283 | return deviceDao.save(device.getTenantId(), device); |
272 | } catch (Exception t) { | 284 | } catch (Exception t) { |
273 | - ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); | ||
274 | - if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { | ||
275 | - // remove device from cache in case null value cached in the distributed redis. | ||
276 | - removeDeviceFromCacheByName(device.getTenantId(), device.getName()); | ||
277 | - removeDeviceFromCacheById(device.getTenantId(), device.getId()); | ||
278 | - throw new DataValidationException("Device with such name already exists!"); | ||
279 | - } else { | ||
280 | - throw t; | ||
281 | - } | 285 | + throw handleDeviceSaveException(device, t); |
282 | } | 286 | } |
283 | } | 287 | } |
284 | 288 | ||
@@ -364,16 +368,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | @@ -364,16 +368,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | ||
364 | } | 368 | } |
365 | 369 | ||
366 | private void removeDeviceFromCacheByName(TenantId tenantId, String name) { | 370 | private void removeDeviceFromCacheByName(TenantId tenantId, String name) { |
367 | - Cache cache = cacheManager.getCache(DEVICE_CACHE); | ||
368 | - cache.evict(Arrays.asList(tenantId, name)); | 371 | + if (tenantId != null && !StringUtils.isEmpty(name)) { |
372 | + Cache cache = cacheManager.getCache(DEVICE_CACHE); | ||
373 | + cache.evict(Arrays.asList(tenantId, name)); | ||
374 | + } | ||
369 | } | 375 | } |
370 | 376 | ||
371 | private void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) { | 377 | private void removeDeviceFromCacheById(TenantId tenantId, DeviceId deviceId) { |
372 | - if (deviceId == null) { | ||
373 | - return; | 378 | + if (tenantId != null && deviceId != null) { |
379 | + Cache cache = cacheManager.getCache(DEVICE_CACHE); | ||
380 | + cache.evict(Arrays.asList(tenantId, deviceId)); | ||
374 | } | 381 | } |
375 | - Cache cache = cacheManager.getCache(DEVICE_CACHE); | ||
376 | - cache.evict(Arrays.asList(tenantId, deviceId)); | ||
377 | } | 382 | } |
378 | 383 | ||
379 | @Override | 384 | @Override |
@@ -560,84 +565,93 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | @@ -560,84 +565,93 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | ||
560 | }, MoreExecutors.directExecutor()); | 565 | }, MoreExecutors.directExecutor()); |
561 | } | 566 | } |
562 | 567 | ||
563 | - @Transactional | ||
564 | @Override | 568 | @Override |
565 | public Device assignDeviceToTenant(TenantId tenantId, Device device) { | 569 | public Device assignDeviceToTenant(TenantId tenantId, Device device) { |
566 | log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device); | 570 | log.trace("Executing assignDeviceToTenant [{}][{}]", tenantId, device); |
567 | - | ||
568 | try { | 571 | try { |
569 | - List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get(); | ||
570 | - if (!CollectionUtils.isEmpty(entityViews)) { | ||
571 | - throw new DataValidationException("Can't assign device that has entity views to another tenant!"); | ||
572 | - } | ||
573 | - } catch (ExecutionException | InterruptedException e) { | ||
574 | - log.error("Exception while finding entity views for deviceId [{}]", device.getId(), e); | ||
575 | - throw new RuntimeException("Exception while finding entity views for deviceId [" + device.getId() + "]", e); | ||
576 | - } | 572 | + return transactionHandler.runInTransaction(() -> { |
573 | + try { | ||
574 | + List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), device.getId()).get(); | ||
575 | + if (!CollectionUtils.isEmpty(entityViews)) { | ||
576 | + throw new DataValidationException("Can't assign device that has entity views to another tenant!"); | ||
577 | + } | ||
578 | + } catch (ExecutionException | InterruptedException e) { | ||
579 | + log.error("Exception while finding entity views for deviceId [{}]", device.getId(), e); | ||
580 | + throw new RuntimeException("Exception while finding entity views for deviceId [" + device.getId() + "]", e); | ||
581 | + } | ||
577 | 582 | ||
578 | - eventService.removeEvents(device.getTenantId(), device.getId()); | 583 | + eventService.removeEvents(device.getTenantId(), device.getId()); |
579 | 584 | ||
580 | - relationService.removeRelations(device.getTenantId(), device.getId()); | 585 | + relationService.removeRelations(device.getTenantId(), device.getId()); |
581 | 586 | ||
582 | - TenantId oldTenantId = device.getTenantId(); | 587 | + TenantId oldTenantId = device.getTenantId(); |
583 | 588 | ||
584 | - device.setTenantId(tenantId); | ||
585 | - device.setCustomerId(null); | ||
586 | - Device savedDevice = doSaveDevice(device, null, true); | 589 | + device.setTenantId(tenantId); |
590 | + device.setCustomerId(null); | ||
591 | + Device savedDevice = doSaveDevice(device, null, true); | ||
587 | 592 | ||
588 | - // explicitly remove device with previous tenant id from cache | ||
589 | - // result device object will have different tenant id and will not remove entity from cache | ||
590 | - removeDeviceFromCacheByName(oldTenantId, device.getName()); | ||
591 | - removeDeviceFromCacheById(oldTenantId, device.getId()); | 593 | + // explicitly remove device with previous tenant id from cache |
594 | + // result device object will have different tenant id and will not remove entity from cache | ||
595 | + removeDeviceFromCacheByName(oldTenantId, device.getName()); | ||
596 | + removeDeviceFromCacheById(oldTenantId, device.getId()); | ||
592 | 597 | ||
593 | - return savedDevice; | 598 | + return savedDevice; |
599 | + }); | ||
600 | + } catch (Exception t) { | ||
601 | + throw handleDeviceSaveException(device, t); | ||
602 | + } | ||
594 | } | 603 | } |
595 | 604 | ||
596 | @Override | 605 | @Override |
597 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}") | 606 | @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#profile.tenantId, #provisionRequest.deviceName}") |
598 | - @Transactional | ||
599 | public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { | 607 | public Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { |
600 | Device device = new Device(); | 608 | Device device = new Device(); |
601 | - device.setName(provisionRequest.getDeviceName()); | ||
602 | - device.setType(profile.getName()); | ||
603 | - device.setTenantId(profile.getTenantId()); | ||
604 | - Device savedDevice = saveDevice(device); | ||
605 | - if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) || | ||
606 | - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) || | ||
607 | - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) || | ||
608 | - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) || | ||
609 | - !StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) { | ||
610 | - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); | ||
611 | - if (deviceCredentials == null) { | ||
612 | - deviceCredentials = new DeviceCredentials(); | ||
613 | - } | ||
614 | - deviceCredentials.setDeviceId(savedDevice.getId()); | ||
615 | - deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType()); | ||
616 | - switch (provisionRequest.getCredentialsType()) { | ||
617 | - case ACCESS_TOKEN: | ||
618 | - deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken()); | ||
619 | - break; | ||
620 | - case MQTT_BASIC: | ||
621 | - BasicMqttCredentials mqttCredentials = new BasicMqttCredentials(); | ||
622 | - mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId()); | ||
623 | - mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername()); | ||
624 | - mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword()); | ||
625 | - deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); | ||
626 | - break; | ||
627 | - case X509_CERTIFICATE: | ||
628 | - deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash()); | ||
629 | - break; | ||
630 | - case LWM2M_CREDENTIALS: | ||
631 | - break; | ||
632 | - } | ||
633 | - try { | ||
634 | - deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); | ||
635 | - } catch (Exception e) { | ||
636 | - throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); | ||
637 | - } | 609 | + try { |
610 | + return transactionHandler.runInTransaction(() -> { | ||
611 | + device.setName(provisionRequest.getDeviceName()); | ||
612 | + device.setType(profile.getName()); | ||
613 | + device.setTenantId(profile.getTenantId()); | ||
614 | + Device savedDevice = saveDevice(device); | ||
615 | + if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) || | ||
616 | + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) || | ||
617 | + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getUsername()) || | ||
618 | + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getPassword()) || | ||
619 | + !StringUtils.isEmpty(provisionRequest.getCredentialsData().getClientId())) { | ||
620 | + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getTenantId(), savedDevice.getId()); | ||
621 | + if (deviceCredentials == null) { | ||
622 | + deviceCredentials = new DeviceCredentials(); | ||
623 | + } | ||
624 | + deviceCredentials.setDeviceId(savedDevice.getId()); | ||
625 | + deviceCredentials.setCredentialsType(provisionRequest.getCredentialsType()); | ||
626 | + switch (provisionRequest.getCredentialsType()) { | ||
627 | + case ACCESS_TOKEN: | ||
628 | + deviceCredentials.setCredentialsId(provisionRequest.getCredentialsData().getToken()); | ||
629 | + break; | ||
630 | + case MQTT_BASIC: | ||
631 | + BasicMqttCredentials mqttCredentials = new BasicMqttCredentials(); | ||
632 | + mqttCredentials.setClientId(provisionRequest.getCredentialsData().getClientId()); | ||
633 | + mqttCredentials.setUserName(provisionRequest.getCredentialsData().getUsername()); | ||
634 | + mqttCredentials.setPassword(provisionRequest.getCredentialsData().getPassword()); | ||
635 | + deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); | ||
636 | + break; | ||
637 | + case X509_CERTIFICATE: | ||
638 | + deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash()); | ||
639 | + break; | ||
640 | + case LWM2M_CREDENTIALS: | ||
641 | + break; | ||
642 | + } | ||
643 | + try { | ||
644 | + deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); | ||
645 | + } catch (Exception e) { | ||
646 | + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); | ||
647 | + } | ||
648 | + } | ||
649 | + removeDeviceFromCacheById(savedDevice.getTenantId(), savedDevice.getId()); // eviction by name is described as annotation @CacheEvict above | ||
650 | + return savedDevice; | ||
651 | + }); | ||
652 | + } catch (Exception t) { | ||
653 | + throw handleDeviceSaveException(device, t); | ||
638 | } | 654 | } |
639 | - removeDeviceFromCacheById(savedDevice.getTenantId(), savedDevice.getId()); // eviction by name is described as annotation @CacheEvict above | ||
640 | - return savedDevice; | ||
641 | } | 655 | } |
642 | 656 | ||
643 | @Override | 657 | @Override |
@@ -818,4 +832,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | @@ -818,4 +832,20 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe | ||
818 | unassignDeviceFromCustomer(tenantId, new DeviceId(entity.getUuidId())); | 832 | unassignDeviceFromCustomer(tenantId, new DeviceId(entity.getUuidId())); |
819 | } | 833 | } |
820 | }; | 834 | }; |
835 | + | ||
836 | + private RuntimeException handleDeviceSaveException(Device device, Exception t) { | ||
837 | + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); | ||
838 | + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { | ||
839 | + // remove device from cache in case null value cached in the distributed redis. | ||
840 | + if (device != null) { | ||
841 | + removeDeviceFromCacheByName(device.getTenantId(), device.getName()); | ||
842 | + removeDeviceFromCacheById(device.getTenantId(), device.getId()); | ||
843 | + } | ||
844 | + return new DataValidationException("Device with such name already exists!"); | ||
845 | + } else if (t instanceof RuntimeException) { | ||
846 | + return (RuntimeException)t; | ||
847 | + } else { | ||
848 | + return new RuntimeException("Failed to save device!", t); | ||
849 | + } | ||
850 | + } | ||
821 | } | 851 | } |
1 | +/** | ||
2 | + * Copyright © 2016-2021 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.dao.tx; | ||
17 | + | ||
18 | +import org.springframework.stereotype.Service; | ||
19 | +import org.springframework.transaction.annotation.Propagation; | ||
20 | +import org.springframework.transaction.annotation.Transactional; | ||
21 | + | ||
22 | +import java.util.function.Supplier; | ||
23 | + | ||
24 | +@Service | ||
25 | +public class TransactionHandler { | ||
26 | + | ||
27 | + @Transactional(propagation = Propagation.REQUIRED) | ||
28 | + public <T> T runInTransaction(Supplier<T> supplier) { | ||
29 | + return supplier.get(); | ||
30 | + } | ||
31 | + | ||
32 | + @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
33 | + public <T> T runInNewTransaction(Supplier<T> supplier) { | ||
34 | + return supplier.get(); | ||
35 | + } | ||
36 | +} |