├── .github
└── workflows
│ └── maven.yml
├── .gitignore
├── Readme.adoc
├── pom.xml
├── src
└── main
│ ├── java
│ └── example
│ │ └── jdbc
│ │ ├── env
│ │ └── Environment.java
│ │ └── movies
│ │ ├── MovieRoutes.java
│ │ ├── MovieServer.java
│ │ └── MovieService.java
│ ├── resources
│ └── log4j.properties
│ └── webapp
│ └── index.html
└── system.properties
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
3 |
4 | name: Java CI with Maven
5 |
6 | on:
7 | push:
8 | branches: [ '**' ]
9 | pull_request:
10 | branches: [ '**' ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up JDK 11
20 | uses: actions/setup-java@v1
21 | with:
22 | java-version: 11
23 | - name: Build with Maven
24 | run: mvn --show-version --batch-mode verify --file pom.xml
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | *.iml
3 | *.ipr
4 | .idea
5 | .classpath
6 | .project
7 |
8 | target
9 | *.iml
10 | *.class
11 | *.jar
12 | *.pyc
13 |
--------------------------------------------------------------------------------
/Readme.adoc:
--------------------------------------------------------------------------------
1 | == Neo4j Movies Example Application
2 |
3 | Even if http://neo4j.org[Neo4j] is all about graphs, its graph query language http://neo4j.org/learn/cypher[Cypher] is well suited to be used with JDBC (Java Database Connectivity).
4 | As you probably know, JDBC is a common way to connect to a datastore, especially since there is a lot of tooling and connectors written around it in the Business Intelligence, Data Migration and ETL world.
5 |
6 | The Neo4j JDBC driver works with Neo4j server in version 1.9.x and 2.x and with embedded and in-memory databases.
7 | It allows you to (transactionally) execute parametrized Cypher statements against your Neo4j database to either create, query or update data.
8 |
9 |
10 | === The Stack
11 |
12 | These are the components of our min- Web Application:
13 |
14 | * Application Type: Java-Web Application
15 | * Web framework: http://www.sparkjava.com/[Spark-Java] (Micro-Webframework)
16 | * Neo4j Database Connector: https://github.com/neo4j-contrib/neo4j-jdbc#minimum-viable-snippet[Neo4j-JDBC] with Cypher
17 | * Database: Neo4j-Server
18 | * Frontend: jquery, bootstrap, http://d3js.org/[d3.js]
19 |
20 | === Endpoints:
21 |
22 | Get Movie
23 |
24 | ----
25 | // JSON object for single movie with cast
26 | curl http://neo4j-movies.herokuapp.com/movie/The%20Matrix
27 |
28 | // list of JSON objects for movie search results
29 | curl http://neo4j-movies.herokuapp.com/search?q=matrix
30 |
31 | // JSON object for whole graph viz (nodes, links - arrays)
32 | curl http://neo4j-movies.herokuapp.com/graph
33 | ----
34 |
35 | === Setup
36 |
37 | Spark is a micro-webframework to easily define routes for endpoints and provide their implementation.
38 | In our case the implementation calls the `MovieService` which has one method per endpoint that returns Java collections
39 | which are turned into JSON using the Google Gson library.
40 |
41 | The `MovieService` uses the Neo4j-JDBC driver to execute queries against the transactional endpoint of Neo4j-Server.
42 | You add the dependency to the JDBC driver in your `pom.xml`:
43 |
44 | [source,xml]
45 | ----
46 | include::pom.xml[tags=jdbc-dependency]
47 | ----
48 |
49 | To use the JDBC driver you provide a connection URL, e.g. `jdbc:neo4j:localhost:7474`, get a `Connection`
50 | which then can be used to create `PreparedStatement`s with your Cypher query.
51 |
52 | You would then set parameters on your statement, please note that only numeric parameter-names are possible, _starting from 1_.
53 |
54 | To access the `ResultSet` you call the cursor method `rs.next()` in a while loop, within which you can access the result-data by `rs.getString("columnName")` where the column name is the `RETURN` clause alias for each column.
55 |
56 | [source,java]
57 | ----
58 | import java.sql.Connection;
59 | import java.sql.DriverManager;
60 | import java.sql.PreparedStatement;
61 | import java.sql.ResultSet;
62 | import java.sql.SQLException;
63 |
64 | public class Example {
65 |
66 | public static void main(String[] args) throws SQLException {
67 |
68 | String query = "MATCH (:Movie {title:{1}})<-[:ACTED_IN]-(a:Person) RETURN a.name as actor";
69 |
70 | try (Connection con = DriverManager.getConnection("jdbc:neo4j://localhost:7474/");
71 | PreparedStatement stmt = con.prepareStatement(query)) {
72 |
73 | stmt.setString(1, "The Matrix");
74 |
75 | try (ResultSet rs = stmt.executeQuery()) {
76 | while(rs.next()) {
77 | System.out.println(rs.getString("actor"));
78 | }
79 | }
80 | }
81 | }
82 | }
83 | ----
84 |
85 | === Run locally:
86 |
87 | Start your local Neo4j Server (http://neo4j.com/download[Download & Install]), open the http://localhost:7474[Neo4j Browser].
88 | Then install the Movies data-set with `:play movies`, click the statement, and hit the triangular "Run" button.
89 |
90 | Start this application with:
91 |
92 | [source,shell]
93 | ----
94 | mvn compile exec:java
95 | ----
96 |
97 | Go to http://localhost:8080
98 |
99 | You can search for movies by title or and click on any entry.
100 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 | 4.0.0
5 | org.neo4j.example
6 | neo4j-movies
7 | 2.0-SNAPSHOT
8 | jar
9 | Neo4j Movies Demo
10 | Neo4j Movies Demo App
11 | https://github.com/neo4j-examples/movies-java-jdbc
12 |
13 |
14 | UTF-8
15 | ${encoding}
16 | ${encoding}
17 | 11
18 | 4.0.8
19 |
20 |
21 |
22 |
23 |
24 | org.neo4j
25 | neo4j-jdbc-bolt
26 | 4.0.1
27 | runtime
28 |
29 |
30 |
31 | org.neo4j
32 | neo4j
33 | ${neo4j.version}
34 |
35 |
36 | com.sparkjava
37 | spark-core
38 | 2.7.2
39 |
40 |
41 | com.google.code.gson
42 | gson
43 | 2.8.6
44 |
45 |
46 |
47 |
48 |
49 |
50 | maven-compiler-plugin
51 | 3.8.1
52 |
53 |
54 | org.codehaus.mojo
55 | exec-maven-plugin
56 | 3.0.0
57 |
58 |
59 |
60 | java
61 |
62 |
63 |
64 |
65 | example.jdbc.movies.MovieServer
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/main/java/example/jdbc/env/Environment.java:
--------------------------------------------------------------------------------
1 | package example.jdbc.env;
2 |
3 | /**
4 | * @author Michael Hunger @since 22.10.13
5 | */
6 | public class Environment {
7 |
8 | public static final int DEFAULT_PORT = 8080;
9 | private static final String DEFAULT_URL = "jdbc:neo4j:bolt://demo.neo4jlabs.com/movies?user=movies&password=movies&database=movies";
10 |
11 | public static int getWebPort() {
12 | String webPort = System.getenv("PORT");
13 | if (webPort == null || webPort.isEmpty()) {
14 | return DEFAULT_PORT;
15 | }
16 | return Integer.parseInt(webPort, 10);
17 | }
18 |
19 | public static String getNeo4jUrl() {
20 | String url = System.getenv("NEO4J_URL");
21 | if (url == null || url.isEmpty()) {
22 | return DEFAULT_URL;
23 | }
24 | return url;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/example/jdbc/movies/MovieRoutes.java:
--------------------------------------------------------------------------------
1 | package example.jdbc.movies;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.GsonBuilder;
5 | import spark.servlet.SparkApplication;
6 |
7 | import java.net.URLDecoder;
8 | import java.nio.charset.StandardCharsets;
9 |
10 | import static spark.Spark.get;
11 |
12 | public class MovieRoutes implements SparkApplication {
13 |
14 | private final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
15 |
16 | private final MovieService service;
17 |
18 | public MovieRoutes(MovieService service) {
19 | this.service = service;
20 | }
21 |
22 | public void init() {
23 | get("/movie/:title", (request, response) ->
24 | gson.toJson(service.findMovie(URLDecoder.decode(request.params("title"), StandardCharsets.UTF_8))));
25 | get("/search", (request, response) ->
26 | gson.toJson(service.search(request.queryParams("q"))));
27 | get("/graph", (request, response) -> {
28 | int limit = request.queryParams("limit") != null ? Integer.parseInt(request.queryParams("limit"), 10) : 100;
29 | return gson.toJson(service.graph(limit));
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/example/jdbc/movies/MovieServer.java:
--------------------------------------------------------------------------------
1 | package example.jdbc.movies;
2 |
3 | import example.jdbc.env.Environment;
4 | import spark.Spark;
5 |
6 | import static spark.Spark.externalStaticFileLocation;
7 |
8 | /**
9 | * @author Michael Hunger @since 22.10.13
10 | */
11 | public class MovieServer {
12 |
13 | public static void main(String[] args) {
14 | Spark.port(Environment.getWebPort());
15 | externalStaticFileLocation("src/main/webapp");
16 | final MovieService service = new MovieService(Environment.getNeo4jUrl());
17 | new MovieRoutes(service).init();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/example/jdbc/movies/MovieService.java:
--------------------------------------------------------------------------------
1 | package example.jdbc.movies;
2 |
3 | import java.sql.Connection;
4 | import java.sql.DriverManager;
5 | import java.sql.PreparedStatement;
6 | import java.sql.ResultSet;
7 | import java.sql.SQLException;
8 | import java.util.ArrayList;
9 | import java.util.Collections;
10 | import java.util.HashMap;
11 | import java.util.List;
12 | import java.util.Map;
13 |
14 | /**
15 | * @author mh
16 | * @since 30.05.12
17 | */
18 | public class MovieService {
19 |
20 | private final String uri;
21 |
22 | public MovieService(String uri) {
23 | this.uri = uri;
24 | }
25 |
26 | public Map findMovie(String title) {
27 | if (title == null) return Collections.emptyMap();
28 | try (Connection connection = connect(uri);
29 | PreparedStatement statement = connection.prepareStatement(
30 | "MATCH (movie:Movie {title:$1})" +
31 | " OPTIONAL MATCH (movie)<-[r]-(person:Person)\n" +
32 | " RETURN movie.title as title, collect({name:person.name, job:head(split(toLower(type(r)),'_')), role:r.roles}) as cast LIMIT 1")) {
33 | statement.setString(1, title);
34 | Map result = new HashMap<>(2);
35 | try (ResultSet resultSet = statement.executeQuery()) {
36 | while (resultSet.next()) {
37 | result.put("title", resultSet.getString("title"));
38 | result.put("cast", resultSet.getArray("cast").getArray());
39 | }
40 | }
41 | return result;
42 | } catch (SQLException e) {
43 | throw new RuntimeException("Could not find movie", e);
44 | }
45 | }
46 |
47 | @SuppressWarnings("unchecked")
48 | public Iterable