I get inconsistent outcomes in Spring Data Neo4j when mapping both ends of a relationship and saving it - spring

I have a User node with FRIEND_REQUEST relationships mapped to a sentFriendRequestList list and to a receivedFriendRequestList list like below:
#Node
data class User(
#Id
#GeneratedValue(UUIDStringGenerator::class)
var userId: String?,
#Relationship(type = "FRIEND_REQUEST", direction = Relationship.Direction.OUTGOING)
var sentFriendRequestList: MutableList<FriendRequest> = mutableListOf(),
#Relationship(type = "FRIEND_REQUEST", direction = Relationship.Direction.INCOMING)
var receivedFriendRequestList: MutableList<FriendRequest> = mutableListOf(),
var email: String
)
The FriendRequest class:
#RelationshipProperties
data class FriendRequest(
#Id
#GeneratedValue
var friendRequestId: Long?,
/**
* Represents the receiver in an OUTGOING relationship and the sender in an INCOMING relationship.
*/
#TargetNode
var friendRequestOtherNode: User
){
constructor(friendRequestOtherNode: User) : this(null, friendRequestOtherNode)
}
When saving multiple friend requests, on some occasions all previously created relationships disappear from the given nodes and only the newly created relationship appears.
I save like this:
fun saveFriendRequest(sender: User, receiver: User) {
val sentFriendRequest = FriendRequest(receiver)
val receivedFriendRequest = FriendRequest(sender)
sender.sentFriendRequestList.add(sentFriendRequest)
receiver.receivedFriendRequestList.add(receivedFriendRequest)
userRepository.save(sender)
userRepository.save(receiver)
}
I don't understand what the problem is, especially since sometimes it runs without failure.
I created a small test project which is available on my GitHub: link. It contains the data structure and a test class that can be run instantly. The tests show the same problem, after multiple runs it can either fail or be successful.

I am a little bit unsure about the right query, but your custom Cypher statement does not return all friends and relationships for deeper links.
Changing it to:
MATCH (user:User{email: \$email})
OPTIONAL MATCH path=(user)-[:FRIEND_REQUEST*]-(friend:User)
WITH user, friend, relationships(path) as friend_requests
RETURN user, collect(friend_requests), collect(distinct(friend))
solved this for me (or the ten test runs were just randomly green).
Another solution would be, to avoid the custom query, define findByEmail(email: String): User and let Spring Data Neo4j create the query.
The problem that occurs is that
First operation:
frodo -> sam
sam -> frodo
Second operation:
frodo -> bilbo
bilbo -> frodo
results for Sam in something like sam-frodo-bilbo.
When you load Frodo or Bilbo the relationships are not completely hydrated to Sam.
The moment you save Bilbo (or Frodo), SDN will through all relationship and eventually not find the Sam relation. Because it is empty, SDN will remove the relationship on save.
Another problem I have seen in your code:
You should definitely annotate the manual defined constructor with #PersistenceConstructor because Kotlin creates an invisible copy constructor and Spring Data in general does not know which to choose. So you might randomly run into the copy constructor.

Related

How to gracefully transform entity into DTO in Kotlin?

I am working on Kotlin + SpringBoot web service, in which I want to transform DTOs into entities in the most convenient way.
Entities:
#Entity
data class Shop(
#Id
#GeneratedValue
val id: Long,
val name: String
#OneToOne
val owner: User,
...
)
#Entity
data class User(
#Id
#GeneratedValue
val id: Long,
val name: String,
...
)
DTO:
data class ShopDTO(
val id: Long,
val name: String,
val ownerId: Long,
val ownerName: String,
...
)
So when someone wants to create a new Shop, my service gets a ShopDTO(name, ownerId) as request body, then I need to transform it into Shop object to be able to save it to the DB. Now here is how my mapper function looks like:
fun fromDTO(source: ShopDTO) = Shop(
id = source.id,
name = source.name,
owner = ???,
...
)
To be able to store a Shop with an owner I only need an id. It would be enough to create a new User with the given ownerId.
To achive this I tried these solutions:
Add default value to the fields in the User class.
Make the fields nullable.
Add a secondary constructor. This also needs default values.
Use some reflection magic to create an empty object and then set the id.
Call a findById method on the UserRepository with the given id.
I want to keep the non-null, immutable fields of my entities and do not want to use reflection. Also do not want to run an unnecessary select DB query just to get back the user by the id.
Could you please suggest me other options? How would you handle this situation? Is there any good mapper framework in Kotlin which can solve this problem?
Firstly, your question says you want to do entity -> DTO, but actually you want to do DTO -> entity, so you should clear that up.
Secondly, you are getting the shop name and owner Id in the ShopDTO. But you are assigning the owner Id to the shop Id in the your fromDTO(source: ShopDTO) function. Changing it up would be sufficient.
fun fromDTO(source: ShopDTO) = Shop(
name = source.name,
owner = ownerRepo.findById(source.ownerId)
)
Obviously, if you're using JPA, then you have to make a DB call to get the owner first. If your business logic doesn't ensure that a User with that Id exists, then you could write a method like this to make a user.
fun getOrCreateUser(ownerId: Long) =
ownerRepo.findUserById(ownerId) ?: User(
id = ownerId,
name = "Some random DefaultName"
).run(ownerRepo::save)
This would get a User by the Id if it exists, or create a new user with some generic name.
Do let me know if this solves your issue!

Is double saving a new entity instance with a Spring data 2 JpaRepository correct?

I have two entities in a bi-directional many to many relationship.
A <-> many to many <-> B
I have an endpoint where a client can create an instance of A, and at the same time add some number of B entities to that A, by passing in an array of B entity id keys. Please keep in mind that these B entities already exist in the database. There is no business or software design case for tightly coupling their creation to the creation of A.
So class A looks like this, and B is the same, but with references to A.
#Entity
class A {
#Id
#GeneratedValue
int id;
#ManyToMany
List<B> bs;
String someValue;
int someValue2;
// With some getters and setters omitted for brevity
}
So at first try my endpoint code looks like this.
public A createA(#RequestBody A aToCreate) {
A savedA = aRepository.save(aToCreate);
savedA.getbs().forEach(b -> Service.callWithBValue(b.getImportantValue());
}
And the client would submit a JSON request like this to create a new A which would contain links to B with id 3, and B with id 4.
{
"bs": [{id:3}, {id:10}],
"someValue": "not important",
"someValue2": 1
}
Okay so everything's working fine, I see all the fields deserializing okay, and then I go to save my new A instance using.
aRepository.save(aToCreate);
And that works great... except for the fact that I need all the data associated with the b entity instances, but the A object returned by aRepository.save() has only populated the autofill fields on A, and done nothing with the B entities. They're still just hollow entities who only have their ids set.
Wut.
So I go looking around, and apparently SimpleJpaRepository does this.
#Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
And since the A entity is brand new, it only persists the A entity, but it doesn't merge it so I don't get any of the rich B data. So okay, if I modify my code to take this into account I get this.
public A createA(#RequestBody A aToCreate) {
A savedA = aRepository.save(aRepository.save(aToCreate));
savedA.getbs().forEach(b -> Service.callWithBValue(b.getImportantValue());
}
Which works just fine. The second pass through the repository service it merges instead of persists, so the B relationships get hydrated.
My question is: Is this correct, or is there something else I can do that doesn't look so ineloquent and awful?
To be clear this ONLY matters when creating a brand new instance of A, and once A is in the database, this isn't an issue anymore because the SimpleJpaRepository will flow into the em.merge() line of code. Also I have tried different CascadingType annotations on the relationship but none of them are what I want. Cascading is about persisting the state of the parent entity's view of its children, to its children, but what I want to do is hydrate the child entities on new instance creation, instead of having to make two trips to the database.
In the case of a new A, aToCreate and savedA are the same instance because that is what the JPA spec madates:
https://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object)
Make an instance managed and persistent.
Spring Data simply returns the same instance so persist/merge can be abstracted into one method.
If the B instances you wish to associate with A are existing entities then you need to fetch a reference to these existing instances and set them on A. You can do this without a database hit by using the T getOne(ID id) method of Spring Data's JpaRepository:
https://docs.spring.io/spring-data/jpa/docs/2.1.4.RELEASE/api/
You can do this in your controller or possibly via a custom deserializer.
This is what I ended up going with. This gives the caller the ability to save and hydrate the instance in one call, and explains what the heck is going on. All my Repository instances now extend this base instance.
public interface BaseRepository<T, ID> extends JpaRepository<T, ID> {
/**
* Saves an instance twice so that it's forced to persist AND then merge. This should only be used for new detached entities that need to be saved, and who also have related entities they want data about hydrated into their object.
*/
#Transactional
default T saveAndHydrate(T save) {
return this.save(this.save(save));
}
}

Updating property and resource link in single PUT query with Spring Data Rest

OK so let's start self referencing object, something like this:
#Data
#Entity
public class FamilyNode {
#Id
#GeneratedValue
private long id;
private boolean orphan;
#ManyToOne
private FamilyNode parent;
}
And a standard repository rest resource like this:
#RepositoryRestResource(collectionResourceRel = "familynodes", path = "familynodes")
public interface FamilyNodeRepository extends CrudRepository<FamilyNode, Long> {
}
Now, let's assume some parent objects which I want to link with already exist with ID=1 and ID=2, each of which were created with a POST to /api/familynodes which looked like this:
{
"orphan": true,
}
If I attempt to create a new client (ID=3) with something like this using a POST request to /api/familynodes, it will work fine with the linked resource updating fine in the DB:
{
"orphan": false,
"parent": "/api/familynodes/1"
}
However, if I attempt to do a PUT with the following body to /api/familynodes/3, the parent property seems to silently do nothing and the database is not updated to reflect the new association:
{
"orphan": false,
"parent": "/api/familynodes/2"
}
Similarly (and this is the use case that I'm getting at), a PUT like this will only update the orphan property but will leave the parent untouched:
{
"orphan": true,
"parent": null
}
So you now have a record which claims to be an orphan, but still has a parent. Of course you could do subsequent REST requests to the resource URI directly but I'm trying to make rest operations atomic so that it's impossible for any single rest query to create invalid state. So now I'm struggling with how do that with what seems like a simple use case without getting into writing my own controller to handle it - am I missing a mechanism here within the realm of spring data rest?
This is the expected behaviour for PUT requests in Spring Data Rest 2.5.7 and above wherein a PUT request does not update the resource links, only the main attributes.
As detailed here by Oliver Gierke:
If we consider URIs for association fields in the payload to update those associations, the question comes up about what's supposed to happen if no URI is specified. With the current behavior, linked associations are simply not a part of the payload as they only reside in the _links block. We have two options in this scenario: wiping the associations that are not handed, which breaks the "PUT what you GET" approach. Only wiping the ones that are supplied using null would sort of blur the "you PUT the entire state of the resource".
You may use a PATCH instead of PUT to achieve the desired result in your case

JPA MERGE failed to update entity field value when this field is a collection(using ElementCollection)

Here we have a Manifest class that includes list of students and teachers, both could be null.
class Manifest{
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "MANIFEST_STUDENT")
List<String> students = new ArrayList<String>();
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "MANIFEST_TEACHER")
List<String> teachers = new ArrayList<String>();;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "MANIFEST_OTHERS")
List<String> others = new ArrayList<String>();;
}
on the UI, there are two multiple select, one for student and one for teacher that let user choose for current manifest.
here is the problem:
When user deselect all students or teachers from the list(meaning remove all students or teachers from current manifest) and click save, unfortunately nothing can be saved, from UI and database it shows that the multiselect chosen looks the SAME as before.
from service layer, the code is simply like this.
manifest.merge();
It seems we must keep at least one student or teacher for the collection field to make the change valid. So what's going on here and what is the solution? BTW, we are on Openjpa.
Kind of resolve the issue, more like a work around:
Before calling merge(), place several condition checkers to make sure the collection fields are not null
public void save(Manifest entity) {
if(entity.getStudents()==null){
entity.setStudents(new ArrayList<String>());
}
if(entity.getTeachers()==null){
entity.setTeachers(new ArrayList<String>());
}
if(entity.getOthers()==null){
entity.setOthers(new ArrayList<String>());
}
entity.merge();
}
Simple as it, it seems the UI returns those collection fields as null even we initiate them as with empty String lists.
cheers.
Initializing a value in a JPA managed class, such as class Manifest, has no bearing on what, or how, JPA will create the class as JPA maps extracted rows to the class. In particular, the result of:
List<String> students = new ArrayList<String>();
is likely to be:
On creation (by JPA) of a new instance, assign an ArrayList<String>() to students.
JPA overwrites students with the data it extracts - the empty ArrayList is dereferenced/lost.
If your code is clearing a list, such as students, use obj.getStudents().clear(). More likely to run into problems if you call obj.setStudents(someEmptyList).
The issue here is how the JPA manager handles empty datasets: as null or as an empty list. The JPA spec (old, not sure about the just released update) doesn't take a position on this point. A relevant article here.
From your comments, it's apparent that OpenJPA may not be respecting a null value for a Collection/List, while it happily manages the necessary changes for when the value is set to an empty list instead. Someone knowing more about OpenJPA than I may be able to help at this stage - meanwhile you've got a workaround.

Integration Test Strategy for Create methods

I want to test if created entity has been correctly persisted to database.There is a service integration test for create method:
#SpringApplicationContext({"setting ...."})
public class PersonServiceIntegrationTest extends UnitilsJUnit4 {
#SpringBeanByName
private PersonService personService;
#Test
public void createPerson() {
String name = "Name";
String sname = "Surename";
DtoPerson item = personService.createPerson(name, sname, Arrays.asList( new DtoAddress("Pisek","CZE", true), new DtoAddress("Strakonice", "CZE", false) );
Assert.notNull("Cannot be null", item);
/*
* This assertion fails because of transaction (I suppose) - item is not in
* database right now.
* Why? Returned dto 'item; is not null?
*/
//domain with all fetched related entities, eg. address
Person p = personService.getPerson(item.getIdPerson());
List<Address> addresses = p.getAddresses();
Assert.notNull("Cannot be null", p);
Assert.notNull("Cannot be null", addresses);//return collection of Address
Assert.notFalse("Cannot be emtpty", addresses.isEmpty());
ReflectionAssert.assertPropertyLeniens("City", Arrays.asList("Pisek", "Strakonice"), addresses);
}
}
Is it necessary to test create entity if I use hibernate? Someone can write you try to test low-level hibernate but hibernate has own tests. There is a trivial code above but I can imagine some specific code which persists more entites at same time (eg. one-many plus several one-one relations). And I want to test if relations has been correctly persisted.
Is there a pattern to do test this way? I have a problem, that record is not at database. I don't want to use returned dto (it presents only agregate root entity - person, but it does not say about person basic data (one-many), person address (one-many) etc.)... i want to get persisted record.
What I do to test the persistence is:
1) I create the Domain entity,
2) save it with Hibernate/JPA,
3) flush and clear the hibernate session/entity manager
4) load the entity again with hibernate
5) compare the original entity with the one that I have (re)loaded
so I am pretty sure that the mapping is more or less correct and every thing get persisted
I decided to rework service method for create person.
PersonService is responsible only to create domain entity Person - test will do only test the returned DtoPerson and its values.
PersonService will inject AddressService, PersonBasicDataService, which they have own create methods with collection as parameter. These services will have own test classes and test only returned collection of DtoAddress or DtoPersonBasicData.
Tests will be simply and will solve only own responsibility. :-)
As #Ralph said in comments under his answer - this test case is not about service layer. There is necessary to test domain layer. And what there is a new idea which I do not use in integration tests - tests has own hibernate session.

Resources