Skip to content

Commit

Permalink
fix: enable the implicit cascading delete for Spring data jpa
Browse files Browse the repository at this point in the history
test: add test for @many to many association

fix: pass entity class

test: add and improve tests
  • Loading branch information
rhfauri committed Sep 22, 2020
1 parent 20697d5 commit b725d34
Show file tree
Hide file tree
Showing 17 changed files with 481 additions and 7 deletions.
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/spring-data-jpa.adoc
Expand Up @@ -161,6 +161,7 @@ INSERT INTO fruit(id, name, color) VALUES (1, 'Cherry', 'Red');
INSERT INTO fruit(id, name, color) VALUES (2, 'Apple', 'Red');
INSERT INTO fruit(id, name, color) VALUES (3, 'Banana', 'Yellow');
INSERT INTO fruit(id, name, color) VALUES (4, 'Avocado', 'Green');
INSERT INTO fruit(id, name, color) VALUES (5, 'Strawberry', 'Red');
----

Hibernate ORM will execute these queries on application startup.
Expand Down
@@ -1,9 +1,20 @@
package io.quarkus.hibernate.orm.panache.runtime;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;

import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadingActions;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.metamodel.model.domain.internal.EntityTypeImpl;

import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.panache.common.Parameters;
Expand Down Expand Up @@ -36,4 +47,76 @@ public static PanacheQuery<?> find(Class<?> entityClass, String query, String co
JpaOperations.bindParameters(jpaQuery, params);
return new CustomCountPanacheQuery(em, jpaQuery, findQuery, countQuery, params);
}

public static long deleteAllWithCascade(Class<?> entityClass) {
EntityManager em = JpaOperations.getEntityManager();
//detecting the case where there are cascade-delete associations, and do the the bulk delete query otherwise.
if (deleteOnCascadeDetected(entityClass)) {
int count = 0;
List<?> objects = JpaOperations.listAll(entityClass);
for (Object entity : objects) {
em.remove(entity);
count++;
}
return count;
}
return JpaOperations.deleteAll(entityClass);
}

/**
* Detects if cascading delete is needed. The delete-cascading is needed when associations with cascade delete enabled
* {@link javax.persistence.OneToMany#cascade()} and also on entities containing a collection of elements
* {@link javax.persistence.ElementCollection}
*
* @param entityClass
* @return true if cascading delete is needed. False otherwise
*/
private static boolean deleteOnCascadeDetected(Class<?> entityClass) {
EntityManager em = JpaOperations.getEntityManager();
Metamodel metamodel = em.getMetamodel();
EntityType<?> entity1 = metamodel.entity(entityClass);
Set<Attribute<?, ?>> declaredAttributes = ((EntityTypeImpl) entity1).getDeclaredAttributes();

CascadeStyle[] propertyCascadeStyles = em.unwrap(SessionImplementor.class)
.getEntityPersister(entityClass.getName(), null)
.getPropertyCascadeStyles();
boolean doCascade = Arrays.stream(propertyCascadeStyles)
.anyMatch(cascadeStyle -> cascadeStyle.doCascade(CascadingActions.DELETE));
boolean hasElementCollection = declaredAttributes.stream().filter(attribute -> attribute.getPersistentAttributeType()
.equals(Attribute.PersistentAttributeType.ELEMENT_COLLECTION)).count() > 0;
return doCascade || hasElementCollection;

}

public static long deleteWithCascade(Class<?> entityClass, String query, Object... params) {
EntityManager em = JpaOperations.getEntityManager();
if (deleteOnCascadeDetected(entityClass)) {
int count = 0;
List<?> objects = JpaOperations.find(entityClass, query, params).list();
for (Object entity : objects) {
em.remove(entity);
count++;
}
return count;
}
return JpaOperations.delete(entityClass, query, params);
}

public static long deleteWithCascade(Class<?> entityClass, String query, Map<String, Object> params) {
EntityManager em = JpaOperations.getEntityManager();
if (deleteOnCascadeDetected(entityClass)) {
int count = 0;
List<?> objects = JpaOperations.find(entityClass, query, params).list();
for (Object entity : objects) {
em.remove(entity);
count++;
}
return count;
}
return JpaOperations.delete(entityClass, query, params);
}

public static long deleteWithCascade(Class<?> entityClass, String query, Parameters params) {
return deleteWithCascade(entityClass, query, params.map());
}
}
Expand Up @@ -19,6 +19,7 @@
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.hibernate.orm.panache.runtime.AdditionalJpaOperations;
import io.quarkus.hibernate.orm.panache.runtime.JpaOperations;
import io.quarkus.spring.data.deployment.DotNames;
import io.quarkus.spring.data.deployment.MethodNameParser;
Expand Down Expand Up @@ -190,7 +191,7 @@ public void add(ClassCreator classCreator, FieldDescriptor entityClassFieldDescr

// call JpaOperations.delete()
ResultHandle delete = methodCreator.invokeStaticMethod(
MethodDescriptor.ofMethod(JpaOperations.class, "delete", long.class,
MethodDescriptor.ofMethod(AdditionalJpaOperations.class, "deleteWithCascade", long.class,
Class.class, String.class, Object[].class),
methodCreator.readInstanceField(entityClassFieldDescriptor, methodCreator.getThis()),
methodCreator.load(parseResult.getQuery()), paramsArray);
Expand Down
Expand Up @@ -42,6 +42,7 @@
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.hibernate.orm.panache.runtime.AdditionalJpaOperations;
import io.quarkus.hibernate.orm.panache.runtime.JpaOperations;
import io.quarkus.spring.data.deployment.DotNames;
import io.quarkus.spring.data.runtime.FunctionalityNotImplemented;
Expand Down Expand Up @@ -887,7 +888,8 @@ private void generateDeleteAll(ClassCreator classCreator, FieldDescriptor entity
try (MethodCreator deleteAll = classCreator.getMethodCreator(deleteAllDescriptor)) {
deleteAll.addAnnotation(Transactional.class);
deleteAll.invokeStaticMethod(
MethodDescriptor.ofMethod(JpaOperations.class, "deleteAll", long.class, Class.class.getName()),
MethodDescriptor.ofMethod(AdditionalJpaOperations.class, "deleteAllWithCascade", long.class,
Class.class.getName()),
deleteAll.readInstanceField(entityClassFieldDescriptor, deleteAll.getThis()));
deleteAll.returnValue(null);
}
Expand Down
Expand Up @@ -3,8 +3,10 @@
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import javax.json.bind.annotation.JsonbDateFormat;
import javax.json.bind.annotation.JsonbProperty;
Expand All @@ -14,6 +16,8 @@
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.MappedSuperclass;
import javax.persistence.OneToMany;
Expand Down Expand Up @@ -44,6 +48,10 @@ public class Person {
@JoinColumn(name = "address_id")
private Address address;

@ManyToMany
@JoinTable(name = "song_like", joinColumns = @JoinColumn(name = "person_id"), inverseJoinColumns = @JoinColumn(name = "song_id"))
private Set<Song> likedSongs = new HashSet<>();

public Person(String name) {
this.name = name;
this.age = RANDOM.nextInt(100);
Expand Down Expand Up @@ -104,6 +112,14 @@ public void setAddress(Address address) {
this.address = address;
}

public Set<Song> getLikedSongs() {
return likedSongs;
}

public void setLikedSongs(Set<Song> likedSongs) {
this.likedSongs = likedSongs;
}

@MappedSuperclass
public static class StreetEntity {

Expand Down Expand Up @@ -168,4 +184,5 @@ public void setPeople(List<Person> people) {
this.people = people;
}
}

}
Expand Up @@ -8,7 +8,9 @@
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

/**
* Demonstrates the ability to add fragments and some of the derived method capabilities
Expand Down Expand Up @@ -44,4 +46,10 @@ public interface PersonRepository extends CrudRepository<Person, Long>, PersonFr
List<Person> findByAddressId(Long addressId);

List<Person> findByAddressStreetNumber(String streetName);

long deleteByAge(Integer age);

@Query(value = "SELECT p FROM Person p JOIN p.likedSongs s WHERE s.id = :songId")
List<Person> findPersonByLikedSong(@Param("songId") Long songId);

}
Expand Up @@ -29,6 +29,9 @@ public class PersonResource {
@Inject
PersonRepository personRepository;

@Inject
SongRepository songRepository;

@Path("/new/{name}")
@GET
@Produces("application/json")
Expand Down Expand Up @@ -109,6 +112,13 @@ public void deleteAllByName(@PathParam("name") String name) {
personRepository.deleteAll(personRepository.findByName(name));
}

@Transactional
@Path("/delete/age/{age}")
@GET
public void deleteByAge(@PathParam("age") Integer age) {
personRepository.deleteByAge(age);
}

@Path("/delete/all")
@GET
public void deleteAll() {
Expand Down Expand Up @@ -222,6 +232,13 @@ public List<Person> findByAddressStreetNumber(@PathParam("streetNumber") String
return personRepository.findByAddressStreetNumber(streetNumber);
}

@GET
@Path("/{id}/songs")
@Produces("application/json")
public List<Song> findPersonLikedSongs(@PathParam("id") Long id) {
return songRepository.findPersonLikedSongs(id);
}

private Date changeNow(LocalDate now, BiFunction<LocalDate, Long, LocalDate> function, long diff) {
return Date.from(function.apply(now, diff).atStartOfDay()
.atZone(ZoneId.systemDefault())
Expand Down
Expand Up @@ -2,29 +2,37 @@

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.CascadeType;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@Entity(name = "Post")
@Table(name = "post")
public class Post {

@Id
@GeneratedValue
@SequenceGenerator(name = "postSeqGen", sequenceName = "postSeq", initialValue = 100, allocationSize = 1)
@GeneratedValue(generator = "postSeqGen")
private Long id;

private String title;

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<PostComment> comments = new ArrayList<>();

@ElementCollection
private Map<String, String> metadata = new HashMap<>();

private boolean bypass;

private ZonedDateTime posted;
Expand Down Expand Up @@ -79,6 +87,14 @@ public void setOrganization(String organization) {
this.organization = organization;
}

public Map<String, String> getMetadata() {
return metadata;
}

public void setMetadata(Map<String, String> metadata) {
this.metadata = metadata;
}

public void addComment(PostComment comment) {
comments.add(comment);
comment.setPost(this);
Expand Down
Expand Up @@ -15,4 +15,6 @@ public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByPostedBefore(ZonedDateTime zdt);

List<Post> findAllByOrganization(String organization);

long deleteByOrganization(String organization);
}
@@ -1,9 +1,13 @@
package io.quarkus.it.spring.data.jpa;

import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
Expand All @@ -19,6 +23,52 @@ public PostResource(PostRepository postRepository, PostCommentRepository postCom
this.postCommentRepository = postCommentRepository;
}

@Path("/new/title/{title}/organization/{organization}")
@GET
@Produces("application/json")
public Post newPost(@PathParam("title") String title, @PathParam("organization") String organization) {
Post post = new Post();
post.setTitle(title);
post.setOrganization(organization);
Map<String, String> metadata = new HashMap<>();
metadata.put("someLabel1", "someValue1");
metadata.put("someLabel2", "someValue2");
post.setMetadata(metadata);
PostComment postComment = new PostComment("new comment for post from " + organization);
postComment.setPost(post);
post.getComments().add(postComment);
return postRepository.save(post);
}

@POST
@Produces("application/json")
@Path("/postId/{id}/key/{key}/value/{value}")
public Optional<Post> addMetadata(@PathParam("id") Long id, @PathParam("key") String key, @PathParam("key") String value) {
Optional<Post> optionalPost = postRepository.findById(id);
Map<String, String> metadata = new HashMap<>();
metadata.put(key, value);
if (optionalPost.isPresent()) {
Post post = optionalPost.get();
post.setMetadata(metadata);
return Optional.of(postRepository.save(post));
}
return Optional.empty();
}

@POST
@Produces("application/json")
@Path("/postComment/postId/{id}/comment/{comment}")
public Optional<Post> addComment(@PathParam("id") Long id, @PathParam("comment") String comment) {
PostComment postComment = new PostComment(comment);
Optional<Post> optionalPost = postRepository.findById(id);
if (optionalPost.isPresent()) {
Post post = optionalPost.get();
post.addComment(postComment);
return Optional.of(postRepository.save(post));
}
return Optional.empty();
}

@GET
@Produces("application/json")
@Path("/all")
Expand Down Expand Up @@ -60,4 +110,17 @@ public List<PostComment> findByPostId(@PathParam("id") Long id) {
public List<PostComment> findAll() {
return postCommentRepository.findAll();
}

@Path("/delete/all")
@GET
public void deleteAll() {
postRepository.deleteAll();
}

@Path("/delete/byOrg/{org}")
@GET
public void deleteByOrganization(@PathParam("org") String organization) {
postRepository.deleteByOrganization(organization);
}

}

0 comments on commit b725d34

Please sign in to comment.