Pull request #416: RED-3942

Merge in RED/persistence-service from RED-3942 to master

* commit 'e136be5bd4b84c1fd6dcacd5c149dadf3b6a845c':
  RED-3942 - Improve clone performance
  RED-3942 - Improve clone performance
  RED-3942 - Improve clone performance
This commit is contained in:
Timo Bejan 2022-05-23 19:42:47 +02:00
commit e96f77fa95
21 changed files with 390 additions and 176 deletions

View File

@ -100,6 +100,5 @@
<artifactId>guava</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -4,13 +4,10 @@ public interface BaseDictionaryEntry {
String getValue();
long getVersion();
boolean isDeleted();
TypeEntity getType();
String getTypeId();
}

View File

@ -17,7 +17,7 @@ import javax.persistence.*;
public class DictionaryEntryEntity implements BaseDictionaryEntry {
@Id
@GeneratedValue
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long entryId;
@Column(length = 4000)
private String value;
@ -25,9 +25,8 @@ public class DictionaryEntryEntity implements BaseDictionaryEntry {
private long version;
@Column
private boolean deleted;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
private TypeEntity type;
@Column(name = "type_id")
private String typeId;
}

View File

@ -1,12 +1,6 @@
package com.iqser.red.service.persistence.management.v1.processor.entity.configuration;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -24,7 +18,7 @@ import lombok.NoArgsConstructor;
public class DictionaryFalsePositiveEntryEntity implements BaseDictionaryEntry {
@Id
@GeneratedValue
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long entryId;
@Column(length = 4000)
private String value;
@ -32,10 +26,8 @@ public class DictionaryFalsePositiveEntryEntity implements BaseDictionaryEntry {
private long version;
@Column
private boolean deleted;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
private TypeEntity type;
@Column( name = "type_id")
private String typeId;
}

View File

@ -1,20 +1,12 @@
package com.iqser.red.service.persistence.management.v1.processor.entity.configuration;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@Builder
@Entity
@ -24,7 +16,7 @@ import lombok.NoArgsConstructor;
public class DictionaryFalseRecommendationEntryEntity implements BaseDictionaryEntry {
@Id
@GeneratedValue
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long entryId;
@Column(length = 4000)
private String value;
@ -32,10 +24,8 @@ public class DictionaryFalseRecommendationEntryEntity implements BaseDictionaryE
private long version;
@Column
private boolean deleted;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
private TypeEntity type;
@Column(name = "type_id")
private String typeId;
}

View File

@ -1,45 +1,35 @@
package com.iqser.red.service.persistence.management.v1.processor.service;
import static com.iqser.red.service.persistence.management.v1.processor.utils.MagicConverter.convert;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.*;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.ColorsEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.FileAttributesGeneralConfigurationEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.TypeEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.WatermarkEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierAttributeConfigEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierTemplateEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileAttributeConfigEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.ReportTemplateEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.ConflictException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DictionaryPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierAttributeConfigPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierStatusPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierTemplatePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.EntryPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.FileAttributeConfigPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.LegalBasisMappingPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.ReportTemplatePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.RulesPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.*;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.DossierTemplateRepository;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.CloneDossierTemplateRequest;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.DossierTemplateStatus;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.dossier.CreateOrUpdateDossierStatusRequest;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.legalbasis.LegalBasis;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.type.DictionaryEntryType;
import com.iqser.red.storage.commons.service.StorageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static com.iqser.red.service.persistence.management.v1.processor.utils.MagicConverter.convert;
@Slf4j
@Service
@ -81,7 +71,6 @@ public class DossierTemplateCloneService {
clonedDossierTemplate.setValidTo(cloneDossierTemplateRequest.getValidTo() == null ? dossierTemplate.getValidTo() : cloneDossierTemplateRequest.getValidTo());
clonedDossierTemplate.setSoftDeleteTime(dossierTemplate.getSoftDeleteTime());
clonedDossierTemplate.setDownloadFileTypes(cloneDossierTemplateRequest.getDownloadFileTypes() == null || cloneDossierTemplateRequest.getDownloadFileTypes().isEmpty() ? dossierTemplate.getDownloadFileTypes() : cloneDossierTemplateRequest.getDownloadFileTypes());
//set rules
cloneRules(dossierTemplate.getId(), clonedDossierTemplate.getId());
//set legal basis
@ -92,18 +81,17 @@ public class DossierTemplateCloneService {
dossierTemplateRepository.save(clonedDossierTemplate);
//set dictionaries
clonedDossierTemplate.setDossierTypes(cloneDictionariesWithEntries(dossierTemplate.getId(), clonedDossierTemplate.getId()));
cloneDictionariesWithEntries(dossierTemplate.getId(), clonedDossierTemplate.getId());
//set dossier attributes
cloneDossierAttributesConfig(dossierTemplate.getId(), clonedDossierTemplate.getId());
//set file attributes
cloneFileAttributesGeneralConfig(dossierTemplate.getId(), clonedDossierTemplate.getId());
clonedDossierTemplate.setFileAttributeConfigs(cloneFileAttributesConfig(dossierTemplate.getId(), clonedDossierTemplate.getId()));
cloneFileAttributesConfig(dossierTemplate.getId(), clonedDossierTemplate.getId());
//set report templates
clonedDossierTemplate.setReportTemplates(cloneReportTemplates(dossierTemplate, clonedDossierTemplate.getId()));
cloneReportTemplates(dossierTemplate, clonedDossierTemplate.getId());
//set watermarks
cloneWatermark(dossierTemplate.getId(), clonedDossierTemplate.getId());
@ -114,11 +102,10 @@ public class DossierTemplateCloneService {
//set dossier status
cloneDossierStates(dossierTemplate.getId(), clonedDossierTemplate.getId());
dossierTemplateRepository.save(clonedDossierTemplate);
}, () -> {
throw new NotFoundException(String.format(dossierTemplatePersistenceService.DOSSIER_TEMPLATE_NOT_FOUND_MESSAGE, dossierTemplateId));
});
return clonedDossierTemplate;
}
@ -135,21 +122,12 @@ public class DossierTemplateCloneService {
}
private List<TypeEntity> cloneDictionariesWithEntries(String dossierTemplateId, String clonedDossierTemplateId) {
List<TypeEntity> clonedTypes = new ArrayList<>();
var types = dictionaryPersistenceService.getAllTypesForDossierTemplate(dossierTemplateId,false);
private void cloneDictionariesWithEntries(String dossierTemplateId, String clonedDossierTemplateId) {
var types = dictionaryPersistenceService.getAllTypesForDossierTemplate(dossierTemplateId, false);
for (TypeEntity t : types) {
TypeEntity te = dictionaryPersistenceService.addType(t.getType(), clonedDossierTemplateId, t.getHexColor(), t.getRecommendationHexColor(), t.getRank(), t.isHint(), t.isCaseInsensitive(), t.isRecommendation(), t.getDescription(), t.isAddToDictionaryAction(), t.getLabel(), null, t.isHasDictionary(), t.isSystemManaged(), t.isAutoHideSkipped());
te.setDossierTemplateId(clonedDossierTemplateId);
clonedTypes.add(te);
for (DictionaryEntryType det : DictionaryEntryType.values()) {
var baseDictionaryEntries = entryPersistenceService.getEntries(t.getId(), det, null);
Set<String> entries = baseDictionaryEntries.stream().map(BaseDictionaryEntry::getValue).collect(Collectors.toSet());
entryPersistenceService.addEntries(te.getId(), entries, te.getVersion(), det);
}
var type = dictionaryPersistenceService.addType(t.getType(), clonedDossierTemplateId, t.getHexColor(), t.getRecommendationHexColor(), t.getRank(), t.isHint(), t.isCaseInsensitive(), t.isRecommendation(), t.getDescription(), t.isAddToDictionaryAction(), t.getLabel(), null, t.isHasDictionary(), t.isSystemManaged(), t.isAutoHideSkipped());
entryPersistenceService.cloneEntries(t.getId(), type.getId());
}
return clonedTypes;
}
@ -172,17 +150,21 @@ public class DossierTemplateCloneService {
private void cloneFileAttributesGeneralConfig(String dossierTemplateId, String clonedDossierTemplateId) {
var fileAttributesGeneralConfig = fileAttributeConfigPersistenceService.getFileAttributesGeneralConfiguration(dossierTemplateId);
var fagc = FileAttributesGeneralConfigurationEntity.builder()
.dossierTemplateId(clonedDossierTemplateId)
.delimiter(fileAttributesGeneralConfig.getDelimiter())
.filenameMappingColumnHeaderName(fileAttributesGeneralConfig.getFilenameMappingColumnHeaderName())
.build();
fileAttributeConfigPersistenceService.setFileAttributesGeneralConfig(clonedDossierTemplateId, fagc);
try {
var fileAttributesGeneralConfig = fileAttributeConfigPersistenceService.getFileAttributesGeneralConfiguration(dossierTemplateId);
var fagc = FileAttributesGeneralConfigurationEntity.builder()
.dossierTemplateId(clonedDossierTemplateId)
.delimiter(fileAttributesGeneralConfig.getDelimiter())
.filenameMappingColumnHeaderName(fileAttributesGeneralConfig.getFilenameMappingColumnHeaderName())
.build();
fileAttributeConfigPersistenceService.setFileAttributesGeneralConfig(clonedDossierTemplateId, fagc);
} catch (NotFoundException e) {
log.debug("No file attribute config found for cloning {}", dossierTemplateId);
}
}
private List<FileAttributeConfigEntity> cloneFileAttributesConfig(String dossierTemplateId, String clonedDossierTemplateId) {
private void cloneFileAttributesConfig(String dossierTemplateId, String clonedDossierTemplateId) {
List<FileAttributeConfigEntity> facList = new ArrayList<>();
var fileAttributesConfig = fileAttributeConfigPersistenceService.getFileAttributes(dossierTemplateId);
@ -200,42 +182,47 @@ public class DossierTemplateCloneService {
facList.add(fac);
});
fileAttributeConfigPersistenceService.setFileAttributesConfig(clonedDossierTemplateId, facList);
return fileAttributeConfigPersistenceService.getFileAttributes(clonedDossierTemplateId);
}
private List<ReportTemplateEntity> cloneReportTemplates(DossierTemplateEntity dossierTemplate, String clonedDossierTemplateId) {
private void cloneReportTemplates(DossierTemplateEntity dossierTemplate, String clonedDossierTemplateId) {
var reportTemplates = dossierTemplate.getReportTemplates();
var reportTemplates = reportTemplatePersistenceService.findByDossierTemplateId(dossierTemplate.getId());
for (ReportTemplateEntity rte : reportTemplates) {
var storedReportTemplate = storageService.getObject(rte.getStorageId());
String storageId = clonedDossierTemplateId + "/" + rte.getFileName();
String templateId = UUID.randomUUID().toString();
try {
storageService.storeObject(storageId, storedReportTemplate.getInputStream());
} catch (IOException e) {
throw new ConflictException("Could not clone report templates of dossier template.");
if (storageService.objectExists(rte.getStorageId())) {
var storedReportTemplate = storageService.getObject(rte.getStorageId());
String storageId = clonedDossierTemplateId + "/" + rte.getFileName();
String templateId = UUID.randomUUID().toString();
try {
storageService.storeObject(storageId, storedReportTemplate.getInputStream());
} catch (IOException e) {
throw new ConflictException("Could not clone report templates of dossier template.");
}
reportTemplatePersistenceService.insert(clonedDossierTemplateId, templateId, storageId, rte.getFileName(), rte.isActiveByDefault(), rte.isMultiFileReport());
} else {
log.debug("Deleted Report-template {} will not be cloned", rte.getTemplateId());
}
reportTemplatePersistenceService.insert(clonedDossierTemplateId, templateId, storageId, rte.getFileName(), rte.isActiveByDefault(), rte.isMultiFileReport());
}
return reportTemplatePersistenceService.findByDossierTemplateId(clonedDossierTemplateId);
}
private void cloneWatermark(String dossierTemplateId, String clonedDossierTemplateId) {
var watermark = watermarkService.getWatermark(dossierTemplateId);
WatermarkEntity we = WatermarkEntity.builder()
.dossierTemplateId(clonedDossierTemplateId)
.text(watermark.getText())
.hexColor(watermark.getHexColor())
.opacity(watermark.getOpacity())
.fontType(watermark.getFontType())
.fontSize(watermark.getFontSize())
.orientation(watermark.getOrientation())
.build();
watermarkService.saveWatermark(clonedDossierTemplateId, we);
try {
var watermark = watermarkService.getWatermark(dossierTemplateId);
WatermarkEntity we = WatermarkEntity.builder()
.dossierTemplateId(clonedDossierTemplateId)
.text(watermark.getText())
.hexColor(watermark.getHexColor())
.opacity(watermark.getOpacity())
.fontType(watermark.getFontType())
.fontSize(watermark.getFontSize())
.orientation(watermark.getOrientation())
.build();
watermarkService.saveWatermark(clonedDossierTemplateId, we);
} catch (NotFoundException e) {
log.debug("No watermark config found for cloning {}", dossierTemplateId);
}
}

View File

@ -8,6 +8,7 @@ import com.iqser.red.service.persistence.management.v1.processor.service.persist
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.FalsePositiveEntryRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.FalseRecommendationEntryRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.TypeRepository;
import com.iqser.red.service.persistence.management.v1.processor.utils.jdbc.JDBCWriteUtils;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.type.DictionaryEntryType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -27,6 +28,7 @@ public class EntryPersistenceService {
private final TypeRepository typeRepository;
private final FalsePositiveEntryRepository falsePositiveEntryRepository;
private final FalseRecommendationEntryRepository falseRecommendationEntryRepository;
private final JDBCWriteUtils jdbcWriteUtils;
@Transactional
public void deleteEntries(String typeId, List<String> values, long version,
@ -102,12 +104,12 @@ public class EntryPersistenceService {
DictionaryEntryEntity entry = new DictionaryEntryEntity();
entry.setVersion(version);
entry.setValue(e);
entry.setType(type);
entry.setTypeId(type.getId());
return entry;
}).collect(Collectors.toList());
entryRepository.saveAll(entryEntities);
jdbcWriteUtils.saveBatch(entryEntities);
break;
}
case FALSE_POSITIVE: {
@ -118,12 +120,11 @@ public class EntryPersistenceService {
DictionaryFalsePositiveEntryEntity entry = new DictionaryFalsePositiveEntryEntity();
entry.setVersion(version);
entry.setValue(e);
entry.setType(type);
entry.setTypeId(type.getId());
return entry;
}).collect(Collectors.toList());
falsePositiveEntryRepository.saveAll(entryEntities);
jdbcWriteUtils.saveBatch(entryEntities);
break;
}
case FALSE_RECOMMENDATION: {
@ -134,14 +135,19 @@ public class EntryPersistenceService {
DictionaryFalseRecommendationEntryEntity entry = new DictionaryFalseRecommendationEntryEntity();
entry.setVersion(version);
entry.setValue(e);
entry.setType(type);
entry.setTypeId(type.getId());
return entry;
}).collect(Collectors.toList());
falseRecommendationEntryRepository.saveAll(entryEntities);
jdbcWriteUtils.saveBatch(entryEntities);
break;
}
}
}
public void cloneEntries(String originalTypeId, String newTypeId){
entryRepository.cloneEntries(originalTypeId, newTypeId);
falseRecommendationEntryRepository.cloneEntries(originalTypeId, newTypeId);
falsePositiveEntryRepository.cloneEntries(originalTypeId, newTypeId);
}
}

View File

@ -11,9 +11,4 @@ public interface DossierAttributeConfigRepository extends JpaRepository<DossierA
List<DossierAttributeConfigEntity> findAllByDossierTemplateId(String dossierTemplateId);
void deleteByDossierTemplateId(String dossierTemplateId);
@Modifying
@Query("delete from DossierAttributeConfigEntity d where d.id in :dossierAttributeIds")
void deleteByDossierAtributeIds(List<String> dossierAttributeIds);
}

View File

@ -12,24 +12,24 @@ import java.util.Set;
public interface EntryRepository extends JpaRepository<DictionaryEntryEntity, Long> {
@Modifying
@Query("update DictionaryEntryEntity e set e.deleted = true, e.version = :version where e.type.id =:typeId and e.value in :values")
@Query("update DictionaryEntryEntity e set e.deleted = true, e.version = :version where e.typeId = :typeId and e.value in :values")
void deleteAllByTypeIdAndVersionAndValueIn(String typeId, long version, List<String> values);
@Modifying
@Query("update DictionaryEntryEntity e set e.version = :version where e.type.id =:typeId and e.deleted = false")
@Query("update DictionaryEntryEntity e set e.version = :version where e.typeId = :typeId and e.deleted = false")
void updateVersionWhereTypeId(long version, String typeId);
@Modifying
@Query("update DictionaryEntryEntity e set e.type.id = :newTypeId, e.version = e.version + 1 where e.type.id =:typeId and e.deleted = false")
@Query("update DictionaryEntryEntity e set e.typeId = :newTypeId, e.version = e.version + 1 where e.typeId =:typeId and e.deleted = false")
void updateTypeIdAndIncrementVersionByOne(String typeId, String newTypeId);
List<DictionaryEntryEntity> findByTypeIdAndVersionGreaterThan(String typeId, long version);
@Modifying
@Transactional
@Query("update DictionaryEntryEntity e set e.deleted = true, e.version = :version where e.type.id = :typeId")
@Query("update DictionaryEntryEntity e set e.deleted = true, e.version = :version where e.typeId = :typeId")
void deleteAllEntriesForTypeId(String typeId, long version);
@ -37,4 +37,11 @@ public interface EntryRepository extends JpaRepository<DictionaryEntryEntity, Lo
@Transactional
@Query(value = "update dictionary_entry set deleted = false, version = :version where type_id = :typeId and value in (:entries) returning value", nativeQuery = true)
List<String> undeleteEntries(String typeId, Set<String> entries, long version);
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Transactional
@Query(value = "insert into dictionary_entry (value, version, deleted, type_id) " +
" select value, 1, false, :newTypeId from dictionary_entry where type_id = :originalTypeId and deleted = false", nativeQuery = true)
void cloneEntries(String originalTypeId, String newTypeId);
}

View File

@ -12,12 +12,12 @@ import java.util.Set;
public interface FalsePositiveEntryRepository extends JpaRepository<DictionaryFalsePositiveEntryEntity, Long> {
@Modifying
@Query("update DictionaryFalsePositiveEntryEntity e set e.deleted = true , e.version = :version where e.type.id =:typeId and e.value in :values")
@Query("update DictionaryFalsePositiveEntryEntity e set e.deleted = true , e.version = :version where e.typeId = :typeId and e.value in :values")
void deleteAllByTypeIdAndVersionAndValueIn(String typeId, long version, List<String> values);
@Modifying
@Query("update DictionaryFalsePositiveEntryEntity e set e.version = :version where e.type.id =:typeId and e.deleted = false")
@Query("update DictionaryFalsePositiveEntryEntity e set e.version = :version where e.typeId = :typeId and e.deleted = false")
void updateVersionWhereTypeId(long version, String typeId);
@ -25,11 +25,17 @@ public interface FalsePositiveEntryRepository extends JpaRepository<DictionaryFa
@Modifying
@Transactional
@Query("update DictionaryFalsePositiveEntryEntity e set e.deleted = true, e.version = :version where e.type.id = :typeId")
@Query("update DictionaryFalsePositiveEntryEntity e set e.deleted = true, e.version = :version where e.typeId = :typeId")
void deleteAllEntriesForTypeId(String typeId, long version);
@Modifying
@Transactional
@Query(value = "update dictionary_false_positive_entry set deleted = false, version = :version where type_id = :typeId and value in (:entries) returning value", nativeQuery = true)
List<String> undeleteEntries(String typeId, Set<String> entries, long version);
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Transactional
@Query(value = "insert into dictionary_false_positive_entry (value, version, deleted, type_id) " +
" select value, 1, false, :newTypeId from dictionary_false_positive_entry where type_id = :originalTypeId and deleted = false", nativeQuery = true)
void cloneEntries(String originalTypeId, String newTypeId);
}

View File

@ -1,26 +1,23 @@
package com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository;
import java.util.List;
import java.util.Set;
import javax.transaction.Transactional;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.DictionaryFalseRecommendationEntryEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.DictionaryFalsePositiveEntryEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.DictionaryFalseRecommendationEntryEntity;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Set;
public interface FalseRecommendationEntryRepository extends JpaRepository<DictionaryFalseRecommendationEntryEntity, Long> {
@Modifying
@Query("update DictionaryFalseRecommendationEntryEntity e set e.deleted = true , e.version = :version where e.type.id =:typeId and e.value in :values")
@Query("update DictionaryFalseRecommendationEntryEntity e set e.deleted = true , e.version = :version where e.typeId = :typeId and e.value in :values")
void deleteAllByTypeIdAndVersionAndValueIn(String typeId, long version, List<String> values);
@Modifying
@Query("update DictionaryFalseRecommendationEntryEntity e set e.version = :version where e.type.id =:typeId and e.deleted = false")
@Query("update DictionaryFalseRecommendationEntryEntity e set e.version = :version where e.typeId = :typeId and e.deleted = false")
void updateVersionWhereTypeId(long version, String typeId);
@ -28,11 +25,17 @@ public interface FalseRecommendationEntryRepository extends JpaRepository<Dictio
@Modifying
@Transactional
@Query("update DictionaryFalseRecommendationEntryEntity e set e.deleted = true, e.version = :version where e.type.id = :typeId")
@Query("update DictionaryFalseRecommendationEntryEntity e set e.deleted = true, e.version = :version where e.typeId = :typeId")
void deleteAllEntriesForTypeId(String typeId, long version);
@Modifying
@Transactional
@Query(value = "update dictionary_false_recommendation_entry set deleted = false, version = :version where type_id = :typeId and value in (:entries) returning value", nativeQuery = true)
List<String> undeleteEntries(String typeId, Set<String> entries, long version);
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Transactional
@Query(value = "insert into dictionary_false_recommendation_entry (value, version, deleted, type_id) " +
" select value, 1, false, :newTypeId from dictionary_false_recommendation_entry where type_id = :originalTypeId and deleted = false", nativeQuery = true)
void cloneEntries(String originalTypeId, String newTypeId);
}

View File

@ -39,7 +39,7 @@ public interface TypeRepository extends JpaRepository<TypeEntity, String> {
@Query("select new com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.type.DictionarySummaryResponse(dt.id, t.id, t.type, t.label, count(e)) " +
"from DossierTemplateEntity dt " +
"left join TypeEntity t on t.dossierTemplateId = dt.id " +
"left join DictionaryEntryEntity e on e.type.id = t.id " +
"left join DictionaryEntryEntity e on e.typeId = t.id " +
"where dt.id in :dossierTemplateIds and t.dossierId is null and (e.id is null or e.deleted = false) " +
"group by dt.id, t.id, t.type, t.label ")
List<DictionarySummaryResponse> findDictionarySummaryList(Set<String> dossierTemplateIds);

View File

@ -0,0 +1,135 @@
package com.iqser.red.service.persistence.management.v1.processor.utils.jdbc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.Table;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.capitalize;
@Service
@RequiredArgsConstructor
public class JDBCWriteUtils {
private final JdbcTemplate jdbcTemplate;
private final String SQL_TEMPLATE = "INSERT INTO %s (%s) values (%s)";
private final Map<Class<?>, EntityMetadata> entityMetadataMap = new HashMap<>();
private final EntityManager entityManager;
public <T> void saveBatch(final List<T> entities) {
if (entities.isEmpty()) {
return;
}
var metadata = getEntityMetadata(entities.iterator().next());
final int batchSize = 500;
for (int j = 0; j < entities.size(); j += batchSize) {
final List<T> batchList = entities.subList(j, Math.min(j + batchSize, entities.size()));
jdbcTemplate.batchUpdate(metadata.getSqlStatement(),
new BatchPreparedStatementSetter() {
@SneakyThrows
@Override
public void setValues(PreparedStatement ps, int i) {
T entity = batchList.get(i);
int paramIndex = 1;
for (var mapping : metadata.getFieldMethodMap().entrySet()) {
ps.setObject(paramIndex++, mapping.getValue().invoke(entity));
}
}
@Override
public int getBatchSize() {
return batchList.size();
}
});
}
entityManager.clear();
}
private <T> EntityMetadata getEntityMetadata(T entity) {
var existingMetadata = entityMetadataMap.get(entity.getClass());
if (existingMetadata != null) {
return existingMetadata;
}
var tableName = getTableName(entity);
var args = getArgs(entity);
var sql = String.format(SQL_TEMPLATE, tableName, String.join(", ", args.keySet()), args.keySet().stream().map(a -> "?").collect(Collectors.joining(", ")));
var metadata = new EntityMetadata(tableName, sql, args);
entityMetadataMap.put(entity.getClass(), metadata);
return metadata;
}
@SneakyThrows
private <T> Map<String, Method> getArgs(T entity) {
var fields = entity.getClass().getDeclaredFields();
Map<String, Method> entityMethodMap = new LinkedHashMap<>();
for (var field : fields) {
var annotations = field.getDeclaredAnnotations();
for (var annotation : annotations) {
if (annotation.annotationType().equals(Column.class)) {
var columnAnnotation = (Column) annotation;
var name = StringUtils.isEmpty(columnAnnotation.name()) ? toSnakeCase(field.getName()) : columnAnnotation.name();
entityMethodMap.put(name, entity.getClass().getMethod(getMethodName(field)));
}
}
}
return entityMethodMap;
}
private String getMethodName(Field field) {
var prefix = "get";
if (field.getType().equals(Boolean.class) || field.getType().equals(boolean.class)) {
prefix = "is";
}
return prefix + capitalize(field.getName());
}
private String toSnakeCase(String name) {
String ret = name.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2");
return ret.toLowerCase();
}
private <T> String getTableName(T entity) {
var tableAnnot = entity.getClass().getDeclaredAnnotation(Table.class);
return tableAnnot.name();
}
@Data
@AllArgsConstructor
public static class EntityMetadata {
private String tableName;
private String sqlStatement;
private Map<String, Method> fieldMethodMap;
}
}

View File

@ -13,7 +13,7 @@ spring:
main:
allow-circular-references: true # FIXME
datasource:
url: jdbc:postgresql://${PSQL_HOST:localhost}:${PSQL_PORT:5432}/${PSQL_DATABASE:redaction}
url: jdbc:postgresql://${PSQL_HOST:localhost}:${PSQL_PORT:5432}/${PSQL_DATABASE:redaction}?cachePrepStmts=true&useServerPrepStmts=true&rewriteBatchedStatements=true
driverClassName: org.postgresql.Driver
username: ${PSQL_USERNAME:redaction}
password: ${PSQL_PASSWORD:redaction}

View File

@ -71,3 +71,5 @@ databaseChangeLog:
file: db/changelog/28-add-update-dictionary-to-manual-resize-redactions.yaml
- include:
file: db/changelog/29-add-remove-digital-signatures-on-upload.changelog.yaml
- include:
file: db/changelog/sql/30-change-bigint-to-serial.sql

View File

@ -0,0 +1,7 @@
ALTER TABLE dictionary_entry ALTER entry_id ADD GENERATED ALWAYS AS IDENTITY;
ALTER TABLE dictionary_false_recommendation_entry ALTER entry_id ADD GENERATED ALWAYS AS IDENTITY;
ALTER TABLE dictionary_false_positive_entry ALTER entry_id ADD GENERATED ALWAYS AS IDENTITY;
SELECT setval(pg_get_serial_sequence('dictionary_entry', 'entry_id'), (select coalesce(max(entry_id)+1,1) from dictionary_entry));
SELECT setval(pg_get_serial_sequence('dictionary_false_recommendation_entry', 'entry_id'), (select coalesce(max(entry_id)+1,1) from dictionary_false_recommendation_entry));
SELECT setval(pg_get_serial_sequence('dictionary_false_positive_entry', 'entry_id'), (select coalesce(max(entry_id)+1,1) from dictionary_false_positive_entry));

View File

@ -0,0 +1,91 @@
package com.iqser.red.service.peristence.v1.server.integration.tests.performance;
import com.iqser.red.service.peristence.v1.server.integration.client.DictionaryClient;
import com.iqser.red.service.peristence.v1.server.integration.client.DossierTemplateClient;
import com.iqser.red.service.peristence.v1.server.integration.service.DossierTemplateTesterAndProvider;
import com.iqser.red.service.peristence.v1.server.integration.service.TypeProvider;
import com.iqser.red.service.peristence.v1.server.integration.utils.AbstractPersistenceServerServiceTest;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.EntryRepository;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.CloneDossierTemplateRequest;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.type.DictionaryEntryType;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@Slf4j
public class EntityPerformanceTest extends AbstractPersistenceServerServiceTest {
@Autowired
private DictionaryClient dictionaryClient;
@Autowired
private DossierTemplateClient dossierTemplateClient;
@Autowired
private DossierTemplateTesterAndProvider dossierTemplateTesterAndProvider;
@Autowired
private TypeProvider typeProvider;
@Autowired
private EntryRepository entryRepository;
@Test
public void testAddToDictionary() {
var template = dossierTemplateTesterAndProvider.provideTestTemplate("test");
var type = typeProvider.testAndProvideType(template);
var fiveKEntries = generateEntries(5000);
var tenKEntries = generateEntries(10000);
long t1 = System.currentTimeMillis();
dictionaryClient.addEntries(type.getTypeId(), fiveKEntries, true, false, DictionaryEntryType.ENTRY);
long t2 = System.currentTimeMillis();
log.info("Add Time: {}ms counting: {} entries", (t2 - t1), entryRepository.findByTypeIdAndVersionGreaterThan(type.getTypeId(), 0).size());
t1 = System.currentTimeMillis();
dictionaryClient.addEntries(type.getTypeId(), tenKEntries, true, false, DictionaryEntryType.ENTRY);
t2 = System.currentTimeMillis();
log.info("Add Time: {}ms counting: {} entries", (t2 - t1), entryRepository.findByTypeIdAndVersionGreaterThan(type.getTypeId(), 0).size());
t1 = System.currentTimeMillis();
dictionaryClient.addEntries(type.getTypeId(), fiveKEntries, true, false, DictionaryEntryType.ENTRY);
t2 = System.currentTimeMillis();
log.info("Update Time: {}ms counting: {} entries", (t2 - t1), entryRepository.findByTypeIdAndVersionGreaterThan(type.getTypeId(), 0).size());
dictionaryClient.addEntries(type.getTypeId(), fiveKEntries, true, false, DictionaryEntryType.FALSE_RECOMMENDATION);
dictionaryClient.addEntries(type.getTypeId(), fiveKEntries, true, false, DictionaryEntryType.FALSE_POSITIVE);
t1 = System.currentTimeMillis();
var cloned = dossierTemplateClient.cloneDossierTemplate(template.getId(), new CloneDossierTemplateRequest());
t2 = System.currentTimeMillis();
log.info("Clone Time: {}", (t2 - t1));
var types = dictionaryClient.getAllTypesForDossierTemplate(cloned.getId(), false);
assertThat(types.size()).isEqualTo(1);
for (var clonedType : types) {
var found = entryRepository.findByTypeIdAndVersionGreaterThan(clonedType.getTypeId(), 0);
assertThat(found.size()).isEqualTo(5000);
}
}
private List<String> generateEntries(int count) {
List<String> entries = new ArrayList<>();
for (var i = 1; i <= count; i++) {
entries.add("Dictionary Entry: " + i);
}
return entries;
}
}

View File

@ -100,7 +100,7 @@ public class FilePerformanceTest extends AbstractPersistenceServerServiceTest {
List<DictionaryEntryEntity> des = new ArrayList<>();
for (int j = 0; j < TYPE_ENTRY_COUNT; j++) {
DictionaryEntryEntity de = new DictionaryEntryEntity();
de.setType(te);
de.setTypeId(te.getId());
de.setDeleted(false);
de.setVersion(1);
de.setValue("Dictionary Entry" +i+"/"+ j);

View File

@ -1,15 +1,23 @@
package com.iqser.red.service.peristence.v1.server.integration.utils;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import com.iqser.red.service.pdftron.redaction.v1.api.model.ByteContentDocument;
import com.iqser.red.service.pdftron.redaction.v1.api.model.DocumentRequest;
import com.iqser.red.service.pdftron.redaction.v1.api.model.UntouchedDocumentResponse;
import com.iqser.red.service.peristence.v1.server.Application;
import com.iqser.red.service.peristence.v1.server.client.RedactionClient;
import com.iqser.red.service.peristence.v1.server.client.SearchClient;
import com.iqser.red.service.peristence.v1.server.integration.client.ApplicationConfigClient;
import com.iqser.red.service.peristence.v1.server.integration.client.FileClient;
import com.iqser.red.service.peristence.v1.server.utils.MetricsPrinterService;
import com.iqser.red.service.peristence.v1.server.utils.StorageIdUtils;
import com.iqser.red.service.persistence.management.v1.processor.client.PDFTronRedactionClient;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.*;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.configuration.ApplicationConfig;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.dossier.file.FileType;
import com.iqser.red.service.redaction.v1.model.RedactionLog;
import com.iqser.red.service.redaction.v1.model.RedactionResult;
import com.iqser.red.storage.commons.StorageAutoConfiguration;
import com.iqser.red.storage.commons.service.StorageService;
import org.assertj.core.util.Lists;
import org.junit.After;
import org.junit.Before;
@ -26,29 +34,14 @@ import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.*;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.iqser.red.service.pdftron.redaction.v1.api.model.DocumentRequest;
import com.iqser.red.service.peristence.v1.server.Application;
import com.iqser.red.service.peristence.v1.server.client.RedactionClient;
import com.iqser.red.service.peristence.v1.server.client.SearchClient;
import com.iqser.red.service.peristence.v1.server.integration.client.FileClient;
import com.iqser.red.service.peristence.v1.server.utils.MetricsPrinterService;
import com.iqser.red.service.peristence.v1.server.utils.StorageIdUtils;
import com.iqser.red.service.persistence.management.v1.processor.client.PDFTronRedactionClient;
import com.iqser.red.service.persistence.service.v1.api.model.dossiertemplate.dossier.file.FileType;
import com.iqser.red.service.redaction.v1.model.RedactionLog;
import com.iqser.red.service.redaction.v1.model.RedactionResult;
import com.iqser.red.storage.commons.StorageAutoConfiguration;
import com.iqser.red.storage.commons.service.StorageService;
import org.testcontainers.shaded.org.apache.commons.io.IOUtils;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@RunWith(SpringRunner.class)
@EnableFeignClients(basePackageClasses = FileClient.class)
@Import(AbstractPersistenceServerServiceTest.TestConfiguration.class)
@ -169,21 +162,21 @@ public abstract class AbstractPersistenceServerServiceTest {
}).when(pdfTronRedactionClient).processUntouchedDocument(any());
when(pdfTronRedactionClient.redact(Mockito.any())).thenAnswer((answer) ->{
when(pdfTronRedactionClient.redact(Mockito.any())).thenAnswer((answer) -> {
Object[] args = answer.getArguments();
DocumentRequest d = (DocumentRequest) args[0];
var untouchedObjectId = StorageIdUtils.getStorageId(d.getDossierId(), d.getFileId(), FileType.ORIGIN);
return new ByteContentDocument(IOUtils.toByteArray(storageService.getObject(untouchedObjectId).getInputStream()));
});
when(pdfTronRedactionClient.redactionPreview(Mockito.any())).thenAnswer((answer) ->{
when(pdfTronRedactionClient.redactionPreview(Mockito.any())).thenAnswer((answer) -> {
Object[] args = answer.getArguments();
DocumentRequest d = (DocumentRequest) args[0];
var untouchedObjectId = StorageIdUtils.getStorageId(d.getDossierId(), d.getFileId(), FileType.ORIGIN);
return new ByteContentDocument(IOUtils.toByteArray(storageService.getObject(untouchedObjectId).getInputStream()));
});
when(pdfTronRedactionClient.redactionPreviewDiff(Mockito.any())).thenAnswer((answer) ->{
when(pdfTronRedactionClient.redactionPreviewDiff(Mockito.any())).thenAnswer((answer) -> {
Object[] args = answer.getArguments();
DocumentRequest d = (DocumentRequest) args[0];
var untouchedObjectId = StorageIdUtils.getStorageId(d.getDossierId(), d.getFileId(), FileType.ORIGIN);
@ -255,8 +248,10 @@ public abstract class AbstractPersistenceServerServiceTest {
postgreSQLContainer.start();
var connectionStringDetails = "?serverTimezone=UTC&cachePrepStmts=true&useServerPrepStmts=true&rewriteBatchedStatements=true";
TestPropertyValues.of(
"spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
"spring.datasource.url=" + postgreSQLContainer.getJdbcUrl() + connectionStringDetails,
"spring.datasource.username=" + postgreSQLContainer.getUsername(),
"spring.datasource.password=" + postgreSQLContainer.getPassword()
).applyTo(configurableApplicationContext.getEnvironment());

View File

@ -24,9 +24,10 @@ public class FileSystemBackArchiverTest {
SplittableRandom sr = new SplittableRandom();
var data = sr.doubles().limit(1024 * 1024).toArray();
for (int i = 0; i < 10; i++) {
log.info("At entry: {}, using {}MB of memory", i, (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024));
var data = sr.doubles().limit(1024 * 1024).toArray();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(data);

View File

@ -77,3 +77,5 @@ management:
metrics.enabled: false
health.enabled: true
endpoints.web.exposure.include: prometheus, health, metrics
logging.level.root: info