Статьи

Назначение UUID для узлов и отношений Neo4j

TL; DR. В этом блоге рассказывается о небольшом демонстрационном проекте на github: neo4j-uuid  и объясняется, как автоматически назначать UUID для узлов и отношений в Neo4j. Также включено очень краткое введение в KernelExtensionFactory в Neo4j 1.9.

Немного Rant на Neo4j узла / идентификаторы отношений

Во многих случаях использования существует потребность в сохранении ссылки на узел Neo4j или отношения в сторонней системе. Первая наивная идея, вероятно, заключается в использовании внутреннего идентификатора узла / отношения, который предоставляет Neo4j. Не делай этого! Когда-либо!

Вы спрашиваете, почему? Ну, идентификатор Neo4j — это, по сути, смещение в одном из файлов магазина, которые использует Neo4j (с учетом математики). Предположим, вы удалили пару узлов. Это создает дыры в файлах хранилища, которые Neo4j может восстановить при создании новых узлов в дальнейшем. А так как идентификатор является смещением файла, есть вероятность, что новый узел будет иметь точно такой же идентификатор, как и ранее удаленный узел. Если вы не обновите синхронно все ссылки на идентификаторы узлов, хранящиеся в другом месте, у вас возникнут проблемы. Если neo4j будет полностью переработан с нуля, метод getId () не будет частью публичного API.

Если вы используете идентификаторы узлов только в запросе приложения, например, в этом нет ничего плохого. Повторюсь: никогда не храните идентификатор узла в сторонней системе. Я официально предупредил вас.

UUID,

Достаточно разглагольствовать, давайте посмотрим, что мы можем сделать, чтобы безопасно хранить ссылки на узлы во внешней системе. По сути, нам нужен идентификатор, который не имеет семантики в отличие от идентификатора узла. Распространенным подходом к этому является использование универсально уникальных идентификаторов (UUID) . Java JDK предлагает реализацию UUID, поэтому мы могли бы потенциально использовать UUID.randomUUID () . К сожалению, случайные UUID генерируются медленно. Предпочтительным подходом является использование MAC машины и временной метки в качестве базы для UUID — это должно обеспечить достаточную уникальность. На http://wiki.fasterxml.com/JugHome есть отличная библиотека, которая   предоставляет именно то, что нам нужно.

Автоматические назначения UUID

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

Я реализовал UUIDTransactionEventHandler, который выполняет следующие задачи:

  • Заполните свойство UUID для каждого нового узла или отношения
  • Отклонить транзакцию, если попытка изменить UUID вручную; либо назначение или удаление

public class UUIDTransactionEventHandler implements TransactionEventHandler {

    public static final String UUID_PROPERTY_NAME = "uuid";

    private final TimeBasedGenerator uuidGenerator = Generators.timeBasedGenerator();

    @Override
    public Object beforeCommit(TransactionData data) throws Exception {

        checkForUuidChanges(data.removedNodeProperties(), "remove");
        checkForUuidChanges(data.assignedNodeProperties(), "assign");
        checkForUuidChanges(data.removedRelationshipProperties(), "remove");
        checkForUuidChanges(data.assignedRelationshipProperties(), "assign");

        populateUuidsFor(data.createdNodes());
        populateUuidsFor(data.createdRelationships());

        return null;
    }

    @Override
    public void afterCommit(TransactionData data, java.lang.Object state) {
    }

    @Override
    public void afterRollback(TransactionData data, java.lang.Object state) {
    }

    /**
     * @param propertyContainers set UUID property for a iterable on nodes or relationships
     */
    private void populateUuidsFor(Iterable propertyContainers) {
        for (PropertyContainer propertyContainer : propertyContainers) {
            if (!propertyContainer.hasProperty(UUID_PROPERTY_NAME)) {

                final UUID uuid = uuidGenerator.generate();
                final StringBuilder sb = new StringBuilder();
                sb.append(Long.toHexString(uuid.getMostSignificantBits())).append(Long.toHexString(uuid.getLeastSignificantBits()));

                propertyContainer.setProperty(UUID_PROPERTY_NAME, sb.toString());
            }
        }
    }

    private void checkForUuidChanges(Iterable> changeList, String action) {
        for (PropertyEntry removedProperty : changeList) {
            if (removedProperty.key().equals(UUID_PROPERTY_NAME)) {
                throw new IllegalStateException("you are not allowed to " + action + " " + UUID_PROPERTY_NAME + " properties");
            }
        }
    }

}

Настройка с использованием KernelExtensionFactory

Есть две оставшиеся задачи для полной автоматизации назначений UUID:

  • Нам нужно настроить автоиндексирование для свойств uuid, чтобы иметь удобный способ поиска узлов или связей по UUID
  • Нам нужно зарегистрировать UUIDTransactionEventHandler с базой данных графа

Начиная с версии 1.9 Neo4j имеет понятие KernelExtensionFactory . Используя KernelExtensionFactory, вы можете предоставить класс, который получает обратные вызовы жизненного цикла, например, при запуске или остановке Neo4j. Это правильное место для настройки автоиндексирования и настройки TransactionEventHandler. Поскольку ServiceLoader JVM используется, KernelExtenstionFactories необходимо зарегистрировать в файле META-INF / services / org.neo4j.kernel.extension.KernelExtensionFactory, перечислив все реализации, которые вы хотите использовать:

org.neo4j.extension.uuid.UUIDKernelExtensionFactory

KernelExtensionFactories может объявлять зависимости, поэтому ниже описывается внутренний интерфейс («Зависимости» в коде), в котором есть только геттеры. Использование прокси Neo4j реализует этот класс и предоставит вам необходимые зависимости. Зависимости совпадают с запрошенным типом, см . Исходный код Neo4j, какие классы поддерживаются для зависимости. KernelExtensionFactories должен реализовать метод newKernelExtension, который должен возвращать экземпляр LifeCycle.

Для нашего проекта UUID мы возвращаем экземпляр UUIDLifeCycle:

package org.neo4j.extension.uuid;

import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.event.TransactionEventHandler;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.graphdb.index.AutoIndexer;
import org.neo4j.graphdb.index.IndexManager;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.kernel.lifecycle.LifecycleAdapter;

import java.util.Map;

/**
 * handle the setup of auto indexing for UUIDs and registers a {@link UUIDTransactionEventHandler}
 */
class UUIDLifeCycle extends LifecycleAdapter {

    private TransactionEventHandler transactionEventHandler;
    private GraphDatabaseService graphDatabaseService;
    private IndexManager indexManager;
    private Config config;

    UUIDLifeCycle(GraphDatabaseService graphDatabaseService, Config config) {
        this.graphDatabaseService = graphDatabaseService;
        this.indexManager = graphDatabaseService.index();
        this.config = config;
    }

    /**
     * since {@link org.neo4j.kernel.NodeAutoIndexerImpl#start()} is called *after* {@link org.neo4j.extension.uuid.UUIDLifeCycle#start()} it would apply config settings for auto indexing. To prevent this we change config here.
     * @throws Throwable
     */
    @Override
    public void init() throws Throwable {
        Map params = config.getParams();
        params.put(GraphDatabaseSettings.node_auto_indexing.name(), "true");
        params.put(GraphDatabaseSettings.relationship_auto_indexing.name(), "true");
        config.applyChanges(params);
    }

    @Override
    public void start() throws Throwable {
        startUUIDIndexing(indexManager.getNodeAutoIndexer());
        startUUIDIndexing(indexManager.getRelationshipAutoIndexer());
        transactionEventHandler = new UUIDTransactionEventHandler();
        graphDatabaseService.registerTransactionEventHandler(transactionEventHandler);
    }

    @Override
    public void stop() throws Throwable {
        stopUUIDIndexing(indexManager.getNodeAutoIndexer());
        stopUUIDIndexing(indexManager.getRelationshipAutoIndexer());
        graphDatabaseService.unregisterTransactionEventHandler(transactionEventHandler);
    }

    void startUUIDIndexing(AutoIndexer autoIndexer) {
        autoIndexer.startAutoIndexingProperty(UUIDTransactionEventHandler.UUID_PROPERTY_NAME);
    }

    void stopUUIDIndexing(AutoIndexer autoIndexer) {
        autoIndexer.stopAutoIndexingProperty(UUIDTransactionEventHandler.UUID_PROPERTY_NAME);
    }
}

Большая часть кода довольно проста, в l.44 / 45 настроена автоиндексирование для свойства uuid. l48 регистрирует UUIDTransactionEventHandler с базой данных графа. Не так очевидно код в методе init (). NodeAutoIndexerImpl в Neo4j сам настраивает автоиндексирование и включает или выключает его в зависимости от соответствующей опции конфигурации. Однако мы хотим, чтобы автоиндексирование всегда было включено. К сожалению, NodeAutoIndexerImpl запускается после нашего кода и переопределяет наши настройки. То есть мы настраиваем настройки l.37-40, чтобы вызвать хорошее поведение NodeAutoIndexerImpl.

Поиск узлов или отношений для UUID

Для полноты картины проект также содержит тривиальное неуправляемое расширение для поиска узлов и связей с использованием интерфейса REST, см. UUIDRestInterface . Отправив HTTP GET по адресу http: // localhost: 7474 / db / data / node / <myuuid>, внутренний идентификатор узла вернулся.

Сборка системы и тестирование

Для построения проекта используется Gradle;  build.gradle — это тривиально Конечно пара тестов включены. Как давний наркоман, я, очевидно, использовал Спока для тестирования. Смотрите тестовый код здесь .

Заключительные слова

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