Обновление RelationshipEntity сбрасывает значения различных других богатых отношений в Neo4j - PullRequest
0 голосов
/ 15 сентября 2018

1.Краткое описание проблемы

Когда я обновляю атрибут одного существующего Neo4j RelationshipEntity, фактические значения различных других богатых отношений того же типа внезапно заменяются их старым, прежним статусом значений.Это кажется , как будто транзакция откатывается.Я ожидаю, что обновляется только атрибут проверяемого RelationshipEntity, а все остальные отношения не затрагиваются.

2.Исходная ситуация

  • , работающая с Neo4j 3.4.7, Spring Boot v2.0.5.RELEASE, Spring v5.0.9.RELEASE
  • без явного использования транзакций
  • сокращеносхема графика:

reduced graph schema

3.Целевое-фактическое сравнение

3.1.Хронологическая последовательность

Упрощенный вариант использования в соответствии со схемой графа можно суммировать следующим образом:

  • Определить все ClassC узлы
  • Для них найти различные связанные ClassB (через ClassA)
  • Для каждого идентифицированного ClassB создать ClassD, включая отношение к ClassB и расширенное отношение CDMapping к ClassC)

3.2 Ожидаемый / положительный результат

Полный описанный блок отлично работает при первом запуске.Установлены различные значения RelationshipEntitys между ClassC и ClassD с атрибутом «Значение по умолчанию», для последнего расширенного отношения предполагается значение «Специальное значение».

after first run

3.3 Подробная проблема

Когда дело доходит до второго цикла блокировки - именно путем сохранения первого нового RelationshipEntity между ClassC и ClassD - атрибуты предыдущего запуска неожиданно возвращаются в «Значение по умолчанию», заменяя исходное «Специальное значение».

during second run

3.4 Neo4j-сгенерированные запросы

В данный момент Neo4j выполняет следующие запросы, запускаемые соответствующими cdMappingDAO.save(cdMapping);:

UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId WITH row,startNode MATCH (endNode) WHERE ID(endNode) = row.endNodeId CREATE (startNode)-[rel:`MAPS_TO`]->(endNode) SET rel += row.props RETURN row.relRef as ref, ID(rel) as id, {type} as type with params {type=rel, rows=[{startNodeId=91, relRef=-45, endNodeId=115, props={attribute=Default Value}}]}
UNWIND {rows} as row MATCH (startNode) WHERE ID(startNode) = row.startNodeId WITH row,startNode MATCH (endNode) WHERE ID(endNode) = row.endNodeId MERGE (startNode)-[rel:`CONTAINS`]->(endNode) RETURN row.relRef as ref, ID(rel) as id, {type} as type with params {type=rel, rows=[{startNodeId=88, relRef=-49, endNodeId=115, props={}}, {startNodeId=92, relRef=-51, endNodeId=91, props={}}, {startNodeId=88, relRef=-54, endNodeId=93, props={}}, {startNodeId=89, relRef=-56, endNodeId=94, props={}}, {startNodeId=92, relRef=-57, endNodeId=90, props={}}]}
UNWIND {rows} as row MATCH (n) WHERE ID(n)=row.nodeId SET n:`ClassA`:`Entity` SET n += row.props RETURN row.nodeId as ref, ID(n) as id, {type} as type with params {type=node, rows=[{nodeId=92, props={name=Class A}}]}
UNWIND {rows} as row MATCH (n) WHERE ID(n)=row.nodeId SET n:`ClassB`:`Entity` SET n += row.props RETURN row.nodeId as ref, ID(n) as id, {type} as type with params {type=node, rows=[{nodeId=88, props={name=Class B1}}, {nodeId=89, props={name=Class B2}}]}
UNWIND {rows} as row MATCH (n) WHERE ID(n)=row.nodeId SET n:`ClassD`:`Entity` SET n += row.props RETURN row.nodeId as ref, ID(n) as id, {type} as type with params {type=node, rows=[{nodeId=115, props={}}, {nodeId=93, props={}}, {nodeId=94, props={}}]}
UNWIND {rows} as row MATCH (n) WHERE ID(n)=row.nodeId SET n:`ClassC`:`Entity` SET n += row.props RETURN row.nodeId as ref, ID(n) as id, {type} as type with params {type=node, rows=[{nodeId=90, props={name=Class C1}}, {nodeId=91, props={name=Class C2}}]}
UNWIND {rows} AS row MATCH ()-[r]-() WHERE ID(r) = row.relId SET r += row.props RETURN ID(r) as ref, ID(r) as id, {type} as type with params {rows=[{relId=104, props={attribute=Default Value}}, {relId=106, props={attribute=Default Value}}], type=rel}

4.Решаемая задача

Подскажите, пожалуйста, почему и по какому компоненту сбрасываются значения атрибутов?Как я могу убедиться, что проверяемый RelationshipEntity обновляется только?Заранее большое спасибо за указание в правильном направлении!

5.Фрагменты кода

5.1 GraphHandler

@Component
public class GraphHandler implements CommandLineRunner {
  private ClassADAO classADAO;
  private ClassBDAO classBDAO;
  private ClassCDAO classCDAO;
  private ClassDDAO classDDAO;
  private CDMappingDAO cdMappingDAO;
  private SessionFactory sessionFactory;


  @Autowired
  public GraphHandler(ClassADAO classADAO, ClassBDAO classBDAO, ClassCDAO classCDAO, ClassDDAO classDDAO, CDMappingDAO cdMappingDAO, SessionFactory sessionFactory) {
    this.classADAO = classADAO;
    this.classBDAO = classBDAO;
    this.classCDAO = classCDAO;
    this.classDDAO = classDDAO;
    this.cdMappingDAO = cdMappingDAO;
    this.sessionFactory = sessionFactory;
  }


  public void run(String... args) {
    createInitialModel();
    runUseCase();
  }


  private void createInitialModel() {
    ClassA classA = new ClassA("Class A");
    ClassB classB1 = new ClassB("Class B1");
    ClassB classB2 = new ClassB("Class B2");
    ClassC classC1 = new ClassC("Class C1");
    ClassC classC2 = new ClassC("Class C2");

    classA.getClassBs().addAll(Arrays.asList(classB1, classB2));
    classA.getClassCs().addAll(Arrays.asList(classC1, classC2));

    classADAO.save(classA);
    classBDAO.save(classB1);
    classBDAO.save(classB2);
    classCDAO.save(classC1);
    classCDAO.save(classC2);
  }


  private void runUseCase() {
    Iterable<ClassC> classCs = classCDAO.findAll();
    for (ClassC classC : classCs) {

      ClassD rememberedClassD = null;
      List<ClassB> classBs = classBDAO.findClassBSelection(classC.getId());
      for (ClassB classB : classBs) {

        ClassD classD = new ClassD();
        classD.setClassB(classB);
        classB.getClassDs().add(classD);
        classDDAO.save(classD);
        rememberedClassD = classD;

        CDMapping cdMapping = new CDMapping(classC, classD, "Default Value");
        cdMappingDAO.save(cdMapping); // <-- here the problem occurs
      }

      // choosing the last created relationship (ClassC-ClassD) and mark it
      CDMapping relationship = cdMappingDAO.getRelationshipByClassCAndClassD(classC.getId(), rememberedClassD.getId());
      relationship.setAttribute("Special Value");
      cdMappingDAO.save(relationship);
    }
  }
}

5.2 CDMapping

@RelationshipEntity(type = "MAPS_TO")
public class CDMapping {
  @Id
  @GeneratedValue
  private Long id;

  @StartNode
  private ClassC classC;

  @EndNode
  private ClassD classD;

  private String attribute;

  public CDMapping(ClassC classC, ClassD classD, String attribute) {
    this.classC = classC;
    this.classD = classD;
    this.attribute = attribute;

    classC.getCdMappings().add(this);
    classD.getCdMappings().add(this);
  }

  // default constructor, getter and setter here
}

5.3 ClassA

@NodeEntity
public class ClassA extends Entity {
  private String name;

  @Relationship(type = "CONTAINS")
  private List<ClassC> classCs = new ArrayList<>();

  @Relationship(type = "MAPS_TO")
  private List<ClassB> classBs = new ArrayList<>();

  // default constructor, getter and setter here
}

5.4 ClassB

@NodeEntity
public class ClassB extends Entity {
  private String name;

  @Relationship(type = "MAPS_TO", direction = Relationship.INCOMING)
  private List<ClassA> classAs = new ArrayList<>();


  @Relationship(type = "CONTAINS")
  private List<ClassD> classDs = new ArrayList<>();

  // default constructor, getter and setter here
}

5.5 ClassC

@NodeEntity
public class ClassC extends Entity {
  private String name;

  @Relationship(type = "CONTAINS", direction = Relationship.INCOMING)
  private ClassA classA;

  @Relationship(type = "MAPS_TO")
  private List<CDMapping> cdMappings = new ArrayList<>();

  // default constructor, getter and setter here
}

5.6 ClassD

@NodeEntity
public class ClassD extends Entity {
  @Relationship(type = "CONTAINS", direction = Relationship.INCOMING)
  private ClassB classB;

  @Relationship(type = "MAPS_TO", direction = Relationship.INCOMING)
  private List<CDMapping> cdMappings = new ArrayList<>();

  // default constructor, getter and setter here
}

Обновление

5.7 CDMappingDAO

@Repository
public interface CDMappingDAO extends Neo4jRepository<CDMapping, Long> {

  @Query("MATCH (classC:ClassC)-[relationship:MAPS_TO]-(classD:ClassD) WHERE id(classC)={classCId} AND id(classD)={classDId} RETURN classC, relationship, classD;")
  CDMapping getRelationshipByClassCAndClassD(@Param("classCId") Long classCId, @Param("classDId") Long classDId);
}

5.8 ClassADAO / ClassCDAO / ClassDDAO

@Repository
public interface ClassADAO extends Neo4jRepository<ClassA, Long> {
}

За исключением первого типа Neo4jRepository, ClassCDAO и ClassDDAO идентичны.

5.9 ClassBDAO

@Repository
public interface ClassBDAO extends Neo4jRepository<ClassB, Long> {
  @Query("MATCH (classC:ClassC)<-[:CONTAINS]-(:ClassA)-[:MAPS_TO]->(classB:ClassB) WHERE id(classC)={classCId} RETURN classB;")
  List<ClassB> findClassBSelection(@Param("classCId") Long classCId);
}

1 Ответ

0 голосов
/ 17 сентября 2018

В данный момент кажется, что где-то в OGM есть ошибка, но я нашел два обходных пути для вашей проблемы.

Обходной путь один оставляет ваш код почти таким, какой он есть:

Изменить runUseCase Один из них находится в GraphHandler, чтобы сохранить CDMapping с глубиной 0:

private void runUseCase() {
  Iterable<ClassC> classCs = classCDAO.findAll();
  for (ClassC classC : classCs) {

    ClassD rememberedClassD = null;
    List<ClassB> classBs = classBDAO.findClassBSelection(classC.getId());
    for (ClassB classB : classBs) {

      ClassD classD = new ClassD();
      classD.setClassB(classB);
      classB.getClassDs().add(classD);
      classDDAO.save(classD);
      rememberedClassD = classD;

      CDMapping cdMapping = new CDMapping(classC, classD, "Default Value");
      cdMappingDAO.save(cdMapping, 0); // <-- here the problem occurs
    }

    // choosing the last created relationship (ClassC-ClassD) and mark it
    CDMapping relationship = cdMappingDAO.getRelationshipByClassCAndClassD(classC.getId(), rememberedClassD.getId());
    relationship.setAttribute("Special Value");
    cdMappingDAO.save(relationship, 0);
  }
}

Это оставляет «специальные» нетронутыми.

Я бы скорее предложил следующее изменение, добавив еще несколько правильных границ транзакции.

Введите класс обслуживания, такой как SomeGraphBasedService.Выделенный класс необходим в связи с тем, как работают декларативные транзакции Springs.Оба метода, createInitialModel и runUseCase, теперь охватывают одну транзакцию, и в ней участвуют все методы DAO.Пожалуйста, обратите особое внимание на комментарий, я сохраняю только самый верхний родительский класс в createInitialModel:

import java.util.Arrays;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class SomeGraphBasedService {
    private final ClassADAO classADAO;
    private final ClassBDAO classBDAO;
    private final ClassCDAO classCDAO;
    private final ClassDDAO classDDAO;
    private final CDMappingDAO cdMappingDAO;

    public SomeGraphBasedService(ClassADAO classADAO, ClassBDAO classBDAO,
        ClassCDAO classCDAO, ClassDDAO classDDAO, CDMappingDAO cdMappingDAO) {
        this.classADAO = classADAO;
        this.classBDAO = classBDAO;
        this.classCDAO = classCDAO;
        this.classDDAO = classDDAO;
        this.cdMappingDAO = cdMappingDAO;
    }

    @Transactional
    public void createInitialModel() {
        ClassA classA = new ClassA("Class A");
        ClassB classB1 = new ClassB("Class B1");
        ClassB classB2 = new ClassB("Class B2");
        ClassC classC1 = new ClassC("Class C1");
        ClassC classC2 = new ClassC("Class C2");

        classA.getClassBs().addAll(Arrays.asList(classB1, classB2));
        classB1.getClassAs().add(classA);
        classB2.getClassAs().add(classA);
        classA.getClassCs().addAll(Arrays.asList(classC1, classC2));

        classADAO.save(classA);
        // No need to save them one by one, the releationships
        // take care of that while saving the parent class at the top
        /*
        classBDAO.save(classB1);
        classBDAO.save(classB2);
        classCDAO.save(classC1);
        classCDAO.save(classC2);
        */
    }

    @Transactional
    public void runUseCase() {

        Iterable<ClassC> classCs = classCDAO.findAll();
        for (ClassC classC : classCs) {

            ClassD rememberedClassD = null;
            List<ClassB> classBs = classBDAO.findClassBSelection(classC.getId());
            for (ClassB classB : classBs) {

                ClassD classD = new ClassD();
                classD.setClassB(classB);
                classB.getClassDs().add(classD);
                classDDAO.save(classD);
                rememberedClassD = classD;

                CDMapping cdMapping = new CDMapping(classC, classD, "Default Value");
                cdMappingDAO.save(cdMapping); // <-- here the problem occurs
            }

            // choosing the last created relationship (ClassC-ClassD) and mark it
            CDMapping relationship = cdMappingDAO
                .getRelationshipByClassCAndClassD(classC.getId(), rememberedClassD.getId());
            relationship.setAttribute("Special Value");
            cdMappingDAO.save(relationship);
        }

        Iterable<CDMapping> f = cdMappingDAO.findAll();
        for (CDMapping ff : f) {
            System.out.println(ff);
        }
    }
}

Тогда ваш бегун командной строки становится таким:

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class GraphHandler implements CommandLineRunner {
    private final SomeGraphBasedService someGraphBasedService;

    public GraphHandler(SomeGraphBasedService someGraphBasedService) {
        this.someGraphBasedService = someGraphBasedService;
    }

    public void run(String... args) {
        this.someGraphBasedService.createInitialModel();
        this.someGraphBasedService.runUseCase();

    }
}

Пока первыйочевидно, это обходной путь, другое решение - то, которое я бы предпочел в сценарии реального мира.

В любом случае, вывод обоих параметров теперь соответствует ожиданиям:

CDMapping{id=21, classC=ClassC{name='Class C1'}, classD=ClassD{classB=ClassB{name='Class B1'}}, attribute='Default Value'}
CDMapping{id=23, classC=ClassC{name='Class C1'}, classD=ClassD{classB=ClassB{name='Class B2'}}, attribute='Special Value'}
CDMapping{id=25, classC=ClassC{name='Class C2'}, classD=ClassD{classB=ClassB{name='Class B1'}}, attribute='Default Value'}
CDMapping{id=27, classC=ClassC{name='Class C2'}, classD=ClassD{classB=ClassB{name='Class B2'}}, attribute='Special Value'}
...