SDN 6: Projection with custom query, entity relations not hydrated

Hello,

I'm using the neo4jClient to execute a custom query, and I'm returning a custom projection class with entities inside.
I need my entities to be hydrated with the data returned from the cypher query.
The issue is that relations are not hydrated in my returned entity.

Version used: SDN 6.1.5

Here is my sample neo4j model:

  • Any node with label ObjetMaquette has a relation EST_DE_TYPE to NoeudType.
  • Any node with label ObjetMaquette has a relation A_POUR_ENFANT to another ObjetMaquette (parent->child relation)
  • Any node with label Formation is also an ObjetMaquette (inheritance).

My custom query will return a list of ObjetMaquette with their root Formations (if they have)

The returned ObjetMaquette needs to be hydrated with the EST_DE_TYPE relation.

The issue is that they are not hydrated.

Am I missing something ?

Thanks !

Edit: I did more tests

  1. neo4jClient and returning a projection class: relations are not hydrated
  2. neo4jTemplate and returning an entity: relations are correctly hydrated
  3. neo4jTemplate and returning a projection class: NoRootNodeMappingException: Could not find mappable nodes or relationships inside Record<{om: node<0>, formationsParentes: [node<2>], types: [node<3>], rs: [relationship<0>]}> for org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntity@38c10190
  4. Query method (@Query) and returning a projection class: relations are not hydrated
  5. Query method returning a projection interface (same structure as the projection class): NotReadablePropertyException: Invalid property 'om' of bean class [com.example.sdn6.entity.NoeudMaquetteEntity]: Could not find field for property during fallback access!

So, I'm still stuck :(

Link to github project: GitHub - gonzalad/sdn6-tests at custom-mapping-relations-not-hydrated, just run ./mvnw test to reproduce the issue (the test uses testcontainer)

The test code

    @Test
    void testFindAllNoeudAndFormationsParentesByCode() {

        String code = "OF1";

        List<NoeudAndFormationsParentesResult> noeudAndFormations = repository.findAllNoeudAndFormationsParentesByCode(code);

        assertThat(noeudAndFormations).hasSize(1);
        NoeudAndFormationsParentesResult result = noeudAndFormations.get(0);
        assertThat(result.getOm()).isNotNull();
        assertThat(result.getOm().getCode()).isEqualTo(code);
        assertThat(result.getFormationsParentes()).hasSize(1);
        // issue: relations are not hydrated
        assertThat(result.getOm().getType()).isNotNull();
    }

Projection class

public class NoeudAndFormationsParentesResult {
    private final NoeudMaquetteEntity om;
    private final List<FormationEntity> formationsParentes;

My custom query

    @Override
    public List<NoeudAndFormationsParentesResult> findAllNoeudAndFormationsParentesByCode(String code) {
        var query = "";
        query += " MATCH pof = (om:ObjetMaquette)-[r:EST_DE_TYPE]->(type:NoeudType)\n"
            + " WHERE \n"
            + " om.code = $code\n";
        query += "WITH om, collect(type) as types, collect(r) as rs\n";
        query += "RETURN \n";
        query += "    om as om,\n";
        query += "    [(om)<-[:A_POUR_ENFANT*1..]-(f:Formation) | f] as formationsParentes,\n";
        query += "    types, rs";
        //@formatter:on
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("code", code);
        List<NoeudAndFormationsParentesResult> noeudsAndFormations = new ArrayList<>();
        BiFunction<TypeSystem, MapAccessor, NoeudMaquetteEntity> mappingFunction = neo4jMappingContext.getRequiredMappingFunctionFor(NoeudMaquetteEntity.class);
        neo4jClient.query(query) //
            .bindAll(parameters) //
            .fetchAs(NoeudMaquetteEntity.class) //
            .mappedBy((t, r) -> {
                NoeudMaquetteEntity om = mappingFunction.apply(t, r.get("om"));
                List<FormationEntity> formationsParentes = this.asListNoeudMaquetteEntity(r.get("formationsParentes"), mappingFunction, t, FormationEntity.class);
                noeudsAndFormations.add(new NoeudAndFormationsParentesResult(om, formationsParentes));
                return om;
            }) //
            .all();
        return noeudsAndFormations;
    }

    private <S extends NoeudMaquetteEntity> List<S> asListNoeudMaquetteEntity(Value value, //
                                                                              BiFunction<TypeSystem, MapAccessor, NoeudMaquetteEntity> mappingFunction, //
                                                                              TypeSystem t, //
                                                                              Class<S> entityType) {
        return StreamSupport.stream(value.values().spliterator(), false) //
            .map(v -> mappingFunction.apply(t, v)) //
            .map(entityType::cast) //
            .collect(Collectors.toList());
    }

The assertThat(result.getOm().getType()).isNotNull() fails

My test data

CREATE (of1:ObjetMaquette {code:'OF1', idDefinition: 'a4f901ab-0bdc-4fb6-a734-19dc44f99303'})
CREATE (of2:ObjetMaquette {code:'OF2', idDefinition: 'f57a0d06-b333-45ac-9239-442fb6f00cb3'})
CREATE (f1:ObjetMaquette:Formation {code:'F1', idDefinition: '7344b15d-0268-47b7-ab00-a81fea73859d'})
CREATE (tof:NoeudType {code:'PT'})
CREATE (tf:NoeudType {code:'Formation'})
CREATE
(of1)-[:EST_DE_TYPE]->(tof),
(f1)-[:A_POUR_ENFANT]->(of1)

Hello,

I've made a local fix by using a copy of RecordMapAccessor.

I can now have my DTO projection class with an entity field and all of the entity relations mapped correctly. I'm using the neo4jClient with a custom mapping method to do that.

Basically I changed

        neo4jClient.query(query) //
            .bindAll(parameters) //
            .fetchAs(NoeudMaquetteEntity.class) //
            .mappedBy((t, r) -> {

                NoeudMaquetteEntity om = mappingFunction.apply(t, r.get("om"));
                ...
                return om;
            }) //

to

        neo4jClient.query(query) //
            .bindAll(parameters) //
            .fetchAs(NoeudMaquetteEntity.class) //
            .mappedBy((t, r) -> {

                NoeudMaquetteEntity om = mappingFunction.apply(t, new RecordMapAccessor(r));
                ...
                return om;
            }) //

IMO, this approach seems a bit hacky, but it works.

Don't know if there's a way to make it work OOB without this hack

Note:

For 1: The thing is that the Neo4jClient is not aware of any mapping information. (More can be found Spring Data Neo4j 3.1. Overview)

The client is mapping agnostic. It doesn’t know about your domain classes and you are responsible for mapping a result to an object suiting your needs.

For 3: It looks like the projection class is assumed to be an entity from SDN site but not considered as a DTO because sdn6-tests/NoeudAndFormationsParentesResult.java at custom-mapping-relations-not-hydrated · gonzalad/sdn6-tests · GitHub does not project the fields of your NoeudMaquetteEntity but is an aggregation class.

Unfortunately I am a little bit lost to find the right code for 4 and 5 in your linked branch.

Thanks Gerrit for your answer !

No worries since I found a solution (even if not perfect ;) )

As of now, I'm approaching the end of my app migration from OGM to SDN6.

I need to fix a misc issue and to do a performance comparison between the app before and after migration.

Just as a note: most issues I had were on projection (either for loading or saving stuff).

As of now, projection implementation is too limited for my application (perhaps due to the entity modelling I had, donno).

To address this, I needed to implement my own saving routines (to save only the node and its direct relationships and not the whole graph) and use this RecordMapAccessor trick to handle my own projection stuff the way I needed it.

If I'm the only one bitten by those issues, I'd say it's not that important :slight_smile: I'll only need to grasp how to correctly address those issues once I'll have done the migration (one step at a time :) )

Thanks for all the help you've provided me !