2022年1月25日 星期二

使用 Java 讀取 Neo4j 的查詢結果

Neo4j 是一套原生實現 (底層設計就是為了圖資料庫設計,而不是用例如一般關聯式資料庫去模擬) 圖資料庫(Graph Database) 的工具

Neo4j Community Edition 版本可以到 Neo4j 官網的下載中心 免費下載使用,
除了有啟動 Neo4j server 的功能外還提供了以網頁存取的方便介面 。

下載 Neo4j Community Edition 後,把下載下來的 zip 檔解壓縮,
進入到資料夾裡的 bin 資料夾,用命令列模式 (command line)
打上

neo4j consle

指令後,可以用瀏覽器到 http://localhost:7474/browser/ 看到 UI 介面,提供各種功能,例如執行語法及可視化結果,預設登入 Database 的帳號密碼會都是 "neo4j" ,可以自行修改。

這邊紀錄下使用 Java 去讀取 Neo4j 查詢結果的方法,首先來看下需要的依賴 Maven Dependency:

<!-- https://mvnrepository.com/artifact/org.neo4j.driver/neo4j-java-driver -->
	<dependency>
	    <groupId>org.neo4j.driver</groupId>
	    <artifactId>neo4j-java-driver</artifactId>
	    <version>4.4.2</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.neo4j/neo4j-jdbc-driver -->
	<dependency>
	    <groupId>org.neo4j</groupId>
	    <artifactId>neo4j-jdbc-driver</artifactId>
	    <version>4.0.4</version>
	    <scope>runtime</scope>
	</dependency>
    
   <!-- https://mvnrepository.com/artifact/org.neo4j/neo4j -->
	<!-- neo4j embedded version -->
	<dependency>
	    <groupId>org.neo4j</groupId>
	    <artifactId>neo4j</artifactId>
	    <version>4.4.2</version>
	</dependency>
    
以上三個 neo4j 的 dependency 可以擇一使用,
可依你想要存取 neo4j 的方式來選擇,分述如下:
  1. org.neo4j.driver 的 neo4j-java-driver :
    使用 Driver 的方式來存取 neo4j ,有較接近 neo4j 原生結構的類別可操作使用,
    需要跟已開啟的 neo4j server 做連線,使用像例如
    neo4j://localhost:7687
    這樣的方式來操縱 neo4j 資料庫。
  2. org.neo4j 的 neo4j-jdbc-driver :
    使用 JDBC 的方式來存 neo4j ,
    需要跟已開啟的 neo4j server 做連線,使用像例如
    jdbc:neo4j:bolt://localhost:7687?user=xxx,password=xxx,scheme=basic
    的方式操縱 neo4j 資料庫。
    跟 neo4j-java-driver 比起來,neo4j-jdbc-driver 沒有接近 neo4j 原生結構的類別可操作使用,
    只能使用 jdbc 的 (Map) ResultSet.getObject() 方式等存取 
  3. org.neo4j 的 neo4j :
    使用 嵌入式(embedded) 的方式來存取本地端的 neo4j 資料庫檔案,
    可以直接處理本地端 neo4j 資料庫檔案 (通常為一個資料夾),
    不需開啟 neo4j server 做連線,適合用在無 server 的環境,
    有較接近 neo4j 原生結構的類別可操作使用,
    例如可直接對整個 neo4j-community-4.4.2 資料夾及指定 Database 名稱來做存取。
接下來我們先來建立一個簡單的 neo4j 資料庫內容,
內容為一個英雄人物曾當過哪些英雄的關係圖,像是這個樣子:
Bruce Wayne -[hasBeenHero] -> Batman
Dick Grayson -[hasBeenHero] -> Batman
Dick Grayson -[hasBeenHero] -> Nightwing
Dick Grayson -[hasBeenHero] -> Robin

建構資料的語法如下
//create "Persion" nodes
MERGE (bruceWayne:Person {name: 'Bruce Wayne'})
MERGE (dickGrayson:Person {name: 'Dick Grayson'})
//create "Hero" nodes
WITH bruceWayne,
	 dickGrayson, 
	 [
	 	{name: 'Batman'},
	 	{name: 'Nightwing'},
	 	{name: 'Robin'}
	 ] AS heros
//create and set relatoinship
FOREACH (hero in heros | 
    CREATE (h:Hero) SET h = hero
    CREATE (dickGrayson)-[:hasBeenHero]->(h)
)
WITH bruceWayne
MATCH (batman:Hero{name:'Batman'})
CREATE (bruceWayne)-[:hasBeenHero]->(batman)
用視覺化來看的話資料庫結果會如下圖:

接下來我們來看看要如何用 Java 把資料庫的 Node 和 Relationship 都查出來,
以下直接上 Java 程式碼,實現了三個 method ,分別對應了上述的三種存取 neo4j Database 的方式,分別是:
queryByDriver() 對應 org.neo4j.driver 的 neo4j-java-driver
queryByJdbc() 對應 org.neo4j 的 neo4j-jdbc-driver
queryByEmbeddedMode() 對應 org.neo4j 的 neo4j

 Neo4jTest.java :
package main;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.neo4j.dbms.api.DatabaseManagementService;
import org.neo4j.dbms.api.DatabaseManagementServiceBuilder;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import org.neo4j.driver.Result;
import org.neo4j.driver.Session;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Relationship;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Transaction;
import org.neo4j.kernel.impl.core.NodeEntity;
import org.neo4j.kernel.impl.core.RelationshipEntity;

public class Neo4jTest {
	
	public static void main(String... args) throws Exception {
		String databaseName = "neo4j";
		String userName = "neo4j";
		String password = "neo4j";
		String neo4jServerUrl = "localhost:7687";
		Path neo4jDBHomeDirectoryPath = Paths.get(ClassLoader.getSystemResource("neo4j-community-4.4.2").toURI());
		//or if the database directory is located on other path:
		//Path neo4jDBHomeDirectoryPath = Paths.get("D:\\xxx\\yyy\\neo4j-community-4.4.2");
		
		queryByDriver("neo4j://" + neo4jServerUrl, userName, password);
		queryByJdbc(neo4jServerUrl, userName, password);
		queryByEmbeddedMode(neo4jDBHomeDirectoryPath, databaseName);
		
		System.out.println("Done");
	}
	
	public static void queryByDriver(String uri, String username, String password) {		
		
		try (Driver driver = GraphDatabase.driver(uri, AuthTokens.basic(username, password));
				Session session = driver.session();) {
			
			List<String> dataList = session.readTransaction(tx -> {
				List<String> resultList = new ArrayList<String>();
				
				Result result = tx.run("MATCH (person:Person)-[relationship:hasBeenHero]->(hero:Hero) "
									 + "RETURN * "
									 + "ORDER BY person.name, hero.name");
				
				while(result.hasNext()) {					
					Record record = result.next();
					Node person = record.get("person").asNode();
					Relationship relationship = record.get("relationship").asRelationship();
					Node hero = record.get("hero").asNode();
					
					String resultStr = person.get("name").asString() + " -" + relationship.type() + "-> " + hero.get("name").asString();
//					System.out.println(resultStr); // who -hasBeenHero-> whatHero
					resultList.add(resultStr);
				}
				
				return resultList;
			});
			
			for (String data : dataList) {
				System.out.println(data);
				/* output:
				   Bruce Wayne -hasBeenHero-> Batman
				   Dick Grayson -hasBeenHero-> Batman
				   Dick Grayson -hasBeenHero-> Nightwing
				   Dick Grayson -hasBeenHero-> Robin
				*/
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void queryByJdbc(String uri, String username, String password) {		
		try (Connection con = DriverManager.getConnection("jdbc:neo4j:bolt://" + uri + "?user=" + username + ",password=" + password +",scheme=basic");
				PreparedStatement pstmt = con.prepareStatement("MATCH (person:Person)-[relationship:hasBeenHero]->(hero:Hero) "
															 + "RETURN * "
															 + "ORDER BY person.name, hero.name");
				ResultSet rs = pstmt.executeQuery();
				){
			
			while(rs.next()) {
				Map person = (Map) rs.getObject("person");				
				Map relationship = (Map) rs.getObject("relationship");
				Map hero = (Map) rs.getObject("hero");
				String resultStr = person.get("name") + " -" + relationship.get("_type") + "-> " + hero.get("name");
				System.out.println(resultStr);
				/* output:
				   Bruce Wayne -hasBeenHero-> Batman
				   Dick Grayson -hasBeenHero-> Batman
				   Dick Grayson -hasBeenHero-> Nightwing
				   Dick Grayson -hasBeenHero-> Robin
				*/				
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	// Don't need to start server manually, it will start server by itself.
	// Don't need username and password.
	public static void queryByEmbeddedMode(Path neo4jDBHomeDirectoryPath, String databaseName) {
		DatabaseManagementService databaseManagementService = new DatabaseManagementServiceBuilder(neo4jDBHomeDirectoryPath).build();
		GraphDatabaseService graphDatabaseService = databaseManagementService.database(databaseName);
		
		// Registers a shutdown hook for the Neo4j instance so that it
	    // shuts down nicely when the VM exits (even if you "Ctrl-C" the
	    // running application).
//	    Runtime.getRuntime().addShutdownHook( new Thread()
//	    {
//	        @Override
//	        public void run()
//	        {
//	        	databaseManagementService.shutdown();
//	        }
//	    } );
	    
	    try(Transaction tx = graphDatabaseService.beginTx();){
	    	org.neo4j.graphdb.Result result = tx.execute("MATCH (person:Person)-[relationship:hasBeenHero]->(hero:Hero) "
	    											   + "RETURN * "
	    											   + "ORDER BY person.name, hero.name");
			
			while(result.hasNext()) {				
				Map<String,Object> record = result.next();
				NodeEntity person = (NodeEntity) record.get("person");
				RelationshipEntity relationship = (RelationshipEntity) record.get("relationship");
				NodeEntity hero = (NodeEntity) record.get("hero");
				String resultStr = person.getProperty("name") + " -" + relationship.getType().name() + "-> " + hero.getProperty("name");
				System.out.println(resultStr);
				/* output:
				   Bruce Wayne -hasBeenHero-> Batman
				   Dick Grayson -hasBeenHero-> Batman
				   Dick Grayson -hasBeenHero-> Nightwing
				   Dick Grayson -hasBeenHero-> Robin
				*/
			}
	    }catch(Exception e) {
	    	e.printStackTrace();
	    }
	    
		databaseManagementService.shutdown();
	}
}
可以注意到的是,只有 queryByDriver() 和 queryByJdbc() 需要提供帳號及密碼,並且需要連接一個已經啟動的 neo4j Database server。
而 queryByEmbeddedMode() 不需帳號、密碼,也不需要一個已經啟動的 neo4j Database server,
它會直接對本地端的 neo4j Database folder 做存取,因為它會自己啟動 DB server ,
所以不要用例如上述的 neo4j console 指令啟動 server,不然可能會出現
Exception in thread "main" java.lang.RuntimeException: Error starting Neo4j database server at D:\xxx\yyy\neo4j-community-4.4.2\data\databases
的錯誤訊息。

原碼下載分享:

2022年1月3日 星期一

Java 物件序列化 (Object Serialize/Deserializ)

Java 可以使用序列化/反序列化的技術將物件實體 (Object Instance) 
轉成位元組格式資料 (通常用 byte array 表示) 再轉回來,
方便我們將物件實體保持起來等之後將其還原回物件實體,
或將其轉文字等用 http request 送給其他 server 並在server 端接收並還原。

Note:

物件必須要實作 Serializable 介面才能被序列化。
這篇文紀錄下 Java  如何物件的序列化(Serialize)/反序列化(Deserialize),
以下程式碼展示了兩個範例,
分別為將物件以檔案的方式
及以文字的方式進行序列化/反序列化。

而不管是將物件序列化成何種型式(例如檔案或文字),
概念都是一樣的,就是以物件被序列化成位元組格式
及將位元組反序列化回物件。

需要注意的是,如果想將物件序列化成文字 (String) 的話,
因為位元組轉成文字後,有可能會因為編碼等問題轉不回原來的位元組資料,
這時可先將位元組資料用例如 Base64 編碼得到字串來當做序列化後的字串保存起來,
之後要反序列化回物件時,把字串用 Base64 解碼成位元組格式,
再將位元組格式的資料反序列化回物件即可。

以下為程式碼範例 (jdk-11):

先建一個簡單的測試用 Class, Person.java:
Person.java:
package myTest;

import java.io.Serializable;

public class Person implements Serializable{
	private static final long serialVersionUID = 1L;
	
	private String name;
	
	public Person(String name) {
		this.name = name;
	}
	
	public void sayHello() {
		System.out.println("Hello, I'm " + name + ".");
	}
}

再來是序列化/反序列化的程式碼:
TestObjectSerialize.java:
package myTest;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class TestObjectSerialize {

	public static void main(String[] args) {
		Person person = new Person("Hugo");
		person.sayHello(); //Hello, I'm Hugo.
		
		writeObjectToFile(person, "D:\\MyClass");
		Person deSerializedObject = readObjectFromFile(Person.class, "D:\\MyClass");
		deSerializedObject.sayHello(); //Hello, I'm Hugo.
		
		String serializedObjectString = writeObjectToString(person);
		Person deSerializedObject2 = readObjectFromString(Person.class, serializedObjectString);
		deSerializedObject2.sayHello(); //Hello, I'm Hugo.
		
		System.out.println("Done");
	}
	
	//----- Serialize/Desrialize Object through file -----//
	public static <T> void writeObjectToFile(T object, String filePath){
		try (FileOutputStream fileOutputStream = new FileOutputStream(filePath);
			 ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);){
			
			objectOutputStream.writeObject(object);
		}catch(IOException e) {
			e.printStackTrace();
		}
	}
	
	public static <T> T readObjectFromFile(Class<T> clazz, String filePath){
		T deserializedObject = null;
		
		try (
				FileInputStream fileInputStream = new FileInputStream(filePath);
				ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
			){
			
			deserializedObject = (T) objectInputStream.readObject();
		}catch(IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}
		
		return deserializedObject;
	}
	
	//----- Serialize/Desrialize Object through String -----//
	//Because Object can be serilaized to String
	//, the String can stored in anywhere or be transfer to other server 
	//and be deserialized to original Object from String.
	//Notice: After serializing Object to Byte Array, you should use some way like Base64Encoding to
	//encode the Byte Array to String,
	//because the String you got directly from Byte Array might not be transfered to original Byte Array.
	public static <T> String writeObjectToString(T object){
		String serializedObjectString = "";

		try (
				ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
				ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
			){
			
			objectOutputStream.writeObject(object);
			serializedObjectString = new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()), "UTF-8");
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		
		return serializedObjectString;
	}
	
	public static <T> T readObjectFromString(Class<T> clazz, String serializedObjectString){
		T deserializedObject = null;
		
		try (
				ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(serializedObjectString.getBytes("UTF-8")));
				ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
			){
			
			deserializedObject = (T) objectInputStream.readObject();
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}
		
		return deserializedObject;
	}

}