Total Pageviews

2018/09/14

[Neo4j] Using Spring Data to create / delete nodes and relationship

假設我想透過 Spring Data API 建立起以下的 graph:


架構規劃如下:

pom.xml 內容:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>neo4j</groupId>
 <artifactId>springdata</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>springdata</name>
 <url>http://maven.apache.org</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <java.version>1.8</java.version>
  <maven.compiler.source>1.8</maven.compiler.source>
  <maven.compiler.target>1.8</maven.compiler.target>
 </properties>

 <dependencies>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-neo4j</artifactId>
   <version>2.0.0.RELEASE</version>
  </dependency>

  <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>1.16.18</version>
  </dependency>

  <dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>23.0</version>
  </dependency>

  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>3.8.1</version>
   <scope>test</scope>
  </dependency>
 </dependencies>

 <repositories>
  <repository>
   <id>spring-libs-release</id>
   <name>Spring Releases</name>
   <url>https://repo.spring.io/libs-release</url>
   <snapshots>
    <enabled>false</enabled>
   </snapshots>
  </repository>
 </repositories>
</project>

Value objects - Brand
package neo4j.springdata.vo;

import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@NodeEntity
@Data
@ToString
@NoArgsConstructor
@RequiredArgsConstructor
public class Brand {

    @Id
    @GeneratedValue
    private Long id;

    @NonNull
    private String name;

}

Value objects - Product
package neo4j.springdata.vo;

import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@NodeEntity
@Data
@ToString
@NoArgsConstructor
@RequiredArgsConstructor
public class Product {
    
    @Id
    @GeneratedValue
    private Long id;

    @NonNull
    private String name;
    
    @Relationship(type = "MADE_BY")
    private Brand madeBy;
    
}

Repository class - BrandRepository
package neo4j.springdata.repository;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import neo4j.springdata.vo.Brand;

@Repository
public interface BrandRepository extends CrudRepository<Brand, Long>{

    @Query("match(b:Brand) where b.name = {name} return b")
    Brand findByName(@Param("name") String name);
    
    @Query("match(n:Brand) optional match (n)-[r]-() delete r, n")
    void deleteAllBrands();
    
}

Repository class - ProductRepository
package neo4j.springdata.repository;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import neo4j.springdata.vo.Product;

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {

    @Query("match (n:Product) optional match (n)-[r]-() delete r, n")
    void deleteAllProducts();
    
}

Service class - BrandService
package neo4j.springdata.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import neo4j.springdata.repository.BrandRepository;
import neo4j.springdata.vo.Brand;

@Service
public class BrandService {

    @Autowired
    private BrandRepository brandRepo;
    
    public void createBrands(List<Brand> brands) {
        brandRepo.saveAll(brands);
    }
    
    public void deleteAllBrands() {
        brandRepo.deleteAllBrands();
    }

}

Service class - ProductService
package neo4j.springdata.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import neo4j.springdata.repository.BrandRepository;
import neo4j.springdata.repository.ProductRepository;
import neo4j.springdata.vo.Brand;
import neo4j.springdata.vo.Product;

@Service
public class ProductService {

    @Autowired
    private BrandRepository brandRepo;
    
    @Autowired
    private ProductRepository productRepo;
    
    public void createProducts(List<Product> products, String brandName) {
        Brand brand = brandRepo.findByName(brandName);
        products.forEach(p -> p.setMadeBy(brand));
        
        productRepo.saveAll(products);
    }
    
    public void deleteAllProducts() {
        productRepo.deleteAllProducts();
    }
    
}

Client class - App
package neo4j.springdata;

import java.util.List;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;

import com.google.common.collect.Lists;

import neo4j.springdata.service.BrandService;
import neo4j.springdata.service.ProductService;
import neo4j.springdata.vo.Brand;
import neo4j.springdata.vo.Product;

@SpringBootApplication
@EnableNeo4jRepositories
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    CommandLineRunner createBrands(BrandService brandService) {
        return args -> {
            List<Brand> brands = Lists.newArrayList(new Brand("Apple"), new Brand("Google"), new Brand("Samsung"));
            brandService.createBrands(brands);
        };
    }

    @Bean
    CommandLineRunner createProducts(ProductService productService) {
        return args -> {
            List<Product> appleProducts = Lists.newArrayList(new Product("iPhne"), new Product("iPad"),
                    new Product("iPad Pro"), new Product("MacBook"), new Product("MackBook Pro"));
            productService.createProducts(appleProducts, "Apple");

            List<Product> googleProducts = Lists.newArrayList(new Product("Nexus 6P"), new Product("Nexus 5X"),
                    new Product("Chromecast"));
            productService.createProducts(googleProducts, "Google");

            List<Product> samsungProducts = Lists.newArrayList(new Product("Samsung Galaxy Note 8"),
                    new Product("Samsung Galaxy Note FE"), new Product("Samsung Galaxy S9"),
                    new Product("Samsung Galaxy A8"));
            productService.createProducts(samsungProducts, "Samsung");
        };
    }

    @Bean
    CommandLineRunner deleteProductAndBrands(BrandService brandService, ProductService productService) {
        return args -> {
            brandService.deleteAllBrands();
            productService.deleteAllProducts();
        };
    }

}


application.properties (記錄連線資訊)
spring.data.neo4j.uri=bolt://localhost
spring.data.neo4j.username=neo4j
spring.data.neo4j.password=secret


2018/09/13

[Neo4j] Forgot password to log into Neo4j server

Problem
If I forgot my password to log into Neo4j server, how to reset it?

How-To


Screenshot for Step1 (Find auth file in your neo4j directory)

Screenshot for Step3 (Restart neo4j server)

Screenshot for Step4 (Open neo4j browser and login using default credentials, neo4j/neo4j )


Screenshot for Step5 (Enter new password)


2018/09/12

[Neo4j] How to find nodes and its relationships from existing graph

目前的 graph 如下



假設我要找出大雄與其他人之間的關係,Cypher 語法如下:
match (m) where m.name="大雄"
match (m)-[r]-(n)
with m, n, type(r) as relationships
return m.name, n.name, relationships


於 Neo4j Browser 執行結果:

2018/09/11

[Neo4j] How to delete relationship/node from existing graph

目前的關係圖如下:

假設我要刪除大雄與小衫之間的情敵關係,Cypher 語法如下:
// 找出大雄與小杉的 nodes
match (m) where m.name="大雄"
match (n) where n.name="小杉"

// 找出兩人間糾葛的情敵關係
match (n)-[r:RIVAL_OF]->(m)

// 刪除情敵關係
delete r

於 Neo4j Browser 執行結果:


更新後的 graph (大雄與小衫之間的情敵關係已刪除)


若我想要刪除小衫 (要先刪除小衫與他人的關係以後,才能刪除小衫),Cypher 語法如下:

// 找出小杉的 nodes
match (n) where n.name="小杉"

// 找出小衫與他人的關係
OPTIONAL MATCH (n)-[r]-() 

// 刪除小衫以及小衫與他人的關係
delete r, n


於 Neo4j Browser 執行結果:



更新後的 graph (小衫已刪除)

2018/09/10

[Neo4j] How to create nodes / relationships to existing graph

目前的關係圖如下:


若我想要:
1. 建立一位新人物,叫做小衫
2. 建立一個新關係,靜香與小衫是朋友關係
3. 再建立一個新關係,大雄與小衫是情敵關係

Cypher 語法如下:
// 建立小衫
create(p:Person {id:"9", name:"小杉", gender:"男性"})
with p

// 找出靜香與大雄的 nodes
match (n) where n.name="靜香"
match (m) where m.name="大雄"

// 建立新關係:靜香與小衫是朋友關係
create (p)-[:FRIEND_OF]->(n)

// 建立新關係:大雄與小衫是情敵關係
create (p)-[:RIVAL_OF]->(m)

於 Neo4j Browser 執行結果:




若我想要找出最新的 graph

Cypher 語法如下:
// 找出所有關聯圖
match (all) return all

於 Neo4j Browser 執行結果:


2018/09/09

[Neo4j] Importing using Load CSV

假設我想要在 neo4j 建立起下圖的關係

由關係圖可以看出會有七個節點,包含
  • 哆啦A夢
  • 大雄
  • 靜香
  • 小夫
  • 胖虎
  • 小叮鈴
  • 玉子 (大雄的媽媽)
  • 伸助 (大雄的爸爸)

這七個人 (nodes) 之間的有九個關係:

  • 大雄的寵物是哆啦A夢
  • 大雄的配偶是靜香
  • 大雄的朋友是小夫
  • 大雄的朋友是胖虎
  • 小夫的朋友是胖虎
  • 哆啦A夢的妹妹是小叮鈴
  • 大雄的爸爸是伸助
  • 大雄的媽媽是玉子
  • 伸助的配偶是玉子

分析與執行步驟如下:


步驟如下:
(1) Prepare CSV file for nodes with UTF-8 encoding and store in neo4j-community-3.3.3\import

(2) Prepare CSV file for relationships with UTF-8 encoding and store in neo4j-community-3.3.3\import

(3) Prepare"load csv" command to load nodes and execute from neo4j browser
using periodic commit 
load csv with headers from
"file:///nodes.csv"
as nodes
create(:Person {id:nodes.node, name:nodes.name, gender:nodes.gender})

產生節點如下:


(4) Prepare"load csv" command to create relationships and execute from neo4j browser
using periodic commit 
load csv with headers from
"file:///relationships.csv"
as line
match (from:Person), (to:Person) where from.id=line.From and to.id=line.To 
create (from)-[:REL {type: line.`Relation Type`}]->(to)

建立關係後如下:


由於,我們期待的圖形是下圖,故會執行步驟 5, 6 來做關係的 rename

(5) Execute additional "cleanup" step for Labels and RelTypes
根據步驟 4 所建立的關係,依據關係的屬性,加以rename,重複建立一條有關係
//Create duplicate relationship with appropriate type
MATCH (n1)-[r1:REL{type:'SPOUSE_OF'}]->(m1),
      (n2)-[r2:REL{type:'PET_OF'}]->(m2),
      (n3)-[r3:REL{type:'FRIEND_OF'}]->(m3),
      (n4)-[r4:REL{type:'SISTER_OF'}]->(m4),
      (n5)-[r5:REL{type:'MOTHER_OF'}]->(m5),
      (n6)-[r6:REL{type:'FATHER_OF'}]->(m6)
merge (n1)-[:SPOUSE_OF]->(m1)
merge (n2)-[:PET_OF]->(m2)
merge (n3)-[:FRIEND_OF]->(m3)
merge (n4)-[:SISTER_OF]->(m4)
merge (n5)-[:MOTHER_OF]->(m5)
merge (n6)-[:FATHER_OF]->(m6)

建立新的關係後,圖形如下:

(6) Remove duplicate relationships
此步驟將會清理多餘的關係,將原本所建立的 REL 關係予以刪除
//Remove duplicate relationships
match ()-[r:REL]-() delete r;

刪除後,圖形如下:


2018/09/08

[PostgreSQL] org.postgresql.util.PSQLException: ERROR: operator does not exist: text = bytea

Problem
When I try to execute the following select SQL statement:
1
2
3
4
5
  select case when (domain_projects <> :domain_projects) then true else false end as result
  from project
  where domain_classifier = true
  and id = :projectId
  and domain_projects is not null

I get this error message:
  org.postgresql.util.PSQLException: ERROR: operator does not exist: text = bytea
  Hint: No operator matches the given name and argument type(s). 
  You might need to add explicit type casts.


How-To
The problem result from the first line:
1
2
3
4
5
  select case when (domain_projects <> :domain_projects) then true else false end as result
  from project
  where domain_classifier = true
  and id = :projectId
  and domain_projects is not null


I need to cast the parameter to specific data type as per instructions. Therefore, the SQL statement should be modified as bellows:
1
2
3
4
5
6
  select
  case when (domain_projects <> cast(:domain_projects as text)) then true else false end as result
  from project
  where domain_classifier = true
  and id = :projectId
  and domain_projects is not null  


2018/09/07

[Spring MVC] Upload ZIP file to server

Scenario

The file structure in ZIP file looks like:


How-To
Here has sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
    @Transactional
    @PostMapping(value = "/{projectId}/importFile/{fileType}", produces = MediaType.APPLICATION_JSON_VALUE)
    public void importFile(@PathVariable Integer projectId, @PathVariable String fileType,
            @RequestParam(value = "file", required = true) MultipartFile file) throws IOException {

        Project project = repo.findOne(projectId);

        File tmpFile = null;
        DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String currentTime = dateFormat.format(new Date());
        try {
            tmpFile = File.createTempFile(file.getName() + "_" + currentTime, ".zip");
            tmpFile.deleteOnExit();
        } catch (IOException e) {
            throw new RuntimeException("建立 temp file 發生錯誤, 錯誤原因: " + e.getMessage(), e);
        }

        InputStream inputStream = null;
        ZipFile zipFile = null;
        try {
            inputStream = file.getInputStream();
            byte[] buffer = new byte[inputStream.available()];
            inputStream.read(buffer);

            Files.write(buffer, tmpFile);

            zipFile = new ZipFile(tmpFile);

            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();

                saveDomainJSON(project, entry, zipFile);
                saveStoryMD(project, entry, zipFile);
            }

            repo.updatePersistModelToNull(projectId);
        } catch (IOException e) {
            throw new RuntimeException("讀取 zip file 發生錯誤, 錯誤原因: " + e.getMessage(), e);
        } finally {
            if (zipFile != null) {
                zipFile.close();
            }
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }    
 
    private void saveDomainJSON(Project project, ZipEntry entry, ZipFile zipFile) throws IOException {
        if (entry.getName().startsWith("domain")) {
            try (InputStream inputSteam = zipFile.getInputStream(entry);) {
                String content = IOUtils.toString(inputSteam, org.apache.commons.io.Charsets.UTF_8);
                // save content into database
            } catch (IOException e) {
                throw new RuntimeException("讀取 domain JSON file 發生錯誤, 錯誤原因: " + e.getMessage(), e);
            }
        }  
    }
 
    private void saveStoryMD(Project project, ZipEntry entry, ZipFile zipFile) throws IOException {
        if (entry.getName().startsWith("story")) {
            try (InputStream inputSteam = zipFile.getInputStream(entry);) {
                String content = IOUtils.toString(inputSteam);
                // save content into database
            } catch (IOException e) {
                throw new RuntimeException("讀取 story Markdown file 發生錯誤, 錯誤原因: " + e.getMessage(), e);
            }
        }  
    }




2018/09/06

[Spring MVC] Download ZIP file from server

Scenario


How-To
Sample code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    @PostMapping(value = "/{projectId}/downloadZIP", produces = "application/zip")
    public FileSystemResource downloadZIP(@PathVariable Integer projectId, Alerter alerter) throws Exception {
        String identifier = repo.getOne(projectId).getIdentifier();

        DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String currentTime = dateFormat.format(new Date());

        String fileName = identifier + "-" + currentTime;
        String domainJSON = projectService.getDomainJSON(projectId);
        String storyMD = projectService.getStoryMD(projectId);

        File tempFile = File.createTempFile(fileName, ".zip");
        tempFile.deleteOnExit();

        try (OutputStream fos = new FileOutputStream(tempFile);
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             ZipOutputStream zos = new ZipOutputStream(bos);) {
            
            zos.putNextEntry(new ZipEntry("domain-" + currentTime + ".json"));
            zos.write(domainJSON.getBytes());
            zos.closeEntry();

            zos.putNextEntry(new ZipEntry("story-" + currentTime + ".md"));
            zos.write(storyMD.getBytes());
            zos.closeEntry();

        } catch (IOException e) {
            throw new RuntimeException("檔案匯出失敗, 錯誤原因:" + e.getMessage(), e);
        }

        return new FileSystemResource(tempFile);
    }