├── .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> search(String query) { 49 | if (query == null || query.trim().isEmpty()) return Collections.emptyList(); 50 | try (Connection connection = connect(uri); 51 | PreparedStatement statement = connection.prepareStatement( 52 | "MATCH (movie:Movie)\n" + 53 | " WHERE movie.title =~ $1\n" + 54 | " RETURN movie")) { 55 | statement.setString(1, "(?i).*" + query + ".*"); 56 | List> results = new ArrayList<>(); 57 | try (ResultSet resultSet = statement.executeQuery()) { 58 | while (resultSet.next()) { 59 | Map movie = resultSet.getObject("movie", Map.class); 60 | results.add(Map.of("movie", movie)); 61 | } 62 | } 63 | return results; 64 | } catch (SQLException e) { 65 | throw new RuntimeException("Could not find movie", e); 66 | } 67 | } 68 | 69 | public Map graph(int limit) { 70 | try (Connection connection = connect(uri); 71 | PreparedStatement statement = connection.prepareStatement( 72 | "MATCH (m:Movie)<-[:ACTED_IN]-(a:Person) " + 73 | " RETURN m.title as movie, collect(a.name) as cast " + 74 | " LIMIT $1")) { 75 | 76 | statement.setInt(1, limit); 77 | 78 | try (ResultSet resultSet = statement.executeQuery()) { 79 | List> nodes = new ArrayList<>(); 80 | List> rels = new ArrayList<>(); 81 | int i = 0; 82 | while (resultSet.next()) { 83 | nodes.add(Map.of("title", resultSet.getString("movie"), "label", "movie")); 84 | int target = i; 85 | i++; 86 | for (Object name : (Object[]) resultSet.getArray("cast").getArray()) { 87 | Map actor = Map.of("title", name, "label", "actor"); 88 | int source = nodes.indexOf(actor); 89 | if (source == -1) { 90 | nodes.add(actor); 91 | source = i++; 92 | } 93 | rels.add(Map.of("source", source, "target", target)); 94 | } 95 | } 96 | return Map.of("nodes", nodes, "links", rels); 97 | } 98 | } catch (SQLException e) { 99 | throw new RuntimeException("Could not find cast", e); 100 | } 101 | } 102 | 103 | private Connection connect(String uri) { 104 | try { 105 | return DriverManager.getConnection(uri); 106 | } catch (SQLException e) { 107 | throw new RuntimeException("Could not connect to server", e); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender 2 | log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout 3 | log4j.appender.STDOUT.layout.conversionPattern=%d %-5p - %-26.26c{1} - %m 4 | 5 | # Specific 6 | log4j.logger.spark=DEBUG,STDOUT 7 | log4j.logger.org.neo4j=WARN,STDOUT 8 | log4j.logger.org.eclipse.jetty=WARN,STDOUT 9 | 10 | # log4j.rootLogger=WARN,STDOUT 11 | -------------------------------------------------------------------------------- /src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Neo4j Movies 7 | 8 | 9 | 10 |
11 |
12 | 40 | 41 |
42 |
43 |
44 |
Search Results
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
MovieReleasedTagline
56 |
57 |
58 |
59 |
60 |
Details
61 |
62 |
63 | 64 |
65 |
66 |

Crew

67 |
    68 |
69 |
70 |
71 |
72 |
73 |
74 | 80 | 81 | 82 | 83 | 118 | 119 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=1.7 2 | --------------------------------------------------------------------------------