Статьи

О доменном моделировании с использованием Spring Data Neo4j

Привет всем, Вилли здесь. В прошлый раз я говорил вам, что я создаю Skybase CMDB с использованием Neo4j и Spring Data Neo4j , и я был рад получить много положительных отзывов об этом. Я показал немного кода, но не так много. В этом посте я покажу вам, как я создаю элемент конфигурации человека (CI) в Skybase, используя Spring Data Neo4j.

Требования к лицу CI

Мы начнем с простого создания КИ человека. Полезно иметь людей в CMDB по разным причинам: они позволяют вам определять детализированные элементы управления доступом (например, Джим может развертывать такие-то приложения в среде разработки; Эрик может развертывать все, что он хочет, где угодно и т. Д.) .); они позволяют вам определять группы, которые будут получать уведомления о критических событиях и инцидентах; и т.п.

Наш сотрудник CI будет иметь имя пользователя, имя и фамилию, некоторые номера телефонов, адрес электронной почты, менеджера, прямые отчеты и, наконец, проекты, над которыми он работает. Нам нужно иметь возможность отображать людей в виде списка, отображать конкретного человека в подробном представлении, позволять пользователям создавать, редактировать и удалять людей и так далее. Вот, например, как будет выглядеть представление списка, по крайней мере сейчас:

А вот как будет выглядеть наше представление деталей:

Отношения между человеком и проектом имеют связанную роль. Эти отношения также являются основой для списка соавторов: два человека являются соавторами, если есть хотя бы один проект, членами которого они являются.

Наших простых требований должно быть достаточно, чтобы показать, каково это — писать код Spring Data Neo4j.

Создание сущностей Person и ProjectMembership

Сначала мы создадим Человека. Я подавил аннотации валидации и JAXB, поскольку они не имеют отношения к нашим текущим целям:

package org.skydingo.skybase.model;

import java.util.Set;
import org.neo4j.graphdb.Direction;
import org.skydingo.skybase.model.relationship.ProjectMembership;
import org.springframework.data.neo4j.annotation.*;
import org.springframework.data.neo4j.support.index.IndexType;

@NodeEntity
public class Person implements Comparable<Person> {
    @GraphId private Long id;

    @Indexed(indexType = IndexType.FULLTEXT, indexName = "searchByUsername")
    private String username;

    private String firstName, lastName, title, workPhone, mobilePhone, email;

    @RelatedTo(type = "REPORTS_TO")
    private Person manager;

    @RelatedTo(type = "REPORTS_TO", direction = Direction.INCOMING)
    private Set<Person> directReports;

    @RelatedToVia(type = "MEMBER_OF")
    private Set<ProjectMembership> memberships;

    public Long getId() { return id; }

    public void setId(Long id) { this.id = id; }

    public String getUsername() { return username; }

    public void setUsername(String username) { this.username = username; }

    ... other accessor methods ...

    public Person getManager() { return manager; }

    public void setManager(Person manager) { this.manager = manager; }

    public Set<Person> getDirectReports() { return directReports; }

    public void setDirectReports(Set<Person> directReports) {
        this.directReports = directReports;
    }

    public Iterable<ProjectMembership> getMemberships() { return memberships; }

    public ProjectMembership memberOf(Project project, String role) {
        ProjectMembership membership = new ProjectMembership(this, project, role);
        memberships.add(membership);
        return membership;
    }

    ... equals(), hashCode(), compareTo() ...
}

Есть много аннотаций, которые мы используем, чтобы создать структуру. Начнем с узлов и их свойств. Затем мы рассмотрим простые отношения между узлами. Затем мы рассмотрим так называемые сущности отношений, которые в основном являются причудливыми отношениями. Во-первых, вот абстрактное представление нашей модели предметной области:

Теперь давайте посмотрим на некоторые детали.

Узлы и их свойства. Когда у нас есть объект на основе узла, сначала мы аннотируем его аннотацией @NodeEntity. Большинство простых свойств узлов (т. Е. Свойств, которые не являются связями с другими узлами) подойдут для поездки. Обратите внимание, что мне не нужно было комментировать firstName, lastName, email и т. Д. Spring Data Neo4j будет обрабатывать отображение там автоматически.

Однако есть несколько исключений. Во-первых, я добавил @GraphId в свое свойство id. Это говорит Spring Data Neo4j, что это идентификатор, который мы можем использовать для поиска. Другая — аннотация @Indexed, которая (неожиданно) создает индекс для рассматриваемого свойства. Это полезно, когда вам нужна альтернатива поиску по идентификатору.

Теперь посмотрим на отношения. Говоря в широком смысле, существуют простые отношения и более сложные отношения. Начнем с простых.

Простые отношения. На низком уровне Neo4j является базой данных графа, поэтому мы можем говорить о графе в теоретических терминах графа, таких как узлы, ребра, направленные ребра, DAG и все такое. Но здесь мы используем графики для моделирования предметной области, поэтому мы интерпретируем понятия низкоуровневых графов в терминах понятий моделирования предметной области более высокого уровня. Язык, который использует Spring Data Neo4j, — это «сущность узла» для узлов и «связь» для ребер.

Наш Person CI имеет простые отношения, называемые REPORTS_TO, которые связывают людей, чтобы мы могли моделировать иерархии отчетов. Person имеет два поля для этих отношений: manager и directReports. Это противоположные сайты с одинаковыми отношениями. Мы используем @RelatedTo (type = «REPORTS_TO») для аннотирования этих полей. У аннотации также есть элемент direction, значением по умолчанию которого является Direction.OUTGOING, что означает, что этот узел является хвостовым краем. Вот почему мы явно указываем direction = Direction.INCOMING для поля directReports.

Как это выглядит в базе данных? Неоклипс раскрывает все. Вот несколько примеров отношений отчетности (щелкните изображение, чтобы увеличить его):

(Немного в стороне: есть аннотация @Fetch — мы увидим ее чуть позже — которая говорит Spring Data Neo4j о необходимости загружать связанный объект. По какой-то причине мне не нужно использовать его для отношений менеджера и прямых отчетов, и я не уверен почему. Если кто-то знает, я был бы признателен за объяснение.)

Отношения сущностей. Помимо отношений REPORTS_TO между людьми, нам важны отношения MEMBER_OF между людьми и проектами. Это более интересно, чем отношение REPORTS_TO, потому что MEMBER_OF имеет ассоциированное свойство — роль — это аналогично добавлению столбца в таблицу ссылок в СУБД, как я упоминал в своем ответе Бригу в предыдущем посте. Метод Person.memberOf () предоставляет удобный способ назначить человека проекту, используя специальную «сущность отношения» ProjectMembership. Вот код:

package org.skydingo.skybase.model.relationship;

import org.skydingo.skybase.model.Person;
import org.skydingo.skybase.model.Project;
import org.springframework.data.neo4j.annotation.*;

@RelationshipEntity(type = "MEMBER_OF")
public class ProjectMembership {
    @GraphId private Long id;
    @Fetch @StartNode private Person person;
    @Fetch @EndNode private Project project;
    private String role;

    public ProjectMembership() { }

    public ProjectMembership(Person person, Project project, String role) {
        this.person = person;
        this.project = project;
        this.role = role;
    }

    public Person getPerson() { return person; }

    public void setPerson(Person person) { this.person = person; }

    public Project getProject() { return project; }

    public void setProject(Project project) { this.project = project; }

    public String getRole() { return role; }

    public void setRole(String role) { this.role = role; }

    ... equals(), hashCode(), toString() ...

}

ProjectMembership, как Person, является сущностью, но это сущность отношений. Мы используем @RelationshipEntity (type = «MEMBER_OF»), чтобы пометить это как объект отношения, и, как и в случае с Person, мы используем @GraphId для свойства id. Аннотации @StartNode и @EndNode указывают хвостовую часть и голову соответственно. @Fetch говорит Spring Data Neo4j загружать узлы с нетерпением. По умолчанию Spring Data Neo4j не очень загружает отношения, поскольку рискует загрузить весь график в память.

Создайте PersonRepository

Вот наш интерфейс PersonRepository:

package org.skydingo.skybase.repository;

import java.util.Set;
import org.skydingo.skybase.model.Person;
import org.skydingo.skybase.model.Project;
import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.repository.GraphRepository;

public interface PersonRepository extends GraphRepository<Person> {

    Person findByUsername(String username);

    @Query("start project=node({0}) match project<--person return person")
    Set<Person> findByProject(Project project);

    @Query(
        "start person=node({0}) " +
        "match person-[:MEMBER_OF]->project<-[:MEMBER_OF]-collaborator " +
        "return collaborator")
    Set<Person> findCollaborators(Person person);
}

В последнем посте я отметил, что все, что нам нужно сделать, это расширить интерфейс GraphRepository; Spring Data генерирует реализацию автоматически.

Для findByUsername () Spring Data может выяснить, для чего предназначен запрос. Для двух других запросов мы используем @Query и язык запросов Cypher, чтобы указать желаемый набор результатов. {0} в запросах относится к параметру метода поиска. В запросе findCollaborators () мы используем [: MEMBER_OF], чтобы указать, каким отношениям мы хотим следовать. Они возвращают наборы вместо Iterables для устранения дубликатов.

Создать веб-контроллер

Мы не будем здесь описывать весь контроллер, но рассмотрим некоторые типичные методы. Предположим, что мы ввели PersonRepository в контроллер.

Создание человека. Чтобы создать человека, мы можем использовать следующее:

@RequestMapping(value = "", method = RequestMethod.POST)
public String createPerson(Model model, @ModelAttribute Person person) {
    personRepo.save(person);
    return "redirect:/people?a=created";
}

Еще раз, мы игнорируем проверку. Все, что нам нужно сделать, это вызвать метод save () в хранилище. Вот так обновления тоже работают.

Нахождение всех людей. Далее, вот как мы можем получить всех людей:

@RequestMapping(value = "", method = RequestMethod.GET)
public String getPersonList(Model model) {
    Iterable<Person> personIt = personRepo.findAll();
    List<Person> people =
        new ArrayList<Person>(IteratorUtil.asCollection(personIt));
    Collections.sort(people);
    model.addAttribute(people);
    return "personList";
}

Нам нужно проделать некоторую работу, чтобы получить Iterable, который PersonRepository.findAll () возвращает в нужный нам формат. IteratorUtil, который поставляется с Neo4j (org.neo4j.helpers.collection.IteratorUtil), помогает здесь.

Нахождение одного человека. Здесь мы хотим отобразить личные данные, которые мы создали выше. Как и в случае с findAll (), мы должны сделать некоторые массажи самостоятельно:

@RequestMapping(value = "/{username}", method = RequestMethod.GET)
public String getPersonDetails(@PathVariable String username, Model model) {
    Person person = personRepo.findByUsername(username);
    List<ProjectMembership> memberships =
        CollectionsUtil.asList(person.getMemberships());
    List<Person> directReports =
        CollectionsUtil.asList(person.getDirectReports());
    List<Person> collaborators =
        CollectionsUtil.asList(personRepo.findCollaborators(person));

    Collections.sort(directReports);
    Collections.sort(collaborators);

    model.addAttribute(person);
    model.addAttribute("memberships", memberships);
    model.addAttribute("directReports", directReports);
    model.addAttribute("collaborators", collaborators);

    return "personDetails";
}

Если вы хотите увидеть JSP, зайдите на сайт Skybase GitHub .

Настройте приложение

Наконец, вот мой файл beans-service.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:neo4j="http://www.springframework.org/schema/data/neo4j"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-3.0.xsd

http://www.springframework.org/schema/data/neo4j

http://www.springframework.org/schema/data/neo4j/spring-neo4j-2.0.xsd

http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

    <context:property-placeholder
        location="classpath:/spring/environment.properties" />
    <context:annotation-config />
    <context:component-scan base-package="org.skydingo.skybase.service" />

    <tx:annotation-driven mode="proxy" />

    <neo4j:config storeDirectory="${graphDb.dir}" />
    <neo4j:repositories base-package="org.skydingo.skybase.repository" />
</beans>

Neo4j имеет базовую модель сопоставления на основе POJO и усовершенствованную модель сопоставления на основе AspectJ. В этой записи блога мы использовали базовый подход, основанный на POJO, поэтому нам не нужно включать конфигурацию, связанную с AspectJ, например <context: spring-configure />.

Там у вас есть это — Person CI при поддержке Neo4j. Удачного кодирования!