How to treat nested entity add/update/delete operations in Hibernate? - spring

I once again joined a project which uses Hibernate (Spring/Hibernate/Kotlin to be exact) and have read through a number of Vlad Mihalcea wonderful articles to refresh my knowledges about this ORM (this article is of my current interest).
What I'm trying to understand is how should I treat add/update/delete operations for nested entities (bidirectional #OneToMany). Here is what I don't understand.
Say we have a Post entity:
#Entity(name = "Post")
#Table(name = "post")
class Post(
#Id
#GeneratedValue
var id: Long? = null,
val title: String,
#OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private val comments: MutableList<PostComment> = MutableList<PostComment>()
) {
fun addComment(comment: PostComment) {
comments.add(comment)
comment.post = this
}
fun removeComment(comment: PostComment) {
comments.remove(comment)
comment.post = null
}
}
And a PostComment entity:
#Entity(name = "PostComment")
#Table(name = "post_comment")
class PostComment(
#Id
#GeneratedValue
var id: Long? = null,
val review: String,
#ManyToOne(fetch = FetchType.LAZY)
var post: Post
) {
override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o !is PostComment) false
return id != null && id == o.id
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
All in all everything is good, but here is the couple of things I don't know how to cover:
In fact Post class won't compile since I set post field of PostComment to null while it is not nullable. What is a good practice to handle it? Should I make all relations nullable in kotlin just because hibernate require it to be so and it is in contradiction with business logic?
It is more or less clear how to add and delete nested entities, though what should we do if we need to update already existing nested entity. Let's imagine we have a Post(id=1, title="lovely post", comments=[PostComment(id=15, review="good", post=this)] and we get a update action with the following PostDto(id=1, title="not that nice post", comments=[PostComment(id=15, review="bad", post=this)]. As you can see we need to update title for Post and review for PostComment. If we take a look at the Vlad's article I linked above we do not see any update methods. I think it was just ommited since it is not related to article topic.
But I wonder what is the good practice to handle such an update? Something like these two approaches comes to my mind but I'm not sure if these are the best things to do:
#Entity(name = "Post")
#Table(name = "post")
class Post(
//fields...
) {
fun addComment(comment: PostComment) {
comments.add(comment)
comment.post = this
}
fun removeComment(comment: PostComment) {
comments.remove(comment)
comment.post = null
}
// not effective, since issue delete/insert queries, but clean
fun updateComment(comment: PostComment) {
val commentId = comment.id!!
comments.removeIf { it.id == commentId }
comment.team = this
}
// effective, since issue only update query, but dirty as hell
fun updateComment(commentId: Long, review: String) {
val comment = comments.find { it.id == commentId }!!
comment.review = review
}
}
Not actual anymore. Refer good explanation by #Chris in a comment section.
Imagine we need an endpoint to update a comment only. What
is the best way to organise our code base for such a scenario?
Should we always update it like this (always fetch old post, looks
inefficient) or is there any better/efficient approach?
#Transactional
fun reassignComment(newPostId: Long, commentDto: CommentDto) {
val comment = commentRepo.findByIdOrNull(commentDto.id)!!
val oldPost = comment.post
val newPost = postRepo.findByIdOrNull(newPostId)!!
oldPost.removeComment(comment)
newPost.addComment(comment)
}
Thanks anyone for your time and input!

Related

Springboot Mongo reactive repository unable to update nested list

I wanted to update a nested list but I experience a strange behavior where I have to call method twice to get it done...
Here is my POJO:
#Document(collection = "company")
data class Company (
val id: ObjectId,
#Indexed(unique=true)
val name: String,
val customers: MutableList<Customer> = mutableListOf()
//other fields
)
Below is my function from custom repository to do the job which I based on this tutorial
override fun addCustomer(customer: Customer): Mono<Company> {
val query = Query(Criteria.where("employees.keycloakId").`is`(customer.createdBy))
val update = Update().addToSet("customers", customer)
val upsertOption = FindAndModifyOptions.options().upsert(true)
//if I uncomment below this will work...
//mongoTemplate.findAndModify(query, update, upsertOption, Company::class.java).block()
return mongoTemplate.findAndModify(query, update, upsertOption, Company::class.java)
}
In order to actually add this customer I have to either uncomment the block call above or call the method two times in the debugger while running integration tests which is quite confusing to me
Here is the failing test
#Test
fun addCustomer() {
//given
val company = fixture.company
val initialCustomerSize = company.customers.size
companyRepository.save(company).block()
val customerToAdd = CustomerReference(id = ObjectId.get(),
keycloakId = "dummy",
username = "customerName",
email = "email",
createdBy = company.employees[0].keycloakId)
//when, then
StepVerifier.create(companyCustomRepositoryImpl.addCustomer(customerToAdd))
.assertNext { updatedCompany -> assertThat(updatedCompany.customers).hasSize(initialCustomerSize + 1) }
.verifyComplete()
}
java.lang.AssertionError:
Expected size:<3> but was:<2> in:
I found out the issue.
By default mongo returns entity with state of before update. To override it I had to add:
val upsertOption = FindAndModifyOptions.options()
.returnNew(true)
.upsert(true)

Unexpected end of subtree with Criteria Query using isEmpty

I've ran into a problem while developing a Spring Boot application with Criteria API.
I'm having a simple Employer entity, which contains a set of Job.ID (not entities, they're pulled out using repository when needed). Employer and Job are in many to many relationship. This mapping is only used on a purpose of finding Employee with no jobs.
public class Employer {
#ElementCollection
#CollectionTable(
name = "EMPLOYEE_JOBS"
joinColumns = #JoinColumn(name = "EMP_ID")
#Column(name = "JOB_ID")
private final Set<String> jobs = new HashSet<>(); //list of ids of jobs for an employee
}
Then I have a generic function, which returns a predicate (Specification) by a given attributePath and command for any IEntity implementation.
public <E extends IEntity> Specification<E> createPredicate(String attributePath, String command) {
return (r, q, b) -> {
Path<?> currentPath = r;
for(String attr : attributePath.split("\\.")) {
currentPath = currentPath.get(attr);
}
if(Collection.class.isAssignableFrom(currentPath.getJavaType())) {
//currentPath points to PluralAttribute
if(command.equalsIgnoreCase("empty")) {
return b.isEmpty((Expression<Collection<?>>)currentPath);
}
}
}
}
If want to get list of all employee, who currently have no job, I wish I could create the predicate as follows:
Specification<Employer> spec = createPredicate("jobs", "empty");
//or if I want only `Work`s whose were done by employer with no job at this moment
Specification<Work> spec = createPredicate("employerFinished.jobs", "empty");
This unfortunately does not works and throws following exception:
org.hibernate.hql.internal.ast.QuerySyntaxException:
unexpected end of subtree
[select generatedAlias0 from Employer as generatedAlias0
where generatedAlias0.jobs is empty]
Is there a workaround how to make this work?
This bug in Hibernate is known since September 2011, but sadly hasn't been fixed yet. (Update: this bug is fixed as of 5.4.11)
https://hibernate.atlassian.net/browse/HHH-6686
Luckily there is a very easy workaround, instead of:
"where generatedAlias0.jobs is empty"
you can use
"where size(generatedAlias0.jobs) = 0"
This way the query will work as expected.

Map new column from Spring Native query to entity

I have a case statement in my Native query where I am attempting to override a field in my entity.
SELECT i.id, i.ONE_TO_ONE_ID, i.ANOTHER, CASE(WHEN condition THEN 'YES' WHEN another_condition THEN 'NO' ELSE 'MAYBE' END) as word ....
I am using this with JpaRepository as a native query, with pagination.
When I run the native query against my db directly, the result set looks as though I expect.
| id_value | MAPPED_ENTITY_ID_value | another value | word_value (YES) |
When I run the native query from my JpaRepository, everything works there, except word is always null. I cant' seem to figure out how to map the additional String word result to a field in my Entity.
Is there a way to get this to map? Or will I have to create an entire #SqlResultSetMapping() for all of my fields coupled with a native query? (hoping not)
UPDATE: 1
I was generalizing above. Here is my Query.
#Query(
name = "listPagedMapping",
value = "SELECT DISTINCT i.ID, i.INSTANCE_ID, i.REGION, i.CNAME_STACK_ID, i.INSTANCE_STATE, i.IP_ADDRESS, i.EC2_ROLE_NAME, i.INSTANCE_OWNER, i.IS_MASTER, i.EC2_MASTER_ID, i.CNAME, i.EC2_START_TIMESTAMP, i.PRIVATE_DNS, i.INSTANCE_NAME, i.AUTO_TERMINATE, i.AUTO_TERMINATE_DATE, i.TERMINATION_ZONE, i.ADMIN_GROUP_AD_LDAP_ID, i.USER_GROUP_AD_LDAP_ID, (CASE WHEN i.INSTANCE_OWNER=:username THEN 'OWNER' WHEN i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) THEN 'ADMIN' WHEN i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) THEN 'USER' END) as PERMISSION FROM USER u, USER_ACCESS_GROUPS g, EC2_PROVISIONING i WHERE i.INSTANCE_OWNER=:username and i.INSTANCE_STATE in (:instanceStates) or u.username=:username and i.INSTANCE_STATE in (:instanceStates) and g.USER_ID=u.USER_ID and (i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) or i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID))",
countQuery = "SELECT count(*) FROM (SELECT DISTINCT i.* FROM USER u, USER_ACCESS_GROUPS g, EC2_PROVISIONING i WHERE i.INSTANCE_OWNER=:username and i.INSTANCE_STATE in (:instanceStates) or u.username=:username and i.INSTANCE_STATE in (:instanceStates) and g.USER_ID=u.USER_ID and (i.ADMIN_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID) or i.USER_GROUP_AD_LDAP_ID IN (g.AD_LDAP_ID))) as ug",
nativeQuery = true)
Page<Ec2Instance> findAllByPermissionUserAdminOrOwnerAndInstanceStateIn(
#Param("username")final String username,
#Param("instanceStates") final Set<String> instanceStates,
final Pageable pageable);
}
Obviously a bit more complex.
I can get it to map to the entity field with using a named query, but then I loose all the default mappings:
#JsonInclude(JsonInclude.Include.NON_NULL)
#SuppressWarnings("unused")
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(exclude={"masterNode", "workers", "associatedBuckets"})
#Entity
#Table(name = "EC2_PROVISIONING")
#SqlResultSetMapping(
name="listPagedMapping",
columns = {
#ColumnResult(name = "permission", type = String.class)
}
)
#NamedNativeQuery(
name = "listAccessibleInstances",
query = ACCESSIBLE_QUERY,
resultSetMapping = "listPagedMapping"
)
public class Ec2Instance {
....
private String permission;
#column(name = "INSTANCE_ID")
private String instanceId;
#ManyToOne
#JoinColumn(name = "EC2_MASTER_ID")
private Ec2Instance masterNode;
#Setter(AccessLevel.NONE)
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "WORKER_EC2_NODES", joinColumns = { #JoinColumn(name = "EC2_MASTER_ID") }, inverseJoinColumns = {
#JoinColumn(name = "ID") })
private Set<Ec2Instance> workers = new HashSet<>();
... More fields ..
}
I guess, I am hoping there is a way to provide a single mapping on-top of the default mapping that is done by ORM. The above code results in only a pageable of Content PERMISSION, rather than the whole entity + permission.
UPDATE: 2
Ok, so I am getting closer... Seems by removing the #ColumnResult I do get the default mapping, plus the PERMISSION field mapped over! Looks like this:
#SqlResultSetMapping(
name="listPagedMapping"
)
The last issue is it does not accept my CountQuery, and causes my tests to fail whenever a Pagination Query results with multiple pages. Looks like Spring try's to come up with its own CountQuery, which is not correct.
UPDATE: 3
To finish this off, looks like I can provide the Count Query as described here: Spring Data - Why it's not possible to have paging with native query
I will give this a go and update back.
I never got this to work quite how I wanted. I am sure I could by mapping my entire entity, but, that would have been painstaking. I ended up solving this by using NamedNativeQueries, with mapping for the additional Column as a result of my Case statement. My entity class is now annotated like:
#JsonInclude(JsonInclude.Include.NON_NULL)
#SuppressWarnings("unused")
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(callSuper = false)
#Entity
#Table(name = "EC2_PROVISIONING")
#SqlResultSetMappings({
#SqlResultSetMapping(
name = "listPagedMapping",
entities = {
#EntityResult(
entityClass = Ec2Instance.class
)
},
columns = {#ColumnResult(name = "permission", type = String.class)}
),
#SqlResultSetMapping(name = "listPagedMapping.count", columns = #ColumnResult(name = "cnt"))
})
#NamedNativeQueries({
#NamedNativeQuery(
name = "Ec2Instance.listAccessibleInstances",
query = ACCESSIBLE_QUERY,
resultClass = Ec2Instance.class,
resultSetMapping = "listPagedMapping"
),
#NamedNativeQuery(
name = "Ec2Instance.listAccessibleInstances.count",
resultSetMapping = "listPagedMapping.count",
query = ACCESSIBLE_QUERY_COUNT
)
})
We also dont need the permission field in this entity anymore. I removed that.
Then in my Repository:
Page<Object[]> listAccessibleInstances(
#Param("username")final String username,
#Param("instanceStates") final Set<String> instanceStates,
final Pageable pageable);
Thats it! Now the result of my case statement is returned with each entity.
Object[0] = original, default mapped entity.
Object[1] = permission

#Param not working in Spring Data JPA

I'm setting up a Spring Data JPA Repo to work with sequences in a postgresql database. I was assuming that this would be pretty simple:
#Query(nativeQuery = true, value = "CREATE SEQUENCE IF NOT EXISTS ':seq_name' START WITH :startAt")
fun createSequence(#Param("seq_name") seq_name: String, #Param("startAt") startAt: Long = 0)
#Query(nativeQuery = true, value = "SELECT nextval(':seq_name')")
fun nextSerial(#Param("seq_name") seq_name: String) : Long
#Query(nativeQuery = true, value = "DROP SEQUENCE IF EXISTS ':seq_name'")
fun dropSequence(#Param("seq_name") seq_name: String)
#Query(nativeQuery = true, value = "setval(':seq_name', :set_to, false")
fun setSequence(#Param("seq_name") seq_name: String, #Param("set_to") setTo: Long)
But for some reason I get
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter with that name [seq_name] did not exist; whenever I'm trying to call the method. Any idea why this might happen?
Ok, based on the answer from #StanislavL and after some debugging around I have a working solution now. As #posz pointed out I cannot bind identifiers which means I have to hard code the queries. I moved the code from a JPA interface to an implemented service which is not as nice but works.
#Service
open class SequenceService (val entityManager: EntityManager){
#Transactional
fun createSequence(seq_name: String, startAt: Long = 0) {
val query = entityManager.createNativeQuery("CREATE SEQUENCE IF NOT EXISTS ${seq_name} START ${startAt}")
with(query){
executeUpdate()
}
}
#Transactional
fun nextSerial(seq_name: String) : Long {
val query = entityManager.createNativeQuery("SELECT nextval(:seq_name)")
with(query){
setParameter("seq_name", seq_name)
val result = singleResult as BigInteger
return result.toLong()
}
}
#Transactional
fun dropSequence(seq_name: String) {
val query = entityManager.createNativeQuery("DROP SEQUENCE IF EXISTS ${seq_name}")
with(query){
executeUpdate()
}
}
#Transactional
fun setSequence(seq_name: String, setTo: Long){
val query = entityManager.createNativeQuery("SELECT setval(:seq_name, :set_to, false)")
with(query){
setParameter("seq_name", seq_name)
setParameter("set_to", setTo)
singleResult
}
}
}
I hope this is helpful for the next person trying to directly work with sequences when using #SequenceGenerator is not an option.

JPA, merge objects updating childrens

I´ve got this on the parent object
#OneToMany(mappedBy="idUser", cascade = CascadeType.MERGE)
public List<Directions> directions;
And in my controller I´ve got this
public static void userUpdate(String apikey, JsonObject body) {
if(validate(apikey)) {
Long idUser = decode(apikey);
User oldUser = User.findById(idUser);
Map<String, User> userMap = new HashMap<String, User>();
Type arrayListType = new TypeToken<Map<String, User>>(){}.getType();
userMap = gson().fromJson(body, arrayListType);
User user = userMap.get("user");
oldUser.em().merge(user);
oldUser.save();
}else{
forbidden();
}
}
It makes the update on the parent object but when I change something on the children object it doesn't update it and neither gives problems with hibernate or Oracle.
Does anyone know why it doesn´t update the child object?
Thanks all!
Updated with solution!
This is how it works for me, as #JB Nizet said you´ve got to save the child objects too.
oldUser.em().merge(user);
oldUser.save();
for (Direction direction : oldUser.directions) {
direction.save();
}
Another aproach!
With this
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "SASHNBR", insertable = true, updatable = true)
public List<Direction> directions;
I´ve been able to make oldUser.save() and get the child objects saved.
AFAIK, Play requires a call to save() on all the modified entities. So you probably need to iterate through the user's directions and save them as well:
for (Direction direction : user.getDirections()) {
direction.save();
}

Resources