spring-projects / spring-data-neo4j

Provide support to increase developer productivity in Java when using Neo4j. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
http://spring.io/projects/spring-data-neo4j
Apache License 2.0
825 stars 619 forks source link

Relationship is always null with inheritance [DATAGRAPH-1467] #2028

Closed spring-projects-issues closed 3 years ago

spring-projects-issues commented 3 years ago

Julian opened DATAGRAPH-1467 and commented

I'm having trouble mapping a relationship which is involved into an Entity with inheritance.

I'm trying to figure this out for three days now but no luck so far. Regardless of my parameter combination, if I create a no args constructor, an all args constructor the relationship is not mapped. For version 6.0.2 the relationship was always populated as null. Interestingly when I define a Collection of Posts in Tag, since version 6.1 M1 this works. Versions prior to this did not work. Now comes the weird thing: versions prior to 6.1 M1 work, when I define a Post Collection with Relationship annotation in Tag, but the Relationship Type has to be some sort of different non existing relationship. Regardless of that the query gets really slow (one second plus). Also for obvious reasons I don't want to populate my Tag with a Post Collection.

I tried moving the Relationship Collection to each individual sub entity, which also did not yield the desired behavior. At some point I thought okay, maybe its just Kotlin being Kotlin, and recreated the situation in Java as well -> same behavior. I attached a demo project which illustrates my problem even further. In the mean time here is also some Kotlin code which some what describes the problem. In the demo.zip file is also a sample data CSV export from neo4j.

Thanks in advance, contact me if you need further elaboration.

 

@Node("Tag")
data class Tag(@Id @Property(name = "id") val id: String)

@Node("Post")
abstract class Post(
    @Id @GeneratedValue val id: UUID? = null,
    @Relationship(type = "BELONGS_TO", direction = OUTGOING) val tags: Set<Tag>
)

@Node("TextPost")
class TextPost(tags: Set<Tag>, val text : String) : Post(tags = tags)

@Node("ImagePost")
class ImagePost(tags: Set<Tag>, val image : String) : Post(tags = tags)

Affects: 6.1 M1 (2021.0.0), 6.0.2 (2020.0.2)

Attachments:

spring-projects-issues commented 3 years ago

Gerrit Meier commented

Thanks for reporting this, really appreciated. You are indeed right, it is dependent on the inheritance. Some insights for you to understand the current (faulty) behaviour: The automatic created queries have some kind of contract when it comes to naming of relationships (vaguely speaking) in the result. Given a PostRepository will create sth. like Post_BELONGS_TO_Tag. The mapping part on the other hand takes the most concrete label it can find for the name creation, ImagePost in my example and looks for ImagePost_BELONGS_TO_Tag that is of course missing

spring-projects-issues commented 3 years ago

Julian commented

Hey Gerrit, thank you for the fast reply! Also thank you for the explanation, this is very interesting and helpful for me on a personal level.

I'm still having some questions, maybe could help me out on them as well. To me it does not feel like the lib is production ready. I encountered a few other mapping issues in the last couple of days which I'm also going to report. Most of it is related to mapping and wrong query generation. In some part it is very basic stuff.

Is there a way to mitigate this by using hand crafted queries? I looked at the generated queries and saw the framework generates a special format. When I write a query like: "MATCH (p:Post) MERGE (p) - [belongs_to:BELONGS_TO] -> (t:Tag)" the mapping of the relationships does not work. Is there some requirements related to the returned result for the framework to map the result propperly? Unfortunally there is nothing in the official Spring Data Neo4J Reference Documentation. Or at least I was not able to finde things related to it. Some further elaboration or some reference material would realy help me a lot.

thanks in advance again and thank you for your work. stay healthy.

spring-projects-issues commented 3 years ago

Gerrit Meier commented

Thanks for the addition. It took me a while to figure this out and I assume that you are using RETURN p, collect(belongs_to), collect(t) in your custom Cypher. In about 30 minutes there should be a 6.1.0-DATAGRAPH-1467-SNAPSHOT available that fixes the initial problem and the one you mentioned in the comment

spring-projects-issues commented 3 years ago

Julian commented

Thank you Gerrit for the fast fixes! Everything is working now as expected in the snapshot.

Edit: I noticed if you try to update an node, which already exists and you don't change any property the update works. But let's say change a posts location the following error occurs:

"Expected a result with a single record, but this result contains at least one more. Ensure your query returns only one record."

 This only happens with hand crafted queries. For Reference I attached my Query below:

public interface PostRepository extends Neo4jRepository<PostEntity, UUID> {

    @Override
    @Query("MATCH (p:Post { id: $id }) " +
    "MERGE (p) - [belongs_to:BELONGS_TO] -> (t:Tag) " +
    "RETURN p, collect(belongs_to), collect(t)")
    Optional<PostEntity> findById(UUID id);

}

 

Edit 2:

Without a @Transactional ontop of the Repository, whenever you use a hand crafted Query Spring complains about missing write transaction.

"Invocation of init method failed; nested exception is org.springframework.dao.InvalidDataAccessResourceUsageException: Writing in read access mode not allowed. Attempted write to internal graph 1 (tintok); Error code 'Neo.ClientError.Statement.AccessMode"

This works :

@Repository
@Transactional
public interface PostRepository extends Neo4jRepository<PostEntity, UUID> {

    @Override
    @Query("MATCH (p:Post { id: $id }) " +
    "MERGE (p) - [belongs_to:BELONGS_TO] -> (t:Tag) " +
    "RETURN p, collect(belongs_to), collect(t)")
    Optional<PostEntity> findById(UUID id);

}

 

Without the @Transactional, it does not.

I'm feeling really sorry to bother you again, especially before Christmas, but I would really like to use Neo4J in combination with Spring, but all these issues prevent us from using it.

 

spring-projects-issues commented 3 years ago

Gerrit Meier commented

Let's start with the read-only problem:

This is due to the fact that you are overriding the findById method. You can see that all (not explicitly with @Transactional annotated) methods in the default implementation of the Neo4jRepository called SimpleNeo4jRepository are read-only transactions. https://github.com/spring-projects/spring-data-neo4j/blob/master/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java#L53 This includes the findById method that you are overriding. Because of this it needs to be explicitly set to (default) readOnly=false via @Transactional If you would define a custom name for the method like findByIdAndCreateTag the default read-write transaction will get used.

On to the next problem: I cannot reproduce it. Taking an empty database, creating one (Post)->(Tag) pattern in there and then calling the method from your comment "just works on my machine" (given the use of the latest DATAGRAPH-1497-SNAPSHOT, I mentioned above) and returns one result. If you can come up with a reproducer, I would be happy to help you.

spring-projects-issues commented 3 years ago

Julian commented

Thanks for your fast response. I modified my original demo project to elaborate further on the weird behavior. I attached it.

There is an DataSet.cypher which I'm using to put some data into the database. It is located next to the classes.

 

Thank you in advance and Merry Christmas from Germany!

^[^Application.zip]^

spring-projects-issues commented 3 years ago

Gerrit Meier commented

Here come my answers in code snippet form:

// TODO: DOES NOT WORK. [ Cannot coerce MAP to Java boolean ]
// Gerrit Targeted in https://jira.spring.io/browse/DATAGRAPH-1472
// boolean doesUserExist = userRepository.existsByUsername("test@user.de");

/* TODO: DOES NOT WORK [ Query generates id as property but is internal neo4j node ( WHERE t.id = 22 ... " ]
   should be ( WHERE id(t) = 22 ... ) */
// Gerrit: You cannot use the internal id in a derived finder method. Spring Data in general assumes that this is a property
var testTag3 = tagRepository.findByIdAndName(22L, "test-tag2");

var user = userRepository.findByUsername("test@user.de");
// TODO: DOES NOT WORK! [The property 'null' is not mapped to a Graph property!]
// Gerrit Targeted for 6.1 if possible
// var postByUser = repository.findAllByUser(user);
// otherwise use
var postByUsername = repository.findAllByUserUsername("test@user.de");

The last both I kept for next year because it fails on the repository.save(post) if I am not debugging but when I step through it will only fail with the optimistic locking exception on the last line (picture me confused).

Frohe Weihnachten aus Braunschweig.

spring-projects-issues commented 3 years ago

Julian commented

Thank you again Gerrit for the overwhelming support! I think I spamed the issue you enough now.

To elaborate on my use case, I use it for a university project  to build a social media platform as a proof of concept. If I can somewhat help you guys , let me know.

Liebe Grüße aus Darmstadt

meistermeier commented 3 years ago

With the fixes for GH-2167 this is also solved.