0
votes

There was a question on "how to add labels dynamically to nodes in Neo4j". Is there a way to dynamically change the entity types?

Take an example:

    @NodeEntity
public class User {

   @Properties(prefix = "custom")
   private Map userProperties;

}

I see from https://neo4j.com/blog/spring-data-neo4j-5-0-release/ that I can create dynamic properties. Can I have dynamic types during run-time as well? I want to change "User" type to "Consumer"/"Admin"/"Producer" dynamically when needed. The entity types are non-exhaustive.

Thanks in advance! :)

2

2 Answers

1
votes

There is an @Labels annotation on a Set<String> that is stored/managed in addition to the main type from the class and interfaces.

see: https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#reference:annotating-entities:node-entity:runtime-managed-labels

0
votes

The @Labels mechanism is great and for many use cases the best solution I'd say.

If you want to have another class back out of your repository, than there's indeed much more work needed.

I'm doing this in a music related project. I have Artist (not abstract and totally usable for anything where I don't know whether it's a band or not) and Band and SoloArtist extending from Artist, with additional labels:

@NodeEntity
public class Artist {}

@NodeEntity
public class Band extends Artist{}

What I do know in a custom repository extension is this:

interface ArtistRepository<T extends Artist> extends Repository<T, Long>, ArtistRepositoryExt {

    Optional<T> findOneByName(String name);

    // Specifying the relationships is necessary here because the generic queries won't recognize that
    // Band has a relationship to country that _should_ be loaded with default depth of 1.

    @Query("MATCH (n:Artist) WITH n MATCH p=(n)-[*0..1]-(m) RETURN p ORDER BY n.name")
    List<T> findAllOrderedByName();

    @Query("MATCH (n:Artist) WHERE id(n) = $id WITH n MATCH p=(n)-[*0..1]-(m) RETURN p")
    Optional<T> findById(@Param("id") Long id);

    <S extends T> S save(S artist);
}

interface ArtistRepositoryExt {
    Band markAsBand(Artist artist);

    SoloArtist markAsSoloArtist(Artist artist);
}

class ArtistRepositoryExtImpl implements ArtistRepositoryExt {

    private static final String CYPHER_MARK_AS_BAND = String.format(
        "MATCH (n) WHERE id(n) = $id\n" +
            "OPTIONAL MATCH (n) - [f:BORN_IN] -> (:Country)\n" +
            "REMOVE n:%s SET n:%s\n" +
            "DELETE f",
        SoloArtist.class.getSimpleName(),
        Band.class.getSimpleName());
    private static final String CYPHER_MARK_AS_SOLO_ARTIST = String.format(
        "MATCH (n) WHERE id(n) = $id\n" +
            "OPTIONAL MATCH (n) - [f:FOUNDED_IN] -> (:Country)\n" +
            "REMOVE n:%s SET n:%s\n" +
            "DELETE f",
        Band.class.getSimpleName(),
        SoloArtist.class.getSimpleName());

    private final Session session;

    public ArtistRepositoryExtImpl(Session session) {
        this.session = session;
    }

    @Override
    public Band markAsBand(Artist artist) {
        session.query(CYPHER_MARK_AS_BAND, Map.of("id", artist.getId()));
        // Needs to clear the mapping context at this point because this shared session
        // will know the node only as class Artist in this transaction otherwise.
        session.clear();
        return session.load(Band.class, artist.getId());
    }

    @Override
    public SoloArtist markAsSoloArtist(Artist artist) {
        session.query(CYPHER_MARK_AS_SOLO_ARTIST, Map.of("id", artist.getId()));
        // See above
        session.clear();
        return session.load(SoloArtist.class, artist.getId());
    }
}

While this works neat, I'll get the idea of effort in a deeper nested class scenario. Also, you have do redeclare derived finder methods as I deed if you want to use an repository in a polymorphic way.

I keep dedicated repositories, too.

If this question was still relevant to you, let me know if works for you, too. You'll find the whole project here:

https://github.com/michael-simons/bootiful-music