Статьи

Neo4j и Cypher: использование MERGE с индексами / ограничениями схемы

Несколько недель я писал о функции MERGE в Cypher, а в последние несколько дней я изучал, как она работает при использовании с индексами схемы и уникальными ограничениями .

Обычный вариант использования Neo4j — это моделирование пользователей и событий, где событием может быть твит, пост в Facebook или пин-код Pinterest. Модель может выглядеть так:

2013 12 22 20 14 04

У нас был бы поток пар (пользователь, событие) и оператор шифрования, подобный следующему, чтобы получить данные в Neo4j:

MERGE (u:User {id: {userId}})
MERGE (e:Event {id: {eventId}})
MERGE (u)-[:CREATED_EVENT]->(m)
RETURN u, e

Мы хотели бы убедиться, что у нас нет дублирующих пользователей или событий, и MERGE предоставляет семантику для этого:

MERGE гарантирует, что шаблон существует в графе. Либо шаблон уже существует, либо его необходимо создать.

Я хотел посмотреть, что произойдет, если я напишу сценарий, который попытается создать одинаковые пары (пользователь, событие) одновременно и в результате получит следующее:

import org.neo4j.cypher.javacompat.ExecutionEngine;
import org.neo4j.cypher.javacompat.ExecutionResult;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.factory.GraphDatabaseFactory;
import org.neo4j.helpers.collection.MapUtil;
import org.neo4j.kernel.impl.util.FileUtils;
 
...
 
public class MergeTime
{
    public static void main(String[] args) throws Exception
    {
        String pathToDb = "/tmp/foo";
        FileUtils.deleteRecursively(new File(pathToDb));
 
        GraphDatabaseService db = new GraphDatabaseFactory().newEmbeddedDatabase( pathToDb );
        final ExecutionEngine engine = new ExecutionEngine( db );
 
        ExecutorService executor = Executors.newFixedThreadPool( 50 );
        final Random random = new Random();
 
        final int numberOfUsers = 10;
        final int numberOfEvents = 50;
        int iterations = 100;
        final List<Integer> userIds = generateIds( numberOfUsers );
        final List<Integer> eventIds = generateIds( numberOfEvents );
        List<Future> merges = new ArrayList<>(  );
        for ( int i = 0; i < iterations; i++ )
        {
            Integer userId = userIds.get(random.nextInt(numberOfUsers));
            Integer eventId = eventIds.get(random.nextInt(numberOfEvents));
            merges.add(executor.submit(mergeAway( engine, userId, eventId) ));
        }
 
        for ( Future merge : merges )
        {
            merge.get();
        }
 
        executor.shutdown();
 
        ExecutionResult userResult = engine.execute("MATCH (u:User) RETURN u.id as userId, COUNT(u) AS count ORDER BY userId");
 
        System.out.println(userResult.dumpToString());
 
    }
 
    private static Runnable mergeAway(final ExecutionEngine engine,
                                      final Integer userId, final Integer eventId)
    {
        return new Runnable()
        {
            @Override
            public void run()
            {
                try
                {
                    ExecutionResult result = engine.execute(
                            "MERGE (u:User {id: {userId}})\n" +
                            "MERGE (e:Event {id: {eventId}})\n" +
                            "MERGE (u)-[:CREATED_EVENT]->(m)\n" +
                            "RETURN u, e",
                            MapUtil.map( "userId", userId, "eventId", eventId) );
 
                    // throw away
                    for ( Map<String, Object> row : result ) { }
                }
                catch ( Exception e )
                {
                    e.printStackTrace();
                }
            }
        };
    }
 
    private static List<Integer> generateIds( int amount )
    {
        List<Integer> ids = new ArrayList<>();
        for ( int i = 1; i <= amount; i++ )
        {
            ids.add( i );
        }
        return ids;
    }
}

Мы создаем максимум 10 пользователей и 50 событий, а затем делаем 100 итераций случайных пар (пользователь, событие) с 50 одновременными потоками. Затем мы выполняем запрос, который проверяет, сколько пользователей каждого идентификатора было создано, и получает следующий вывод:

+----------------+
| userId | count |
+----------------+
| 1      | 6     |
| 2      | 3     |
| 3      | 4     |
| 4      | 8     |
| 5      | 9     |
| 6      | 7     |
| 7      | 5     |
| 8      | 3     |
| 9      | 3     |
| 10     | 2     |
+----------------+
10 rows

Затем я добавил в индекс схемы пользователей и событий, чтобы увидеть, будет ли это иметь какое-то значение, что недавно спросил Джавад Караби в группе пользователей .

CREATE INDEX ON :User(id)
CREATE INDEX ON :Event(id)

Мы не ожидаем, что это будет иметь значение, так как индексы схемы не гарантируют уникальность, но я все равно запустил t и получил следующий вывод:

+----------------+
| userId | count |
+----------------+
| 1      | 2     |
| 2      | 9     |
| 3      | 7     |
| 4      | 2     |
| 5      | 3     |
| 6      | 7     |
| 7      | 7     |
| 8      | 6     |
| 9      | 5     |
| 10     | 3     |
+----------------+
10 rows

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

CREATE CONSTRAINT ON (user:User) ASSERT user.id IS UNIQUE
CREATE CONSTRAINT ON (event:Event) ASSERT event.id IS UNIQUE

Теперь, если мы запустим тест, мы получим только одного пользователя:

+----------------+
| userId | count |
+----------------+
| 1      | 1     |
| 2      | 1     |
| 3      | 1     |
| 4      | 1     |
| 5      | 1     |
| 6      | 1     |
| 7      | 1     |
| 8      | 1     |
| 9      | 1     |
| 10     | 1     |
+----------------+
10 rows

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

Насколько я могу судить, такое дублирование узлов, с которым мы объединяемся, происходит только в том случае, если вы попытаетесь создать один и тот же узел дважды одновременно. Как только узел будет создан, мы можем использовать MERGE с неуникальным индексом, и дублированный узел не будет создан.

Весь код из этого поста доступен как суть, если вы хотите поиграть с ним.