Saving/Updating Neo4j object with relationships in Spring Data Neo4j

I have two questions. Below I list the object I am using.

@Data
@Builder
@Node("Profile")
@NoArgsConstructor
@AllArgsConstructor
public class Profile {

    @Id
    @GeneratedValue(generatorClass = UUIDStringGenerator.class)
    private String profileId;

    private long userId;

    private String bio;

    private String firstName;

    private String lastName;

    private long profilePictureId;

    @Version
    private long version;

    @Relationship(type = "HAS", direction = Relationship.Direction.OUTGOING)
    private Email email;
}
  1. When I was trying to save Profile object with Email set as not null it is not being saved in db and relationship is not created. So I am creating it using @Query. Is it possible to do it using just profileRepository.save() like in Spring Data Neo4j? I was using method like below:
@Query("""
            MATCH (e:Email {emailId: $emailId})
            MATCH (p:Profile {profileId: $profileId})
            CREATE (e)<-[:HAS]-(p)
            """)
    void createEmailHasRelation(@Param(Params.EMAIL_ID) String emailId, @Param(Params.PROFILE_ID) String profileId);
  1. When I am trying to update a Node with this simple method below Node is being saved once more with a new bio value but it loses all of its relationships. How can I update its values without losing its relationships?
@Override
    public final ProfileRequest updateProfileBio(String profileId, String bio) {
        log.info("Updating profile bio for profileId: {}", profileId);

        Profile profile = getProfileFromRepository(profileId);

        profile.setBio(bio);

        profile = profileRepository.save(profile);

        return profileMapper.mapProfileToProfileRequest(profile);
    }

You don’t need a custom query to save. All I have done is create the java object as defined and call the "save" method on the repository. What does your Email object object look like? Have you tried calling "save" by passing a Profile object with the Email object?

The email object:

@Data
@Builder
@Node("Email")
@AllArgsConstructor
@NoArgsConstructor
public class Email {

    @Id
    @GeneratedValue(generatorClass = UUIDStringGenerator.class)
    private String mailId;

    private String emailValue;

    private PrivacyLevel privacyLevel;

}

I tried something like:

Email email = Email
   .build()
   ...
   .build();

profile.setEmail(email);
profileRepository.save(profile);

And it did save neither email nor reliationship. Moreover it feals kinds useless to declare those @Relationship because even with those relationships exist in database when I call
profile.getEmail() it will always be null so I have to run query each time i want to access this object by myself (emailRepository.findByProfileId()).

I finally found solution by myself so lets see:

  1. There was this problem when I saved the email it was created with relation but when I queried for profile it was always null to fix you have to make your profileRepository query look like this:
    @Query("""
            MATCH (p:Profile)
            WHERE p.profileId = $profileId
            MATCH(p)-[r:HAS|BORN_ON|WORKS_AS|IS|STUDIED_AT|WENT_TO|LIVES_IN|FROM]->(e)
            RETURN p, collect(r) as rel, collect(e) as nodes
            """)
    Optional<Profile> findByProfileId(@Param(Params.PROFILE_ID) String profileId);

r - is a relationship you want to query if want only certain types you have to specify them,
e - is a target node of a some type that is connected with your node using relationship in our case email
I have no idea why I have to collect() relations and nodes and using the return as below does not work but the above wrorks.

RETURN p, e as email
// another
RETURN p, collect(e) as email
  1. Problem with the method in provided service is that when i was calling getProfileFromRepository method I did not receive relationships (they were null) so when I was calling profileRepository.save() it was removing all the relationships for my profile node. After fixing using the first query now it perfectly works.

The topic where I found a clue about the whole idea: Spring Data Neo4j 7.1.2 - relationships only return empty sets.

I was able to get it to work using your entities without a custom query. The benefit of SDN is that it will perform the translation from the database entities to your domain objects for you without implementing code.

As you can see from the test, the profile object was saved using just the repository's generated "save" method and retrieved using the repo's "findById" method. The relationship to the Email object and the Email object were created and retrieved.

Note: I did not know what an Email's PrivacyLevel type was, so I removed it.

Profile Entity:

package com.example.sdndemo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Version;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.support.UUIDStringGenerator;

@Data
@Builder
@Node("Profile")
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class Profile {

    @Id
    @GeneratedValue(generatorClass = UUIDStringGenerator.class)
    private String profileId;

    private long userId;

    private String bio;

    private String firstName;

    private String lastName;

    private long profilePictureId;

    @Version
    private long version;

    @Relationship(type = "HAS", direction = Relationship.Direction.OUTGOING)
    private Email email;
}

Email entity:

package com.example.sdndemo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.support.UUIDStringGenerator;

@Data
@Builder
@Node("Email")
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Email {

    @Id
    @GeneratedValue(generatorClass = UUIDStringGenerator.class)
    private String mailId;

    private String emailValue;
}

Repository:

package com.example.sdndemo;

import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProfileRepository extends Neo4jRepository<Profile, String> {
}

Test:

package com.example.sdndemo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Optional;

@SpringBootTest
class SdnDemoApplicationTests {

    @Autowired
    ProfileRepository profileRepository;

    @Test
    void testSave() {
        Email email = Email.of(null, "mickey@google.com");
        Profile profile = Profile.of(
                null,
                200,
                "bio",
                "firstName",
                "lastName",
                1000,
                0,
                email
        );
        Profile savedProfile = profileRepository.save(profile);
        System.out.println("saved profile: " + profile);

        Optional<Profile> queriedProfile = profileRepository.findById(savedProfile.getProfileId());
        System.out.println("retrieved profile: " + queriedProfile.get());
    }
}

Profile objects printed in the log. Notice it has the Email object.

saved profile: Profile(profileId=122fa05f-d494-4fcf-91a2-bbb04dd2ad33, userId=200, bio=bio, firstName=firstName, lastName=lastName, profilePictureId=1000, version=1, email=Email(mailId=d3442d54-609e-4a66-8b10-3ca79e48ffb3, emailValue=mickey@google.com))
retrieved profile: Profile(profileId=122fa05f-d494-4fcf-91a2-bbb04dd2ad33, userId=200, bio=bio, firstName=firstName, lastName=lastName, profilePictureId=1000, version=1, email=Email(mailId=d3442d54-609e-4a66-8b10-3ca79e48ffb3, emailValue=mickey@google.com))

Yes I understand that this can also be a solution but I need solution that will apply to custom queries because in more complex queries like on below the basic queries using method naming will not be enough. Apparently the one I mentioned before is not good enough for such queries.

    @Query(
            value = """
                    MATCH (p:Profile)-[:FRIENDS_WITH]-(f:Profile)
                    WHERE p.profileId = $profileId
                    WITH f, EXISTS((f)<-[:FOLLOWS]-(p)) as isFollowed
                    MATCH (p)-[:FRIENDS_WITH]-(f1:Profile)-[:FRIENDS_WITH]-(f)
                    RETURN f as profile, COUNT(DISTINCT f1) as mutualFriendsCount, isFollowed
                    SKIP $skip LIMIT $limit
                    """,
            countQuery = """
                    MATCH (p:Profile)-[:FRIENDS_WITH]->(f:Profile)
                    WHERE p.profileId = $profileId
                    RETURN COUNT(f)
                    """
    )
    Page<FriendData> findFriendsByProfileId(@Param(Params.PROFILE_ID) String profileId, Pageable pageable);

Yes, you may have a need for custom queries. The thing to remember is the query result has to be a single row. The relationships and related nodes need to be returned in order for the domain object to be created. If the related objects are lists, then you need to collect them in the query.

https://docs.spring.io/spring-data/neo4j/reference/appendix/custom-queries.html

Yes I understand that I need to put them into single row but unfortunately I cannot do somehitng like this:

MATCH (p)-[r:HAS|BORN_ON|WORKS_AT|IS|STUDIED_AT|WENT_TO|LIVES_IN|FROM]-(e)
RETURN collect(p, collect(r) as rel, collect(e) as nodes) as profile

That is why I do not know how to get relationships of a profile and get the mutual friends count in one query

the custom queries still are designed to map your query results to you domain object. It looks like you want some more ad hoc queries. You may want to consider using the Neo4jClient that comes with SDN. You can write any query and manually map them to your result object.

https://docs.spring.io/spring-data/neo4j/reference/appendix/neo4j-client.html#neo4j-client-domain-example