Search code examples
neo4jrelationshipspring-data-neo4jspring-data-neo4j-4unlink

Spring Data Neo4j (4.2.0.M1) Unlink issue between nodes


We are using the 4.2.0M1 version (currently the latest) of Spring Data Neo4j and we are facing an issue when we try to remove a linked child node from the parent collection and then save through the parent repository.

MODEL CLASSES:

@NodeEntity
public class Movie {

    @GraphId
    private Long graphId;

    private String name;

    /**
     * @return the graphId
     */
    public Long getGraphId() {
        return graphId;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

}

@NodeEntity
public class Actor {

    @GraphId
    private Long graphId;

    private String name;

    @Relationship(type = "ACTS_IN")
    private Set<Movie> movies = new HashSet<>();

    /**
     * @return the graphId
     */
    public Long getGraphId() {
        return graphId;
    }

    /**
     * @return the movies
     */
    public Set<Movie> getMovies() {
        return movies;
    }

    public void addMovie(Movie movie) {
        movies.add(movie);
    }

    public void removeMovie(Movie movie) {
        movies.remove(movie);
    }

    /**
     * @param movies the movies to set
     */
    public void setMovies(Set<Movie> movies) {
        this.movies = movies;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

}

THE TEST CLASS:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes={RelationAndCacheTest.TestConfiguration.class})
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
public class RelationAndCacheTest {


@Autowired
private ActorRepository actorRepository;

private static Session neo4jSession;

@Configuration
@EnableAutoConfiguration
@EnableTransactionManagement
@EnableExperimentalNeo4jRepositories("com.xxx")
public static class TestConfiguration {

    @Bean
    public org.neo4j.ogm.config.Configuration configuration() {
        org.neo4j.ogm.config.Configuration config = new org.neo4j.ogm.config.Configuration();
        config
        .driverConfiguration()
        .setDriverClassName("org.neo4j.ogm.drivers.http.driver.HttpDriver").setURI("http://localhost:7474");
            return config;
        }

        @Bean
        public SessionFactory sessionFactory() {
            return new SessionFactory(configuration(), "com.xxx") {
                @Override
                public Session openSession() {
                    neo4jSession =  super.openSession();
                    return neo4jSession;
                }
            };
        }

        @Bean
        public Neo4jTransactionManager transactionManager() {
            return new Neo4jTransactionManager(sessionFactory());
        }

    }

    @Test
    public void relationModificationTest() {

        /** Create an actor named Roger and save it => working */
        Actor actor = new Actor();
        actor.setName("Roger");
        actor = actorRepository.save(actor);

        /** Create a movie and link it to the actor by saving it through the actor repository => working */
        Movie movie = new Movie();
        movie.setName("movie");
        actor.addMovie(movie);
        actor = actorRepository.save(actor);

        /** Remove the movie from the actor and save through the actor repository => link not removed !! */
        actor.removeMovie((Movie) actor.getMovies().toArray()[0]);
        actor.setName("bob");
        actor = actorRepository.save(actor);
    }

}

The link is not supposed to be removed ? is it a bug ? Does anybody is facing the same issue ?


Solution

  • TL;DR

    There are two solutions to the problem:

    1) annotate the test method @Transactional

    2) fetch the objects each time prior to any operation that mutates them.

    Full explanation

    There is an important difference between the behaviour of SDN 4.1 and SDN 4.2 with regard to the underlying OGM Session object.

    The primary role of the Session object is to keep track of what you're doing so it can determine which operations to perform on the graph when you persist an object. It is essentially a cache that keeps track of the state of your objects as you go through the cycle of loading, updating and saving them.

    In SDN 4.1, the Session was not bound to the lifecycle of a Spring transaction. A Session was established outside a transactional context and the Session scope (lifetime) was managed by annotating it with the @Scope annotation, or by programmatically requesting a new Session when required.

    In 4.2 the lifetime of the Session has been changed to be bound to the Spring transactional context in which a Repository operation occurs, which in turn is bound to the thread making the request. To ensure this always works, a new transaction must be created for you if one isn't already in flight. Each new transaction will now get a new Session object.

    So, the reason why this code worked in 4.1 and does not in 4.2 is that there is no longer any shared Session between calls to the ActorRepository. Session information from the first call to the repository is not available for the second one (including critically, which relationships are new, and which are currently persisted in the graph), because they are taking part in separate transactions.

    The 4.2 behaviour was changed because 4.1 forced several limitations on SDN applications's ability to fully integration with the Spring framework.

    Please see http://graphaware.com/neo4j/2016/09/30/upgrading-to-sdn-42.html for further details, including the steps to take in order to upgrade from SDN 4.1 to 4.2