Несколько недель я писал о функции MERGE в Cypher, а в последние несколько дней я изучал, как она работает при использовании с индексами схемы и уникальными ограничениями .
Обычный вариант использования Neo4j — это моделирование пользователей и событий, где событием может быть твит, пост в Facebook или пин-код Pinterest. Модель может выглядеть так:
У нас был бы поток пар (пользователь, событие) и оператор шифрования, подобный следующему, чтобы получить данные в 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 с неуникальным индексом, и дублированный узел не будет создан.
Весь код из этого поста доступен как суть, если вы хотите поиграть с ним.