Несколько недель я писал о функции 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 с неуникальным индексом, и дублированный узел не будет создан.
Весь код из этого поста доступен как суть, если вы хотите поиграть с ним.