added tutorial content to guidebook
This commit is contained in:
9
pom.xml
9
pom.xml
@@ -170,6 +170,11 @@
|
||||
<version>4.4</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!--dependency>
|
||||
<groupId>org.apache.xmlgraphics</groupId>
|
||||
<artifactId>fop</artifactId>
|
||||
<version>0.93</version>
|
||||
</dependency-->
|
||||
<dependency>
|
||||
<groupId>javax.media</groupId>
|
||||
<artifactId>jai-core</artifactId>
|
||||
@@ -183,7 +188,9 @@
|
||||
<foCustomization>${project.basedir}/src/docbkx/resources/xsl/fopdf.xsl</foCustomization>
|
||||
<!-- original spring-build fopdf xsl
|
||||
<foCustomization>src/docbkx/resources/xsl/pdf/fopdf.xsl</foCustomization> -->
|
||||
<htmlStylesheet>css/html.css</htmlStylesheet>
|
||||
<htmlStylesheet>${project.basedir}/src/docbkx/resources/css/html.css</htmlStylesheet>
|
||||
<imgSrcPath>${project.basedir}/src/docbkx/resources/images/</imgSrcPath>
|
||||
<admonGraphicsPath>${project.basedir}/src/docbkx/resources/images/</admonGraphicsPath>
|
||||
<!-- produce single-page html output -->
|
||||
<chunkedOutput>false</chunkedOutput>
|
||||
<htmlCustomization>${project.basedir}/src/docbkx/resources/xsl/html.xsl</htmlCustomization>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<toc/>
|
||||
|
||||
<preface id="foreword:rod">
|
||||
<!--preface id="foreword:rod">
|
||||
<title>Foreword: Rod Johnson, CEO of SpringSource</title>
|
||||
<para>
|
||||
Rod ...
|
||||
@@ -49,7 +49,7 @@
|
||||
<para>
|
||||
Emil ...
|
||||
</para>
|
||||
</preface>
|
||||
</preface-->
|
||||
|
||||
<preface id="about-book">
|
||||
<title>About this book</title>
|
||||
@@ -69,14 +69,21 @@
|
||||
</para>
|
||||
</partintro>
|
||||
|
||||
<xi:include href="tutorial/about-spring-data.xml"/>
|
||||
<xi:include href="tutorial/about-tutorial.xml"/>
|
||||
<xi:include href="tutorial/setup.xml"/>
|
||||
<xi:include href="tutorial/domain.xml"/>
|
||||
<xi:include href="tutorial/neo4j.xml"/>
|
||||
<xi:include href="tutorial/spring-data-graph.xml"/>
|
||||
<xi:include href="tutorial/annotations.xml"/>
|
||||
<xi:include href="tutorial/data-access.xml"/>
|
||||
<xi:include href="tutorial/indexing.xml"/>
|
||||
<xi:include href="tutorial/repository.xml"/>
|
||||
<xi:include href="tutorial/relationships.xml"/>
|
||||
<xi:include href="tutorial/running.xml"/>
|
||||
<xi:include href="tutorial/webapp.xml"/>
|
||||
<xi:include href="tutorial/security.xml"/>
|
||||
<xi:include href="tutorial/social.xml"/>
|
||||
<xi:include href="tutorial/user-experience.xml"/>
|
||||
<xi:include href="tutorial/import.xml"/>
|
||||
<xi:include href="tutorial/recommendations.xml"/>
|
||||
</part>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<para>Executing the application creates the following graph in the Graph Database:</para>
|
||||
<mediaobject>
|
||||
<imageobject>
|
||||
<imagedata fileref="images/helloworlds.png" />
|
||||
<imagedata fileref="helloworlds.png" format="PNG"/>
|
||||
</imageobject>
|
||||
</mediaobject>
|
||||
</section>
|
||||
@@ -41,11 +41,15 @@
|
||||
<para>See the readme file for instruction on how to compile and run the application.</para>
|
||||
<para>An excerpt of the data stored in the Graph Database after executing the application:
|
||||
</para>
|
||||
<mediaobject>
|
||||
<imageobject>
|
||||
<imagedata fileref="images/imdb.png" />
|
||||
</imageobject>
|
||||
</mediaobject>
|
||||
<para>
|
||||
<screenshot>
|
||||
<mediaobject>
|
||||
<imageobject>
|
||||
<imagedata fileref="imdb.png"/>
|
||||
</imageobject>
|
||||
</mediaobject>
|
||||
</screenshot>
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
@@ -58,7 +62,7 @@
|
||||
</para>
|
||||
<mediaobject>
|
||||
<imageobject>
|
||||
<imagedata fileref="images/restaurant.png" />
|
||||
<imagedata fileref="restaurant.png" />
|
||||
</imageobject>
|
||||
</mediaobject>
|
||||
</section>
|
||||
@@ -74,7 +78,7 @@
|
||||
</para>
|
||||
<mediaobject>
|
||||
<imageobject>
|
||||
<imagedata fileref="images/restaurant-social.png" />
|
||||
<imagedata fileref="restaurant-social.png" />
|
||||
</imageobject>
|
||||
</mediaobject>
|
||||
</section>
|
||||
|
||||
BIN
src/docbkx/resources/images/domain.png
Normal file
BIN
src/docbkx/resources/images/domain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:about-spring-data">
|
||||
<title>About Spring Data</title>
|
||||
<para>
|
||||
* What is spring data?
|
||||
* What is spring data graph?
|
||||
* Why should I use it?
|
||||
</para>
|
||||
</chapter>
|
||||
@@ -1,8 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:about-tutorial">
|
||||
<title>About this tutorial</title>
|
||||
<title>Allow me to introduce - Cineasts.net</title>
|
||||
<para>
|
||||
* What is Cineasts.net
|
||||
Once upon a time I wanted to build a social movie database myself. First things first - I had a name: "Cineasts" - the people
|
||||
crazy about movies. So I went ahead and bought the domain, cineasts.net. So, the project was almost done.
|
||||
</para><para>
|
||||
I had some ideas as well. Of course there should be Actors who play Roles in Movies. I needed the Cineast, too, someone
|
||||
had to rate the movies after all. And while they were there, they could also make friends. Find someone to accompany them to the cinema or share
|
||||
movie preferences. Even better, the engine behind all that could recommend new friends and movies to them, derived from their interests and
|
||||
existing friends.
|
||||
</para><para>
|
||||
<mediaobject>
|
||||
<imageobject>
|
||||
<imagedata fileref="cineasts.png"/>
|
||||
</imageobject>
|
||||
</mediaobject>
|
||||
</para><para>I looked for possible sources for data, IMDB was my first stop, but they charge 15k for data usage. Fortunately I found themoviedb.org which has
|
||||
liberal terms and conditions and a nice API for fetching the data.
|
||||
</para><para>
|
||||
There were many more ideas but I wanted to get something done over the course of one day. So this was the scope I was going to tackle. * What is Cineasts.net
|
||||
</para>
|
||||
</chapter>
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:annotations">
|
||||
<title>Annotating the domain model</title>
|
||||
<title>Decorations - Annotated Domain</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
<section>
|
||||
<title>Persistent entities</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Indexing</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Relationships</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
I looked at the documentation again, found a simple Hello-World example and tried to understand it. The entities were annotated with @NodeEntity, that was simple, so
|
||||
I added it too. Relationships got their own annotation named @RelationshipEntity. Property fields should be taken care of automatically.
|
||||
</para><para>
|
||||
Ok lets put this into a test. How to assure that a field was persisted to the graph store? There seemed to be two possibilities. First was to get a
|
||||
GraphDatabaseContext injected and use its getById() method. The other one was a Finder approach which I ignored for new. Lets keep things simple.
|
||||
How to persist an entity and how to get its id? No idea. So further study of the documentation revealed that there were a bunch of methods introduced to the
|
||||
entities by the aspects. That was not obvious. But I found the two that would help me here - entity.persist() and entity.getNodeId().
|
||||
</para><para>
|
||||
So my test looked like this.
|
||||
</para><para>
|
||||
|
||||
<programlisting language="java" ><![CDATA[
|
||||
@Autowired GraphDatabaseContext graphDatabaseContext;
|
||||
|
||||
@Test public void persistedMovieShouldBeRetrievableFromGraphDb() {
|
||||
Movie forestGump = new Movie("Forest Gump", 1994).persist();
|
||||
Movie retrievedMovie = graphDatabaseContext.getById(forestGump.getNodeId());
|
||||
assertEqual("retrieved movie matches persisted one",forestGump,retrievedMovie);
|
||||
assertEqual("retrieved movie title matches","Forest Gump",retrievedMovie.getTitle());
|
||||
}
|
||||
|
||||
]]></programlisting>
|
||||
</para><para>
|
||||
|
||||
That worked, cool. But what about transactions I didn't declare the test to be transactional? After further reading I learned that persist() creates an
|
||||
implicit transaction - so that was like an EntityManager would behave. Ok for me. I also learned that for more complex operations on the entities I needed
|
||||
external transactions.
|
||||
</para>
|
||||
</chapter>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:data-access">
|
||||
<title>Accessing data</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
<section>
|
||||
<title>Importing data</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Repository</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Controllers</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
@@ -1,38 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:domain">
|
||||
<title>The domain</title>
|
||||
<title>Setting the Stage - Movies Domain</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
<section>
|
||||
<title>Movie</title>
|
||||
<para>
|
||||
...
|
||||
The domain model was the next thing I planned to work on. I wanted to flesh it out first before diving into library details. Going along the ideas outlined before I
|
||||
came up with this. I also peeked in the datamodel of my import data source themoviedb to confirm that it matched my expectations.
|
||||
|
||||
<!--
|
||||
http://yuml.me/diagram/scruffy;dir:lr/class/%23%20Cineasts.net%20Domain,%20%5BActor%7Cname;%7CplayedIn()%5D*-ACTS_IN-%3E%5BRole%7Cname;%5D,%20%5BRole%5D-ACTS_IN-%3E*%5BMovie%7Ctitle;year%5D,%20%5BUser%7Clogin;name;password;%7Crate();befriend();%5D*-RATED-%3E%5BRating%7Cstars;comment;%5D,%20%5BRating%5D-RATED-%3E*%5BMovie%5D,%20%5BUser%5D*-FRIEND-%3E*%5BUser%5D.png
|
||||
-->
|
||||
</para><para>
|
||||
<mediaobject>
|
||||
<imageobject>
|
||||
<imagedata fileref="domain.png"/>
|
||||
</imageobject>
|
||||
</mediaobject>
|
||||
|
||||
</para><para>
|
||||
In Java code this looked like. Pretty straightforward.
|
||||
</para><para>
|
||||
|
||||
|
||||
<programlisting language="java" ><![CDATA[
|
||||
class Movie {
|
||||
int id;
|
||||
String title;
|
||||
int year;
|
||||
Set<Role> cast;
|
||||
}
|
||||
|
||||
class Actor {
|
||||
int id;
|
||||
String name;
|
||||
Set<Movie> filmography;
|
||||
Role playedIn(Movie movie, String role);
|
||||
}
|
||||
class Role {
|
||||
Movie movie;
|
||||
Actor actor;
|
||||
String role;
|
||||
}
|
||||
class User {
|
||||
String login;
|
||||
String name;
|
||||
String password;
|
||||
Set<Rating> ratings;
|
||||
Set<User> friends;
|
||||
Rating rate(Movie movie, int stars, String comment);
|
||||
void befriend(User user);
|
||||
}
|
||||
class Rating {
|
||||
User user;
|
||||
Movie movie;
|
||||
int stars;
|
||||
String comment;
|
||||
}
|
||||
]]></programlisting>
|
||||
|
||||
</para><para>
|
||||
I wrote some basic tests to assure that the basic plumbing worked. Check.
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Role</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Actor</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>User</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Rating</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
23
src/docbkx/tutorial/import.xml
Normal file
23
src/docbkx/tutorial/import.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:import">
|
||||
<title>The dusty archives - Importing Data</title>
|
||||
<para>
|
||||
Now it was time to pull the data from themoviedb.org. Registering there and getting an API key was simple, using the API on the commandline with curl too.
|
||||
Looking at the JSON returned for movies and people I decided to pimp my domain model and add some more fields so that the representation in the UI was worth
|
||||
the effort.
|
||||
</para><para>
|
||||
For the import process I created a separate importer that used HttpClient and JSON to fetch and parse the data and then some transactional methods to actually
|
||||
insert it as movies, roles and actors. User data was not available so I created an anonymous user called 'Cineast' that I attributed all the ratings and comments
|
||||
to. I also created a version of the importer that read the json files from local disk, so that I didn't have to strain the remote API that much and that often.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="javascript" ><![CDATA[
|
||||
JSON sample for movie & actor
|
||||
]]></programlisting>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
Code for importer & mapper
|
||||
]]></programlisting>
|
||||
</para>
|
||||
|
||||
</chapter>
|
||||
37
src/docbkx/tutorial/indexing.xml
Normal file
37
src/docbkx/tutorial/indexing.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:indexing">
|
||||
<title>Do I know you? - Indexing</title>
|
||||
<para>
|
||||
Then there was an @Indexed annotation for fields. I wanted to try this too. That would guide the next test. I added an @Indexed to the id field of the movie.
|
||||
This field is intended to represent the external id that will be used in URIs and will stable over database imports and updates. This time I went with the
|
||||
Finder to retrieve my indexed movie.
|
||||
|
||||
</para><para>
|
||||
|
||||
<programlisting language="java" ><![CDATA[
|
||||
@NodeEntity
|
||||
class Movie {
|
||||
@Indexed
|
||||
int id;
|
||||
String title;
|
||||
int year;
|
||||
}
|
||||
|
||||
@Autowired FinderFactory finderFactory;
|
||||
|
||||
@Test public void persistedMovieShouldBeRetrievableFromGraphDb() {
|
||||
int id=1;
|
||||
Movie forestGump = new Movie(id, "Forest Gump", 1994).persist();
|
||||
NodeFinder<Movie> movieFinder = finderFactory.createNodeEntityFinder(Movie.class);
|
||||
Movie retrievedMovie = movieFinder.getByPropertyValue(id);
|
||||
assertEqual("retrieved movie matches persisted one",forestGump,retrievedMovie);
|
||||
assertEqual("retrieved movie title matches","Forest Gump",retrievedMovie.getTitle());
|
||||
}
|
||||
|
||||
]]></programlisting>
|
||||
|
||||
</para><para>
|
||||
TODO This failed with an exception about not being in a transaction. Oh, I forgot to add the @Transactional. So I added it to the test.
|
||||
</para>
|
||||
</chapter>
|
||||
37
src/docbkx/tutorial/neo4j.xml
Normal file
37
src/docbkx/tutorial/neo4j.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:neo4j">
|
||||
<title>Graphs ahead - Learning Neo4j</title>
|
||||
<para>
|
||||
Then came the unknown - how to put these domain objects into the graph. First I read up about graph databases, especially Neo4j. Their datamodel consists
|
||||
of nodes and relationships all of which can have properties. Relationships as first class citizens - I liked that. Then there was the possibility to index
|
||||
both by field, value pairs to quickly get hold of them as starting points for further processing. Other useful operations were manual traversal of relationships
|
||||
and a powerful traversal based on a query like Traversal Description. That all seemed pretty easy.
|
||||
</para><para>
|
||||
I also learned that Neo4j was transactional and provided the known ACID guarantees for my data. This was unsual for a NoSQL database but easier for me to get
|
||||
my head around than non-transactional eventual persistence. That also meant that I had to manage transactions somehow. Keep that in mind.
|
||||
</para><para>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
enum RelationshipTypes implements RelationshipType { ACTS_IN };
|
||||
|
||||
GraphDatabaseService gds = new EmbeddedGraphDatabase("/path/to/store");
|
||||
Node forest=gds.createNode();
|
||||
forest.setProperty("title","Forest Gump");
|
||||
forest.setProperty("year",1994);
|
||||
gds.index().forNodes("movies").add(forest,"id",1);
|
||||
|
||||
Node tom=gds.createNode();
|
||||
tom.setProperty("Tom Hanks");
|
||||
|
||||
Relationship role=tom.createRelationshipTo(forest,ACTS_IN);
|
||||
role.setProperty("role","Forest Gump");
|
||||
|
||||
Node movie=gds.index().forNodes("movies").get("id",1).getSingle();
|
||||
print(movie.getProperty("title"));
|
||||
for (Relationship role : movie.getRelationships(ACTS_IN,INCOMING)) {
|
||||
Node actor=role.getOtherNode(movie);
|
||||
print(actor.getProperty("name") +" as " + role.getProperty("role"));
|
||||
}
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</chapter>
|
||||
@@ -1,8 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:recommendations">
|
||||
<title>Adding recommendations</title>
|
||||
<title>Movies! Friends! Bargains! - Recommendations</title>
|
||||
<para>
|
||||
|
||||
In the last part of this exercise I wanted to add some recommendation algorithms to my app. One was the recommendation of movies that my friends liked very much
|
||||
(and their friends in descending importance). The second was recommendations for new friends that also liked the movies that I liked most.
|
||||
</para><para>
|
||||
Doing this kind of ranking algorithms is the real fun with graph databases. They are applied to the graph by traversing it in a certain order, collecting information
|
||||
on the go and deciding which paths to follow and what to include in the results.
|
||||
</para><para>
|
||||
Lets say I'm only interested in the top 10 recommendations each.
|
||||
</para><para>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
// TODO Work In Progress 1/path.length()*stars
|
||||
user.breathFirst().relationship(FRIEND, OUTGOING).relationship(RATED, OUTGOING).evaluate(new Evaluator(Path path) {
|
||||
if (path.length > 5) return EXCLUDE_AND_STOP;
|
||||
Relationship rating = path.lastRelationship();
|
||||
if (rating.getType().equals(RATED)) {
|
||||
rating.getProperty()
|
||||
return INCLUDE_AND_STOP;
|
||||
}
|
||||
return INCLUDE_AND_CONTINUE;
|
||||
})
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</chapter>
|
||||
|
||||
107
src/docbkx/tutorial/relationships.xml
Normal file
107
src/docbkx/tutorial/relationships.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:relationships">
|
||||
<title>A convincing act - Relationships</title>
|
||||
<section>
|
||||
<title>Value in Relationships - Creating them</title>
|
||||
<para>
|
||||
Next were relationships. Direct relationships didn't require any annotation. Unfortunately I had none of those. So I went for the Role relationship
|
||||
between Movie and Actor. It had to be annotated with @RelationshipEntity and the @StartNode and @EndNode had to be marked.
|
||||
So my Role looked like this:
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java"><![CDATA[
|
||||
@RelationshipEntity
|
||||
class Role {
|
||||
@EndNode
|
||||
Movie movie;
|
||||
@StartNode
|
||||
Actor actor;
|
||||
String role;
|
||||
}
|
||||
]]></programlisting>
|
||||
</para><para>
|
||||
When writing a test for that I tried to create the relationship entity with new, but got an exception saying that this was not allowed. Some weird restriction
|
||||
about having only correctly constructed RelationshipEntities. So I remembered a relateTo method from the list of introduced methods on the NodeEntities. After
|
||||
quickly checking it turned out to be exactly what I needed. I added the method for connecting movies and actors to the actor - seemed more natural.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
public Role playedIn(Movie movie, String roleName) {
|
||||
Role role = relateTo(movie, Role.class, "ACTS_IN");
|
||||
role.setRole(roleName);
|
||||
return role;
|
||||
}
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Who's there ? - Accessing related entities</title>
|
||||
<para>
|
||||
What was left - accessing those relationships. I already had the appropriate fields in both classes. Time to annotate them correctly. For the fields providing
|
||||
access to the entities on the other side of the relationship this was straightforward. Providing the target type again (thanks to Java's type erasure) and the
|
||||
relationship type (that I learned from the Neo4j lesson before) there was only the direction left. Which defaults to OUTGOING so only for the movie I had to
|
||||
specify it.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
@NodeEntity
|
||||
class Movie {
|
||||
@Indexed
|
||||
int id;
|
||||
String title;
|
||||
int year;
|
||||
@RelatedTo(elementClass = Actor.class, type = "ACTS_IN", direction = Direction.INCOMING)
|
||||
Set<Actor> cast;
|
||||
}
|
||||
|
||||
@NodeEntity
|
||||
class Actor {
|
||||
@Indexed
|
||||
int id;
|
||||
String name;
|
||||
@RelatedTo(elementClass = Movie.class, type = "ACTS_IN")
|
||||
Set<Movie> cast;
|
||||
|
||||
public Role playedIn(Movie movie, String roleName) {
|
||||
Role role = relateTo(movie, Role.class, "ACTS_IN");
|
||||
role.setRole(roleName);
|
||||
return role;
|
||||
}
|
||||
}
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>May I introduce ? - Accessing Relationships themselves</title>
|
||||
<para>
|
||||
While reading about those relationship-sets I learned that they are handled by managed collections of spring data graph. So whenever I add something to the
|
||||
set or remove it, it automatically reflects that in the underlying relationships. Neat. But this also meant I mustn't initialize the fields. Something I will
|
||||
certainly forget not to do in the future, so watch out for it.
|
||||
</para><para>
|
||||
I didn't forget to add test for those. So I could assure that the collections worked as advertised (and also ran into the intialization problem above).
|
||||
</para><para>
|
||||
But I still couldn't access the Role relationships. There was more to read about this. For accessing the relationship in between the nodes there was a separate
|
||||
annotation @RelatedToVia. And I had to declare the field as readonly Iterable<Role>. That should make sure that I never tried to add Roles (which I couldn't create on my own anyway)
|
||||
to this field. Otherwise the annotation attributes were similar to those used for @RelatedTo. So off I went, creating my first real relationship (just kidding).
|
||||
</para><para>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
@NodeEntity
|
||||
class Movie {
|
||||
@Indexed
|
||||
int id;
|
||||
String title;
|
||||
int year;
|
||||
@RelatedTo(elementClass = Actor.class, type = "ACTS_IN", direction = Direction.INCOMING)
|
||||
Set<Actor> cast;
|
||||
|
||||
@RelatedToVia(elementClass = Role.class, type = "ACTS_IN", direction = Direction.INCOMING)
|
||||
Iterable<Roles> roles;
|
||||
}
|
||||
]]></programlisting>
|
||||
</para><para>
|
||||
After the tests proved that those relationship fields really mirrored the underlying relationships in the graph and instantly reflected additions and removals I was
|
||||
satisfied with my domain so far and went for some coffee and chocolate.
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
26
src/docbkx/tutorial/repository.xml
Normal file
26
src/docbkx/tutorial/repository.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:repository">
|
||||
<title>Serving a good cause - Repository</title>
|
||||
<para>
|
||||
That was the first method to add to the repository. So I created a repository for my application, annotated
|
||||
it with @Repository and @Transactional.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java"><![CDATA[
|
||||
@Repository @Transactional
|
||||
public class CineastsRepostory {
|
||||
FinderFactory finderFactory;
|
||||
Finder<Movie> movieFinder;
|
||||
@Autowired
|
||||
public CineastsRepostory(FinderFactory finderFactory) {
|
||||
this.finderFactory = finderFactory;
|
||||
this.movieFinder = finderFactory.createNodeEntityFinder(Movie.class);
|
||||
}
|
||||
public Movie getMovie(int id) {
|
||||
return movieFinder.getById(id);
|
||||
}
|
||||
}
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</chapter>
|
||||
55
src/docbkx/tutorial/running.xml
Normal file
55
src/docbkx/tutorial/running.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:running">
|
||||
<title>Curtains Up! - Get it running</title>
|
||||
<section>
|
||||
<title>Requisites - Populating the database</title>
|
||||
<para>
|
||||
Time to put this on display. But I needed some test data first. So I wrote a small class for populating the database which could be called from my controller.
|
||||
To make it safe to call it several times I added index lookups to check for existing entries. A simple /populate endpoint for the controller that called it would
|
||||
be enough for now.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java"><![CDATA[
|
||||
TODO code
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Behind the scenes - Peeking at the Datastore</title>
|
||||
|
||||
<section>
|
||||
<title>Eye candy - Neoclipse visualization</title>
|
||||
<para>
|
||||
After filling the database I wanted to see what the graph looked like. So I checked out two tools that are available for inspecting the graph. First Neoclipse, an
|
||||
eclipse RCP application or plugin that connects to existing graph stores and visualizes their content. After getting an exception about concurrent access, I learned
|
||||
that I have to use Neoclipse in readonly mode when my webapp had an active connection to the store. Good to know.
|
||||
</para><para>
|
||||
TODO neoclipse image
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Hardcore "Hacking" - Neo4j Shell</title>
|
||||
<para>
|
||||
Besides my movies and actors connected by ACTS_IN relationships there were some other nodes. The reference node which is kind of a root node in Neo4j and can be used
|
||||
to anchor subgraphs for easier access. And Spring Data Graph also represented the type hierarchy of my entities in the graph. Obviously for some internal housekeeping
|
||||
and type checking.
|
||||
</para><para>
|
||||
For us console junkies there is also a shell that can reach into a running neo4j store (if that one was started with enableRemoteShell) or provide readonly access
|
||||
to a graph store directory.
|
||||
</para><para>
|
||||
|
||||
<programlisting language="shell" ><![CDATA[
|
||||
neo4j-shell -readonly -path /path/to/my/graphdb
|
||||
]]></programlisting>
|
||||
</para><para>
|
||||
It uses some shell metaphors like cd and ls to navigate the graph. There are also more advanced commands like using indexes and traversals. I tried to play around with them
|
||||
in this shell sesson.
|
||||
</para><para>
|
||||
<programlisting language="shell" ><![CDATA[
|
||||
TODO shell session
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
</chapter>
|
||||
@@ -1,8 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:security">
|
||||
<title>Adding security</title>
|
||||
<title>Protecting Assets - Adding Security</title>
|
||||
<para>
|
||||
To use the user in the webapp I had to put it in the session and add login and registration pages. Of course the pages that only worked with a valid user
|
||||
account had to be secured as well.
|
||||
</para><para>
|
||||
I used Spring Security to that, writing a simple UserDetailsService that used my repository for looking up the users and validating their credentials. The config is located
|
||||
in a separate applicationContext-security.xml.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="xml" ><![CDATA[
|
||||
<security:global-method-security secured-annotations="enabled">
|
||||
</security:global-method-security>
|
||||
|
||||
<security:http auto-config="true" access-denied-page="/auth/denied"> <!-- use-expressions="true" -->
|
||||
<security:intercept-url pattern="/admin/*" access="ROLE_ADMIN"/>
|
||||
<security:intercept-url pattern="/import/*" access="ROLE_ADMIN"/>
|
||||
<security:intercept-url pattern="/user/*" access="ROLE_USER"/>
|
||||
<security:intercept-url pattern="/auth/login" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
|
||||
<security:intercept-url pattern="/auth/register" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
|
||||
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
|
||||
<security:form-login login-page="/auth/login" authentication-failure-url="/auth/login?login_error=true"
|
||||
default-target-url="/user"/>
|
||||
<security:logout logout-url="/auth/logout" logout-success-url="/" invalidate-session="true"/>
|
||||
</security:http>
|
||||
|
||||
<security:authentication-manager>
|
||||
<security:authentication-provider user-service-ref="userDetailsService">
|
||||
<security:password-encoder hash="md5">
|
||||
<security:salt-source system-wide="cewuiqwzie"/>
|
||||
</security:password-encoder>
|
||||
</security:authentication-provider>
|
||||
</security:authentication-manager>
|
||||
|
||||
<bean id="userDetailsService" class="org.neo4j.movies.service.CineastsUserDetailsService"/> ]]></programlisting>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
@Service
|
||||
public class CineastsUserDetailsService implements UserDetailsService, InitializingBean {
|
||||
|
||||
@Autowired private FinderFactory finderFactory;
|
||||
private NodeFinder<User> userFinder;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
userFinder = finderFactory.createNodeEntityFinder(User.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException, DataAccessException {
|
||||
final User user = findUser(login);
|
||||
if (user==null) throw new UsernameNotFoundException("Username not found",login);
|
||||
return new CineastsUserDetails(user);
|
||||
}
|
||||
|
||||
public User findUser(String login) {
|
||||
return userFinder.findByPropertyValue("users","login",login);
|
||||
}
|
||||
}
|
||||
|
||||
public class CineastsUserDetails implements UserDetails {
|
||||
private final User user;
|
||||
|
||||
public CineastsUserDetails(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<GrantedAuthority> getAuthorities() {
|
||||
User.Roles[] roles = user.getRoles();
|
||||
if (roles ==null) return Collections.emptyList();
|
||||
return Arrays.<GrantedAuthority>asList(roles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return user.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return user.getLogin();
|
||||
}
|
||||
|
||||
....
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
]]></programlisting>
|
||||
</para>
|
||||
<para>
|
||||
After that a logged in user was available in the session and could so be used for all the social interactions. Most of the work done next was adding controller methods
|
||||
and JSPs for the views.
|
||||
</para>
|
||||
</chapter>
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:setup">
|
||||
<title>Project setup</title>
|
||||
<title>Scope: Spring</title>
|
||||
<para>
|
||||
...
|
||||
Being a Spring Developer, I would, of course, choose components of the Spring Framework to do most of the work. I'd already come up with the ideas -
|
||||
that should be enough.
|
||||
</para><para>
|
||||
What database would fit both the complex network of cineasts, movies, actors, roles, ratings and friends? And also be able to support the
|
||||
recommendation algorithms that I thought of? I had no idea.
|
||||
</para><para>
|
||||
But, wait, there was the new Spring Data project that started in 2010 bringing
|
||||
the convenience of the Spring programming model to NoSQL databases. That should fit my experience and help me getting started. I looked
|
||||
at the list of projects supporting the different NoSQL databases. Only one mentioned the kind of social network I was thinking of -
|
||||
Spring Data Graph for Neo4j, a graph database. Neo4j's pitch of "value in relationships" and the accompanying docs looked like what I needed.
|
||||
I decided to give it a try.
|
||||
</para>
|
||||
<section>
|
||||
<title>Spring Data Graph</title>
|
||||
<title>Preparations - Required Setup</title>
|
||||
<para>
|
||||
...
|
||||
To setup the project I created a public github account and began setting up the infrastructure for a spring web project using maven as build
|
||||
system. So I added the dependencies for the springframework libraries, put the web.xml for the DispatcherServlet and the applicationContext.xml
|
||||
in the webapp directory.
|
||||
</para><para>
|
||||
|
||||
|
||||
<programlisting language="xml" ><![CDATA[
|
||||
TODO setup code?
|
||||
]]></programlisting>
|
||||
|
||||
</para><para>
|
||||
With this setup I was ready for the first spike: creating a simple MovieController showing a static view. Check. Next was the setup for Spring Data Graph.
|
||||
I looked at the README at github and then checked it with the manual. Quite a lot of maven setup for aspectj but otherwise not so much to add.
|
||||
I added just a few lines to my spring configuration.
|
||||
</para><para>
|
||||
|
||||
|
||||
<programlisting language="xml" ><![CDATA[
|
||||
TODO config code?
|
||||
]]></programlisting>
|
||||
|
||||
</para><para>
|
||||
I spun up jetty to see if there were any obvious issues with the config. Check.
|
||||
</para>
|
||||
<section>
|
||||
<title>Maven configuration</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Spring configuration</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
<section>
|
||||
<title>Spring MVC</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
<section>
|
||||
<title>Maven configuration</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Spring configuration</title>
|
||||
<para>
|
||||
...
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
@@ -1,8 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:social">
|
||||
<title>Adding social</title>
|
||||
<title>Movies 2.0 - Adding social</title>
|
||||
<para>
|
||||
...
|
||||
But this was just a plain old movie database (POMD). My idea of socializing this business was not realized.
|
||||
</para>
|
||||
<section>
|
||||
<title>See, mom a Cineast! - Users</title>
|
||||
<para>
|
||||
So I took the User class that I already coded up before and made it a full fledged Spring Data Graph member.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
@NodeEntity
|
||||
class User {
|
||||
@Indexed
|
||||
String login;
|
||||
String name;
|
||||
String password;
|
||||
@RelatedTo(elementClass=Movie.class, type="RATED")
|
||||
Set<Rating> ratings;
|
||||
|
||||
@RelatedTo(elementClass=User.class, type="FRIEND")
|
||||
Set<User> friends;
|
||||
|
||||
public Rating rate(Movie movie, int stars, String comment) {
|
||||
return relateTo(movie, Rating.class, "RATED").rate(stars, comment);
|
||||
}
|
||||
public void befriend(User user) {
|
||||
this.friends.add(user);
|
||||
}
|
||||
}
|
||||
class Rating {
|
||||
@StartNode User user;
|
||||
@EndNode Movie movie;
|
||||
int stars;
|
||||
String comment;
|
||||
public Rating rate(int stars, String comment) {
|
||||
this.stars=stars; this.comment = comment;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
]]></programlisting>
|
||||
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Beware, Critics - Rating</title>
|
||||
<para>
|
||||
I also put a ratings field into the movie to be able to show its ratings. And a method to average the stars it got.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
class Movie {
|
||||
@RelatedToVia(elementClass=Rating.class, type="RATED", direction = Direction.INCOMING)
|
||||
Iterable<Rating> ratings;
|
||||
|
||||
public int getStars() {
|
||||
int stars, int count;
|
||||
for (Rating rating : ratings) {
|
||||
stars += rating.getStars(); count++;
|
||||
}
|
||||
return count == 0 ? 0 : stars / count;
|
||||
}
|
||||
}
|
||||
]]></programlisting>
|
||||
</para>
|
||||
<para>
|
||||
Fortunately my tests showed my the division by zero error when calculating the stars for a movie without ratings. I also added a few user and ratings to the
|
||||
database population code. And three methods to rate movies, lookup users and add friends to the repository.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java" ><![CDATA[
|
||||
TODO code
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
10
src/docbkx/tutorial/spring-data-graph.xml
Normal file
10
src/docbkx/tutorial/spring-data-graph.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:about-spring-data">
|
||||
<title>Conjuring Magic - Spring Data Graph</title>
|
||||
<para>
|
||||
But that was the pure graph database. Using this in my domain would pollute my classes with lots of graph database details. I didn't want that. Spring Data Graph
|
||||
promised to do the heavy lifting for me. So I checked that next. Obviously it heavily depended on aspectj magic. So there would be certain behavour that was
|
||||
just observable without being visible in my code. But I was going to give it a try.
|
||||
</para>
|
||||
</chapter>
|
||||
9
src/docbkx/tutorial/user-experience.xml
Normal file
9
src/docbkx/tutorial/user-experience.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:user-experience">
|
||||
<title>Oh the Glamour - More UI</title>
|
||||
<para>
|
||||
TODO screenshots
|
||||
</para>
|
||||
|
||||
</chapter>
|
||||
49
src/docbkx/tutorial/webapp.xml
Normal file
49
src/docbkx/tutorial/webapp.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN" "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
|
||||
<chapter id="tutorial:webapp">
|
||||
<title>Showing off - Web views</title>
|
||||
<para>
|
||||
After I had the means to put some data in the graph database, I also wanted to show it. So adding the controller method to show a single movie with its attributes
|
||||
and cast in a jsp was straightforward. Actually just using the repository to look the movie up and add it to the model. Then forward to the /movies/show view and voilá.
|
||||
</para><para>
|
||||
TODO screenshot of movie display
|
||||
</para>
|
||||
|
||||
<section>
|
||||
<title>What was his name? - Searching</title>
|
||||
<para>
|
||||
The next thing was to allow users to search for some movies. So I needed some fulltext-search capabilities. As the index provider implementation of Neo4j builds on
|
||||
lucene I was delighted to see that fulltext indexes are supported out of the box.
|
||||
</para>
|
||||
<para>
|
||||
So I happily annotated the title field of my Movie class with @Index(fulltext=true) and was told with an exception that I have to specify a separate index name for that.
|
||||
So it became @Indexed(fulltext = true, indexName = "search"). The corresponding finder method is called findAllByQuery. So there was my second repository method for
|
||||
searching movies. To restrict the size of the returned set I just added a limit for now that cuts the result after that many entries.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="java"><![CDATA[
|
||||
public void List<Movie> searchForMovie(String query, int count) {
|
||||
List<Movie> movies=new ArrayList<Movie>(count);
|
||||
for (Movie movie : movieFinder.findAllByQuery("title", query)) {
|
||||
movies.add(movie);
|
||||
if (count-- == 0) break;
|
||||
}
|
||||
return movies;
|
||||
}
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Look what i've found - Listing Results</title>
|
||||
|
||||
<para>
|
||||
I then used this result in the controller to render a list of movies driven by a search box. The movie properties and the cast was accessed by the getters in the
|
||||
domain classes.
|
||||
</para>
|
||||
<para>
|
||||
<programlisting language="jsp" ><![CDATA[
|
||||
TODO jsp fragment
|
||||
]]></programlisting>
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
Reference in New Issue
Block a user