1

Versions used: spring-data-neo4j 4.2.0-BUILD-SNAPSHOT / neo4j-ogm 2.0.6-SNAPSHOT

I'm having problems to correctly fetch relationship entities.

The following fetch calls don't return consistent results (executed in the same transaction):

  1. session.query("MATCH (:A)-[b:HAS_B]-(:C) RETURN count(b) as count") returns 1
  2. session.query("MATCH (:A)-[b:HAS_B]-(:C) RETURN b") correctly returns the relationship entity as a RelationshipModel object
  3. session.query(B.class, "MATCH (:A)-[b:HAS_B]-(:C) RETURN b") returns null !

Important remark: When all operations (create, fetch) are done in the same transaction, it seems to be fine.

I have been able to implement a workaround by using session.query(String, Map) to query the relationship entity and map it by myself into my POJO.

@NodeEntity
public class A {
    public A () {}
    public A (String name) {
        this.name = name;
    }

    @GraphId
    private Long graphId;

    private String name;

    @Relationship(type="HAS_B", direction=Relationship.OUTGOING)
    private B b;
}

@RelationshipEntity(type="HAS_B")
public class B {
    public B () {}
    public B (String name, A a, C c) {
        this.name = name;
        this.a = a;
        this.c = c;
    }

    @GraphId
    private Long graphId;

    @StartNode
    private A a;

    @EndNode
    private C c;

    private String name;
}

@NodeEntity
public class C {
    public C () {}
    public C (String name) {
        this.name = name;
    }

    @GraphId
    private Long graphId;

    private String name;
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class, classes={MyTest.TestConfiguration.class})
public class MyTest {
    @Autowired
    private MyBean myBean;

    @Configuration
    @EnableAutoConfiguration
    @EnableTransactionManagement
    @EnableNeo4jRepositories("com.nagra.ml.sp.cpm.core.repositories")
    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.embedded.driver.EmbeddedDriver");
            return config;
        }
        @Bean
        public SessionFactory sessionFactory() {
            return new SessionFactory(configuration(), "com.nagra.ml.sp.cpm.model");
        }
        @Bean
        public Neo4jTransactionManager transactionManager() {
            return new Neo4jTransactionManager(sessionFactory());
        }
        @Bean
        public MyBean myBean() {
            return new MyBean();
        }
    }

    @Test
    public void alwaysFails() {
        myBean.delete();
        myBean.create("1");
        try { Thread.sleep(2000); } catch (InterruptedException e) {} //useless
        myBean.check("1"); // FAILS HERE !
    }

    @Test
    public void ok() {
        myBean.delete();
        myBean.createAndCheck("2");
    }
}

@Transactional(propagation = Propagation.REQUIRED)
public class MyBean {

    @Autowired
    private Session neo4jSession;

    public void delete() {
        neo4jSession.query("MATCH (n) DETACH DELETE n", new HashMap<>());
    }

    public void create(String suffix) {
        C c = new C("c"+suffix);
        neo4jSession.save(c);
        A a = new A("a"+suffix);
        neo4jSession.save(a);
        B bRel = new B("b"+suffix, a, c);
        neo4jSession.save(bRel);
    }

    public void check(String suffix) {
        //neo4jSession.clear(); //Not working even with this
        Number countBRels = (Number) neo4jSession.query("MATCH (:A)-[b:HAS_B]-(:C) WHERE b.name = 'b"+suffix+"' RETURN count(b) as count", new HashMap<>()).iterator().next().get("count");
        assertEquals(1, countBRels.intValue()); // OK
        Iterable<B> bRels = neo4jSession.query(B.class, "MATCH (:A)-[b:HAS_B]-(:C) WHERE b.name = 'b"+suffix+"' RETURN b", new HashMap<>());
        boolean relationshipFound = bRels.iterator().hasNext();
        assertTrue(relationshipFound); // FAILS HERE !
    }

    public void createAndCheck(String suffix) {
        create(suffix);
        check(suffix);
    }
}
tigrou83
  • 157
  • 10
  • Just confirming: Has the other transaction committed? – Jasper Blues Oct 04 '16 at 09:42
  • Yes, according to Neo4jTransactionManager logs, the first 2 transactions are correctly commited (for delete() and create() calls). The third transaction (for check()) is rollbacked because of the assertion exception. And I can also see in the logs that a new session is created for each transaction ("Opened new Session [org.neo4j.ogm.session.Neo4jSession@xxxx] for Neo4j OGM transaction") – tigrou83 Oct 04 '16 at 09:56
  • Which methods ? I tried to mark methods in MyBean with @Transactional, but it doesn't solve the problem. – tigrou83 Oct 04 '16 at 10:07
  • If I mark my test method (alwaysFails()) with Transactional too, it's working because everything is done in the same transaction. – tigrou83 Oct 04 '16 at 10:12
  • Yes I tried to put @Transactional on createAndCheck() (and all other methods of this class), but it's not working better. createAndCheck() has no problems, it's when I call create() and then check() directly that the problem occurs. – tigrou83 Oct 04 '16 at 10:14
  • Ah, this is currently expected behavior. However in other projects (Spring Data JPA), I believe that with tests you get an implicit transaction and rollback. Not the case here. – Jasper Blues Oct 04 '16 at 10:16
  • But that doesn't explain why my fetch query doesn't return what has been committed in database. – tigrou83 Oct 04 '16 at 11:18
  • Hmm good point. Another user had the same issue and it was solved as such. Detailed explanation and possible bug investigstion still required. – Jasper Blues Oct 04 '16 at 11:30

2 Answers2

4

This query session.query(B.class, "MATCH (:A)-[b:HAS_B]-(:C) RETURN b") returns only the relationship but not the start node or end node and so the OGM cannot hydrate this. You need to always return the start and end node along with the relationship like session.query(B.class, "MATCH (a:A)-[b:HAS_B]-(c:C) RETURN a,b,c")

The reason it appears to work when you both create and fetch data in the same transaction is that the session already has a cached copy of a and c and hence b can be hydrated with cached start and end nodes.

Luanne
  • 19,145
  • 1
  • 39
  • 51
0

Firstly, please upgrade from OGM 2.0.6-SNAPSHOT to 2.1.0-SNAPSHOT. I have noticed some off behaviour in the former which might be one part of the issue.

Now on to your test. There are several things going on here which are worth investigating.

  • Use of @DirtiesContext: You don't seem to be touching the context and if you are using it to reset the context between tests so you get a new Session/Transaction then that's going about it the wrong way. Just use @Transactional instead. The Spring JUnit runner will treat this in a special manner (see next point).
  • Being aware that Transactional tests automatically roll back: Jasper is right. Spring Integration Tests will always roll back by default. If you want to make sure your JUnit test commits then you will have to @Commit it. A good example of how to set up your test can be seen here.
  • Knowing how Spring Transaction proxies work. On top of all this confusion you have to make sure you don't simply call transactional method to transactional method in the same class and expect Spring's Transactional behaviour to apply. A quick write up on why can be seen here.

If you address those issues everything should be fine.

Community
  • 1
  • 1
digx1
  • 1,100
  • 5
  • 9
  • Thanks for your feedback ! I've tried 2.1.0-SNAPSHOT, problem is still present. I've tried to remove @DirtiesContext, problem still present. For transactions, since Transactional annotation is configured on my spring bean and not on my test, transactions are commited correctly. And I can see that only external calls to MyBean methods are creating a new transaction, so everything seems to be working perfectly around transactions. I suspect it is more a problem related to cache and/or mapping. – tigrou83 Oct 06 '16 at 09:05
  • My point is that your test set up isn't correct. If your setup is wrong then it doesn't matter how you try and test it. Transaction isolation means any assertion outside of the currently executing transaction won't be seen. Each call in your test is a separate session/transaction. Because Spring rolls back the transaction nothing gets seen in the next call. – digx1 Oct 06 '16 at 23:56
  • Sorry but I don't understand why my test setup is wrong. Let me explain myself. My goal is to test a REST API on my SDN application. Each time a REST call is done, a new session/transaction will be created. The setup I've shown in SO is a simplification of this setup, where MyBean takes the role of my REST API implementation. I don't want my test to be transactional. I want that each call from my test to the MyBean class creates a new session/transaction .... – tigrou83 Oct 10 '16 at 08:40
  • ....And you seem to be certain that my transactions are rollbacking, but it is not true. My test is not transactional, only my spring bean is. So the transactions are committed, the proof is that I can fetch the data created in a transaction from another transaction. You also seem to be forgetting the main point, which is that I'm able to fetch data from Neo4j using session.query(String, Map), which is returning a RelationshipModel object with correct data inside. What is not working is the same query using session.query(Class, String, Map) which is returning null. .... – tigrou83 Oct 10 '16 at 08:41
  • ....That's the proof that the data is actually in the database but is not correctly mapped/returned into my POJO. If you still think my test setup is wrong, please explain me how I'm supposed to implement a test with multiple transactions, otherwise that how I've done it. – tigrou83 Oct 10 '16 at 08:41
  • I've just noticed you are using the embedded driver and there does indeed seem to be some odd behaviour between it and the Spring Integration test framework. If you just need the test to pass and to prove your tests will rollback at the end of the test add a logback config, delete the two `myBean.delete()`s and add a `@Transactional` to the tests. Your tests will pass. If you need to test your REST API's transactionally then you can use a Spring `MockMvc` type test instead. If you need to test them as they are you can open a bug and we can take a look: https://github.com/neo4j/neo4j-ogm/issues – digx1 Oct 10 '16 at 11:36
  • Ok so let's forget about testing for a second. I've tried to start my neo4j database, start my SDN application (Spring boot application) connecting to Neo4j with HttpDriver, and do manual calls on the REST API of my application (1 POST call to create a resource), 1 GET call to get the resource), and I my problem still occurs ! Most of the times (not everytime...), the relationship entities are not retrieved and I get a partially empty GET response payload. Do you have an explanation for this kind of behavior ? – tigrou83 Oct 10 '16 at 12:12
  • Without getting a better understanding of how you have build the app I can't really provide any general advice. Maybe have a read of this and make sure your app follows the advice: http://graphaware.com/neo4j/2016/09/30/upgrading-to-sdn-42.html – digx1 Oct 10 '16 at 12:16