Статьи

Моделирование предметной области с помощью Spring Data Neo4j [code]

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

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

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

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

Список лиц

Просмотр списка Person  (Нажмите на изображение , в этой статье , чтобы увеличить.)

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

Персональные данные

Персональные данные

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

Наши простые требования должны быть достаточно, чтобы показать, что он чувствует, как писать 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);
}

I noted in the last post that all we need to do is extend the GraphRepository interface; Spring Data generates the implementation automatically.

Spring Data репозитории

Spring Data repositories

For findByUsername(), Spring Data can figure out what the intended query is there. For the other two queries, we use @Query and the Cypher query language to specify the desired result set. The {0} in the queries refers to the finder method parameter. In the findCollaborators() query, we use [:MEMBER_OF] to indicate which relationship we want to follow. These return Sets instead of Iterables to eliminate duplicates.

Create the web controller

We won’t cover the entire controller here, but we’ll cover some representative methods. Assume that we’ve injected a PersonRepository into the controller.

Creating a person. To create a person, we can use the following:

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

Once again, we’re ignoring validation. All we have to do is call the save() method on the repository. That’s how updates work too.

Finding all people. Next, here’s how we can get all people:

@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";
}

We have to do some work to get the Iterable that PersonRepository.findAll() returns into the format we want. IteratorUtil, which comes with Neo4j (org.neo4j.helpers.collection.IteratorUtil), helps here.

Finding a single person. Here we want to display the personal details we built out above. As with findAll(), we have to do some of the massaging ourselves:

@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";
}

If you want to see the JSPs, check out the Skybase GitHub site.

Configure the app

Finally, here’s my beans-service.xml file:

<?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 has a basic POJO-based mapping model and an advanced AspectJ-based mapping model. In this blog post we’ve been using the basic POJO-based approach, so we don’t need to include AspectJ-related configuration like <context:spring-configured />.

There you have it–a Person CI backed by Neo4j. Happy coding!

To see the code in more detail, or to get involved in Skybase development, please see the Skybase GitHub site.

Source:  http://skydingo.com/blog/?p=64