1. 概述
Neo4j 是目前市面上最流行、功能最全的图数据之一,本文将学习如何在Java中使用Neo4j。
2. 内嵌 Neo4j
Neo4j数据库可以独立安装,也可嵌入到Java中作为应用程序的一部分。为了简单起见,本文使用嵌入式方式:
首先,添加 Maven 依赖:
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j</artifactId>
<version>5.8.0</version>
</dependency>
获取最新版本请访问Maven中央仓库。
Next, let’s create a DatabaseManagementService:
DatabaseManagementService managementService = new
DatabaseManagementServiceBuilder(new File("data/cars").toPath())
.setConfig(GraphDatabaseSettings.transaction_timeout, Duration.ofSeconds( 60 ) )
.setConfig(GraphDatabaseSettings.preallocate_logical_logs, true ).build();
This includes an embedded database. Finally, we create a GraphDatabaseService:
GraphDatabaseService graphDb = managementService.database( DEFAULT_DATABASE_NAME );
Now the real action can begin! First, we need to create some nodes in our graph and for that, we need to start a transaction since Neo4j will reject any destructive operation unless a transaction has been started:
Transaction transaction = graphDb.beginTx();
Each operation that we execute like createNode/execute should run in the context of the created transaction and use that object.
Once we have a transaction in progress, we can start adding nodes:
Node car = transaction.createNode(Label.label("Car"));
car.setProperty("make", "tesla");
car.setProperty("model", "model3");
Node owner = transaction.createNode(Label.label("Person"));
owner.setProperty("firstName", "baeldung");
owner.setProperty("lastName", "baeldung");
Here we added a node Car with properties make and model as well as node Person with properties firstName and lastName
Now we can add a relationship:
owner.createRelationshipTo(car, RelationshipType.withName("owner"));
The statement above added an edge joining the two nodes with an owner label. We can verify this relationship by running a query written in Neo4j’s powerful Cypher language:
Result result = transaction.execute(
"MATCH (c:Car) <-[owner]- (p:Person) " +
"WHERE c.make = 'tesla'" +
"RETURN p.firstName, p.lastName");
Here we ask to find a car owner for any car whose make is tesla and give us back his/her firstName and lastName. Unsurprisingly, this returns: {p.firstName=baeldung, p.lastName=baeldung}
3. Cypher 查询语言
Neo4j provides a very powerful and pretty intuitive querying language which supports the full range of features one would expect from a database. Let us examine how we can accomplish that standard create, retrieve, update and delete tasks.
**3.1. 创建节点
Create keyword can be used to create both nodes and relationships.
CREATE (self:Company {name:"Baeldung"})
RETURN self
Here we’ve created a company with a single property name. A node definition is marked by parentheses and its properties are enclosed in curly braces. In this case, self is an alias for the node and Company is a node label.
3.2. 创建关系
It is possible to create a node and a relationship to that node all in a single query:
Result result = transaction.execute(
"CREATE (baeldung:Company {name:\"Baeldung\"}) " +
"-[:owns]-> (tesla:Car {make: 'tesla', model: 'modelX'})" +
"RETURN baeldung, tesla");
Here we’ve created nodes baeldung and tesla and established an ownership relationship between them. Creating relationships to pre-existing nodes is, of course, also possible.
3.3. 查询数据
MATCH keyword is used to find data in combination with RETURN to control which data points are returned. The WHERE clause can be utilized to filter out only those nodes that have the properties we desire.
Let us figure out the name of the company that owns tesla modelX:
Result result = transaction.execute(
"MATCH (company:Company)-[:owns]-> (car:Car)" +
"WHERE car.make='tesla' and car.model='modelX'" +
"RETURN company.name");
3.4. 更新节点
SET keyword can be used for updating node properties or labels. Let us add mileage to our tesla:
Result result = transaction.execute("MATCH (car:Car)" +
"WHERE car.make='tesla'" +
" SET car.milage=120" +
" SET car :Car:Electro" +
" SET car.model=NULL" +
" RETURN car");
Here we add a new property called milage, modify labels to be both Car and Electro and finally, we delete model property altogether.
3.5. 删除节点
DELETE keyword can be used for permanent removal of nodes or relationships from the graph:
transaction.execute("MATCH (company:Company)" +
" WHERE company.name='Baeldung'" +
" DELETE company");
Here we deleted a company named Baeldung.
3.6. 参数绑定
In the above examples, we have hard-coded parameter values which isn’t the best practice. Luckily, Neo4j provides a facility for binding variables to a query:
Map<String, Object> params = new HashMap<>();
params.put("name", "baeldung");
params.put("make", "tesla");
params.put("model", "modelS");
Result result = transaction.execute("CREATE (baeldung:Company {name:$name}) " +
"-[:owns]-> (tesla:Car {make: $make, model: $model})" +
"RETURN baeldung, tesla", params);
4. Java Driver
So far we’ve been looking at interacting with an embedded Neo4j instance, however, in all probability for production, we’d want to run a stand-alone server and connect to it via a provided driver. First, we need to add another dependency in our maven pom.xml:
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>5.6.0</version>
</dependency>
You can follow this link to check for the latest version of this driver.
To simulate a production setup we will use the test containers that will start a neo4j server in a docker container. For this we will use the dependency of test containers for neo4j.
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
<exclusion>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
</exclusions>
</dependency>
Now we can establish a connection using a neo4j container:
boolean containerReuseSupported = TestcontainersConfiguration.getInstance().environmentSupportsReuse();
Neo4jContainer neo4jServer = new Neo4jContainer<>(imageName).withReuse(containerReuseSupported);
Driver driver = GraphDatabase.driver(
neo4jServer.getBoltUrl(), AuthTokens.basic("neo4j", "12345"));
Then, create a session:
Session session = driver.session();
Finally, we can run some queries:
session.run("CREATE (baeldung:Company {name:\"Baeldung\"}) " +
"-[:owns]-> (tesla:Car {make: 'tesla', model: 'modelX'})" +
"RETURN baeldung, tesla");
Once we are done with all our work we need to close both session and the driver:
session.close();
driver.close();
5. JDBC Driver
It is also possible to interact with Neo4j via a JDBC driver. Yet another dependency for our pom.xml:
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-jdbc-driver</artifactId>
<version>4.0.9</version>
</dependency>
You can follow this link to download the latest version of this driver.
Next, let’s establish a JDBC connection that will connect to an existing instance of neo4j server which runs in a test conainer as we presented in the previous section:
String uri = "jdbc:neo4j:" + neo4jServer.getBoltUrl() + "/?user=neo4j,password=" + DEFAULT_PASSWORD + ",scheme=basic";
Connection con = DriverManager.getConnection(uri);
Here con is a regular JDBC connection which can be used for creating and executing statements or prepared statements:
try (Statement stmt = con.
stmt.execute("CREATE (baeldung:Company {name:\"Baeldung\"}) "
+ "-[:owns]-> (tesla:Car {make: 'tesla', model: 'modelX'})"
+ "RETURN baeldung, tesla")
ResultSet rs = stmt.executeQuery(
"MATCH (company:Company)-[:owns]-> (car:Car)" +
"WHERE car.make='tesla' and car.model='modelX'" +
"RETURN company.name");
while (rs.next()) {
rs.getString("company.name");
}
}
6. Object-Graph-Mapping
Object-Graph-Mapping or OGM is a technique which enables us to use our domain POJOs as entities in the Neo4j database. Let us examine how this works. The first step, as usually, we add new dependencies to our pom.xml:
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-core</artifactId>
<version>4.0.5</version>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-embedded-driver</artifactId>
<version>3.2.39</version>
</dependency>
You can check the OGM Core Link and OGM Embedded Driver Link to check for the latest versions of these libraries.
Second, we annotate our POJO’s with OGM annotations:
@NodeEntity
public class Company {
private Long id;
private String name;
@Relationship(type="owns")
private Car car;
}
@NodeEntity
public class Car {
private Long id;
private String make;
@Relationship(direction = INCOMING)
private Company company;
}
@NodeEntity informs Neo4j that this object will need to be represented by a node in the resulting graph. @Relationship communicates the need to create a relationship with a node representing the related type. In this case, a company owns a car.
Please note that Neo4j requires each entity to have a primary key, with a field named id being picked up by default. An alternatively named field could be used by annotating it with @Id @GeneratedValue.
Then, we need to create a configuration that will be used to bootstrap Neo4j‘s OGM. For this we will use the test container to simulate a neo4j server:
Configuration.Builder baseConfigurationBuilder = new Configuration.Builder()
.uri(NEO4J_URL)
.verifyConnection(true)
.withCustomProperty(CONFIG_PARAMETER_BOLT_LOGGING, Logging.slf4j())
.credentials("neo4j", Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_PASSWORD)).orElse("").trim());
From the above configuration, we will configure the driver that will be passed to the SessionFactory:
Driver driver = new org.neo4j.ogm.drivers.bolt.driver.BoltDriver();
driver.configure(baseConfigurationBuilder.build());
After that, we initialize SessionFactory with the driver that we created and a package name in which our annotated POJOs reside:
SessionFactory factory = new SessionFactory(getDriver(), "com.baeldung.neo4j.domain");
Finally, we can create a Session and begin using it:
Session session = factory.openSession();
Car tesla = new Car("tesla", "modelS");
Company baeldung = new Company("baeldung");
baeldung.setCar(tesla);
session.save(baeldung);
Here we initiated a session, created our POJO’s and asked OGM session to persist them. Neo4j OGM runtime transparently converted objects to a set of Cypher queries which created appropriate nodes and edges in the database.
If this process seems familiar, that is because it is! That is precisely how JPA works, the only difference being whether object gets translated into rows that are persisted to an RDBMS, or a series of nodes and edges persisted to a graph database.
7. 总结
This article looked at some basics of a graph-oriented database Neo4j.
As always, the code in this write-up is all available over on Github.