Avoid multiple delete statements with Spring Data JPA on ManyToMany - java

I have some entities that are related with a #ManyToMany like this (irrelevant fields, constructors and methods removed for brevity):
#Entity
public class Profile {
#Id
#GeneratedValue
private Long id;
#ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(
name = "profile_skills",
joinColumns = #JoinColumn(name = "profile_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "skill_id", referencedColumnName = "id")
)
private Set<Skill> skills;
public void addSkill(Skill skill) {
this.skills.add(requireNonNull(skill));
}
public Set<Skill> getSkills() {
return unmodifiableSet(skills);
}
public void removeSkill(Skill skill) {
this.skills.remove(skill);
}
}
#Entity
#EqualsAndHashCode
public class Skill {
#Id
private String id;
#Column(nullable = false)
private String name;
public String getId() {
return id;
}
}
And in a service class I'm updating Profile with new Skills like this:
#Service
#Transactional
public class ProfileUpdaterService implements ProfileUpdater {
private final ProfileRepository profileRepository;
private final SkillRepository skillRepository;
ProfileUpdaterService(ProfileRepository profileRepository, SkillRepository skillRepository) {
this.profileRepository = profileRepository;
this.skillRepository = skillRepository;
}
#Override
public WebResponse updateData(Supplier<User> userSupplier, ProfileRequest request) {
Profile profile = profileRepository.findByOwner(userSupplier.get())
.map(updateProfileValues(request))
.orElseGet(() -> makeProfile(userSupplier, request));
Profile save = profileRepository.save(profile);
return save != null ? () -> OperationResult.PROFILE_UPDATED : () -> OperationResult.OPERATION_FAILED;
}
private Function<Profile, Profile> updateProfileValues(ProfileRequest request) {
return profile -> {
updateSkills(request, profile);
return profile;
};
}
private void updateSkills(ProfileRequest request, Profile profile) {
Set<Skill> requestSkills = extractSkillsFromRequest(request).collect(toSet());
requestSkills.forEach(profile::addSkill);
Set<Skill> removedSkills = profile.getSkills().stream()
.filter(skill -> !requestSkills.contains(skill))
.collect(toSet());
removedSkills.forEach(profile::removeSkill);
}
private Stream<Skill> extractSkillsFromRequest(ProfileRequest request) {
if (request.getSkills().isEmpty()) {
return Stream.empty();
}
return request.getSkills().stream()
.filter(s -> !s.isEmpty())
.map(this::findOrSaveSkill)
.distinct();
}
private Skill findOrSaveSkill(String s) {
return skillRepository.findById(s)
.orElseGet(() -> skillRepository.save(Skill.of(s)));
}
}
TL;DR: I get a new set of Skills from a request and I have to add the ones that are not present in the Profile and delete the ones not present in the request.
This way of doing it performs one delete statement on the join table for each skill that is removed from the Profile. What would be a (clean) way to combine this into a single delete statement?
NOTE: This is a Spring Boot 1.5.12 app with Spring Data JPA 1.11.11

The issue was, of course, my insufficient knowledge of Hibernate. This problem can be fixed easily setting Hibernate's jdbc.batch_size property. In Spring Boot, on application.properties add this line:
spring.jpa.properties.hibernate.jdbc.batch_size=20
And done, now Hibernate batches up to 20 statements.

Related

Spring - Save entity after delete return EntityNotFoundException

I have many tables that belong to the same Project by ID. When I reload a Project with an existing ID, I need to clear all entities from the database.
Controller:
#CrossOrigin
#RequestMapping(value = "projects", method = RequestMethod.POST)
public ResponseEntity<?> uploadProject(MultipartFile file) {
JsonDataDto projectDto = converterService.convertToDto(file, JsonDataDto.class);
if(projectRepository.exists(projectDto.getId())) {
// Delete all project entities from DB
projectService.delete(projectDto.getId());
}
// Save project to DB
importService.import(projectDto);
}
Project Service (delete):
#Service
#Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public class GenericProjectService implements ProjectService {
// Fields
#Override
public void delete(UUID projectId) {
entity1Repository.deleteByProjectId(projectId)
...
// Most entities are associated with a project by a foreign key.
// Some entities are not linked by a foreign key and are removed manually (entity1Repository for example)
projectRepository.delete(projectId);
}
}
Import Service (save):
#Service
public class GenericImportService implements ImportService {
// Fields
#Override
#Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public void import(JsonDataDto projectDto) {
Collection<Entity1> entity1 = projectDto.getEntity1()
.stream().map(e -> e1Repository.save(e1Mapper.to(e))).collect(...);
Map<UUID, Type> types = new HashMap<>();
Map<UUID, TypeDto> typeDtosById = projectDto.getTypes().stream()
.collect(Collectors.toMap(TypeDto::getId, Function.identity()));
for (UUID typeId : typeDtosById.keySet()) {
saveType(typeId, typeDtosById, types, ...);
}
}
private void saveType(...) {
Type type = new Type();
// Set fields and relations
// Get exception here
type = typeRepository.save(type);
types.put(typeId, type);
}
}
Type Class:
#Entity
#Data
#Table(name = "...", schema = "...")
public class Type {
#Id
private TypePK typeId;
/*
#Data
#NoArgsConstructor
#AllArgsConstructor
#Embeddable
public class TypePK implements Serializable {
#Type(type = "pg-uuid")
#Column(name = "id")
private UUID id;
#ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JoinColumn(name = "project_id", insertable = false, updatable = false)
private Project project;
}
*/
// Fields
#org.hibernate.annotations.Type(type = "pg-uuid")
#Column(name = "parent_id")
private UUID parentId;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumns({
#JoinColumn(name = "parent_id", referencedColumnName = "id", updatable = false, insertable = false),
#JoinColumn(name = "project_id", referencedColumnName = "project_id", updatable = false, insertable = false)})
private Type parent;
}
When the project does not exist in database, the save is successful. If I delete project from controller, it will also be successfully deleted from the database.
If project exists in database and I try to save it again, I get an error: "Unable to find package.Type with id TypePK(id=7e8281fe-77b8-475d-8ecd-c70522f5a403, project=Project(id=8d109d33-e15e-ca81-5f75-09e00a81a194))"
The entities are removed from the database, but the save transaction is rolled back.
I tried to force close the transaction after delete but it did not help:
public void delete(UUID projectId) {
TransactionStatus ts = TransactionAspectSupport.currentTransactionStatus();
entity1Repository.deleteByProjectId(projectId)
...
ts.flush();
}
The only way I found is, in fact, a crutch. I just wait a couple of seconds before starting save:
if(projectRepository.exists(projectDto.getId())) {
// Delete all project entities from DB
projectService.delete(projectDto.getId());
}
// Any timer
DateTime waitFor = DateTime.now().plusSeconds(2);
while(DateTime.now().isBefore(waitFor)) { }
// Save project to DB
importService.import(projectDto);
I managed to solve the problem using the suggestion in this comment: https://stackoverflow.com/a/14369708/10871976
I added an adapter to the delete transaction. On successful deletion in the "afterCompletion" method, I call project saving.

Java Spring Reactive, returning one Mono<..> from many multiple requests

[Java, Spring Reactive, MongoDB]
I'm currently trying to learn Reactive programming by doing and I found a challenge.
I have db object CategoryDB which looks like this:
#NoArgsConstructor
#Getter
#Setter
#Document(collection = DBConstraints.CATEGORY_COLLECTION_NAME)
class CategoryDB {
#Id
private String id;
private String name;
private String details = "";
#Version
private Long version;
private String parentCategoryId;
private Set<String> childCategoriesIds = new HashSet<>();
}
In a service layer I want to use model object Category.
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
}
I want to create Service with method Mono<Category getById(String id). In this case I want to fetch just one level of childCategories and direct parent Category. By default repository deliver Mono findById(..) and Flux findAllById(..) which I could use, but I'm not sure what would be the best way to receive expected result. I would be grateful for either working example or directions where can I find solution for this problem.
I've spent some time to figure out solution for this problem, but as I'm learning I don't know if it's good way of solving problems.
Added some methods to Category:
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
public void addChildCategory(Category childCategory) {
childCategory.updateParentCategory(this);
this.childCategories.add(childCategory);
}
public void updateParentCategory(Category parentCategory) {
this.parentCategory = parentCategory;
}
}
Function inside service would look like this:
#Override
public Mono<Category> findById(String id) {
return categoryRepository.findById(id).flatMap(
categoryDB -> {
Category category = CategoryDBMapper.INSTANCE.toDomain(categoryDB);
Mono<CategoryDB> parentCategoryMono;
if(!categoryDB.getParentCategoryId().isBlank()){
parentCategoryMono = categoryRepository.findById(categoryDB.getParentCategoryId());
}
else {
parentCategoryMono = Mono.empty();
}
Mono<List<CategoryDB>> childCategoriesMono = categoryRepository.findAllById(categoryDB.getChildCategoriesIds()).collectList();
return Mono.zip(parentCategoryMono, childCategoriesMono, (parentCategoryDB, childCategoriesDB) -> {
Category parentCategory = CategoryDBMapper.INSTANCE.toDomain(parentCategoryDB);
category.updateParentCategory(parentCategory);
childCategoriesDB.forEach(childCategoryDB -> {
Category childCategory = CategoryDBMapper.INSTANCE.toDomain(childCategoryDB);
category.addChildCategory(childCategory);
});
return category;
});
}
);
}
Where mapper is used for just basic properties:
#Mapper
interface CategoryDBMapper {
CategoryDBMapper INSTANCE = Mappers.getMapper(CategoryDBMapper.class);
#Mappings({
#Mapping(target = "parentCategoryId", source = "parentCategory.id"),
#Mapping(target = "childCategoriesIds", ignore = true)
})
CategoryDB toDb(Category category);
#Mappings({
#Mapping(target = "parentCategory", ignore = true),
#Mapping(target = "childCategories", ignore = true)
})
Category toDomain(CategoryDB categoryDB);
}
As I said I don't know if it's correct way of solving the problem, but it seem to work. I would be grateful for review and directions.

Spring Data JPA Many to Many Service Repository Problem

I am trying to add ManyToMany entity to my application. I created entity but cannot implement it.
Actor class
#Entity
#Table(name = "actor")
public class Actor {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(nullable = false, name = "actor_name")
private String actorName;
#ManyToMany(mappedBy = "actor", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Movie> movie = new HashSet<Movie>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getActorName() { return actorName; }
public void setActorName(String actorName) {
this.actorName = actorName;
}
public Set<Movie> getMovie() {
return movie;
}
public void setMovie(Set<Movie> movie) {
this.movie = movie;
}
}
In movie class I have
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(
name = "movie_actor",
joinColumns = {#JoinColumn(name = "movie_id")},
inverseJoinColumns = {#JoinColumn(name = "actor_id")}
)
Set<Actor> actor = new HashSet<Actor>();
........................
public Set<Actor> getActor () {
return actor;
}
public void setActor(Set<Actor> actor){
this.actor = actor;
}
I created my entity just like this but in MovieService;
Actor actor = ActorRepository.findByActorName(movie.getActor().getActorName());
movie.setActor(actor);
This part gives me error. movie.getActor().getActorName() method cannot find. Where do I need to look? In IDE it also says method getActorName and setActorName is never used. I am also adding my ActorRepository and ActorService to closer look to the problem.
ActorRepository
public interface ActorRepository extends JpaRepository<Actor, Integer> {
Set<Actor> findByActorName(String actorName);
}
ActorService
#Service
public class ActorService {
private ActorRepository actorRepository;
#Autowired
public ActorService(ActorRepository actorRepository) {
this.actorRepository = actorRepository;
}
public List<Actor> getAllActor() {
return actorRepository.findAll();
}
}
After adding ManyToMany I was using is as OneToMany entity. Services is works for OneToMany. How can I use them for ManyToMany? I need to add multiple actors to my movies. I couldn't find MVC projects for ManyToMany implementation.
You're invoking movie.getActor().getActorName() which basically does a getActorName() on a Set<Actor> object.
You're basically treating the relation as a ManyToOne instead of a OneToMany
You could use the following to fetch the first Actor of the Set
ActorRepository.findByActorName(movie.getActors().iterator().next().getActorName());
But then of course, you don't have all your Actor's names
What you could do is the following
public interface ActorRepository extends JpaRepository<Actor, Integer> {
Set<Actor> findByActorNameIn(List<String> actorName);
}
And invoke it that way
ActorRepository.findByActorNameIn(
movie.getActors()
.stream()
.map(Actor::getName)
.collect(Collectors.toList())
);

Transient field is lost when appending new OneToMany entity

I have two entities which are linked via a OneToMany relationship:
#Entity
#Table(name="bookcase")
public class BookCase {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Transient
#Getter #Setter private Long oldId;
/*
https://vladmihalcea.com/a-beginners-guide-to-jpa-and-hibernate-cascade-types/
*/
#OneToMany(mappedBy = "bookCase", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Bookshelf> bookShelves = new HashSet<>();
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Set<Bookshelf> getBookShelves() { return bookShelves; }
public void setBookShelves(Set<Bookshelf> bookShelves) { this.bookShelves = bookShelves; }
}
#Entity
#Table(name="bookshelf")
public class Bookshelf {
private static final Logger log = LoggerFactory.getLogger(Bookshelf.class);
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Transient
#Getter #Setter private Long oldId;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "bookcase_id")
private BookCase bookCase;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public BookCase getBookCase() { return bookCase; }
public void setBookCase(BookCase bookCase) {
this.bookCase = bookCase;
bookCase.getBookShelves().add(this);
}
#Transient
#Setter private OldIdListener oldIdListener;
/*
When the id is saved, listening DTOs can update their ids
*/
#PostPersist
public void triggerOldId() {
log.info("Postpersist triggered for {}", id);
if (oldIdListener != null) {
oldIdListener.updateId(oldId, id);
}
}
}
public interface OldIdListener {
void updateId(long oldId, long newId);
}
The following test fails:
#Test
public void testThatCascadingListenerIsTriggered() {
var mock = mock(OldIdListener.class);
var mock2 = mock(OldIdListener.class);
var mock3 = mock(OldIdListener.class);
var bookcase = new BookCase();
var shelf1 = new Bookshelf();
shelf1.setOldId(-5L);
shelf1.setBookCase(bookcase);
shelf1.setOldIdListener(mock);
var shelf2 = new Bookshelf();
shelf2.setOldId(-6L);
shelf2.setBookCase(bookcase);
shelf2.setOldIdListener(mock2);
var saved = bookCaseRepository.save(bookcase);
verify(mock).updateId(eq(-5L), anyLong());
verify(mock2).updateId(eq(-6L), anyLong());
var savedBookCase = bookCaseRepository.findById(saved.getId()).get();
assertThat(savedBookCase.getBookShelves()).hasSize(2);
var shelf3 = new Bookshelf();
shelf3.setOldId(-10L);
shelf3.setBookCase(savedBookCase);
shelf3.setOldIdListener(mock3);
savedBookCase.getBookShelves().add(shelf3);
bookCaseRepository.save(savedBookCase);
verify(mock3).updateId(eq(-10L), anyLong());
}
mock3 is never called.
When debugging the code, I can see that the transient fields oldId and oldIdListener are set to null when the #PostPersist method is called on object shelf3, not on shelf1 and 2.
I think this is because I am modifying the Set object; but the object is correctly persisted, it just loses all transient fields. This does not happen when the entire tree is persisted for the first time.
Is this the wrong way to insert a new element to a OneToMany set or where is the error here?
I'm using Spring Boot 2.1.
Thanks!
The field which annotation with #Transient will not persist to the database, so if you want it to persist, you must remove #Transient.

Remote Access To Jpa Entity

I'm currently working on system that consists of Java Web app and C# client app. Web app has Java Web Service, which has method that returns entity object of Program class:
#WebMethod(operationName = "getProgram")
public Program getProgram(#WebParam(name = "macAddress") String macAddress){
Device device = DeviceManager.getInstance().getDevice(macAddress);
if(device != null){
return device.getProgram();
}
return null;
}
This return object of type Program which has many properties and relations:
#Entity
#Table(name = "PROGRAM", schema = "APP")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "Program.getProgramsByWeather", query = "SELECT p FROM Program p WHERE p.weather = :weather")})
public class Program extends DbEntity implements Serializable {
private static final long serialVersionUID = 1L;
#JoinColumn(name = "LOGO_ID", referencedColumnName = "ID")
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch= FetchType.EAGER)
private Logo logo;
#JoinColumn(name = "WEATHER_ID", referencedColumnName = "ID")
#ManyToOne
private Weather weather;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "program", orphanRemoval = true)
private List<ProgramPlaylist> programPlaylistList = new ArrayList<>();
#OneToMany(cascade = CascadeType.ALL, mappedBy = "program", orphanRemoval = true)
private List<ProgramTicker> programTickerList = new ArrayList<>();
#Column(name = "UPDATED")
private boolean updated;
public Program() {
}
public Program(String name, AppUser owner) {
super(name, owner);
}
public Logo getLogo() {
return logo;
}
public void setLogo(Logo logo) {
this.logo = logo;
}
public Weather getWeather() {
return weather;
}
public void setWeather(Weather weather) {
this.weather = weather;
}
public boolean isUpdated() {
return updated;
}
public void setUpdated(boolean updated) {
this.updated = updated;
}
#XmlElement
public List<ProgramPlaylist> getProgramPlaylistList() {
return programPlaylistList;
}
#XmlElement
public List<ProgramTicker> getProgramTickerList() {
return programTickerList;
}
#Override
public String toString() {
return "Program[ id=" + getId() + " ]";
}
}
Client can get this object and accessing some properties in client app like program.name, which it inherits from DbEntity, but when i try to call something like this:
program.logo.name
client throws NullReferenceException.
Same exception occurs when i try to iterate over the elements of programPlaylistList ArrayList.
I'm assuming that the object itself that is passed through to client isn't fully loaded.
How can i solve this problem, please help?!
EDIT
Ok, so I printed out XML response that client get from service and its populated correctly, but for some reason object fields aren't populated and are mostly null.
Why is this occurring?
Bye default, the fetch strategy for #OneToMany annotations is LAZY, have you tried specifying it to EAGER like in the #oneToOne field (fetch= FetchType.EAGER)?

Categories

Resources