1. 介绍

介绍如何使用Neo4j的插件,目前支持neo4j 3.5.x以上版本

目前支持neo4j与hibernate混用模式,用户角色等还是使用关系型数据库存储,而需要使用neo4j特性的部分使用neo4j库.

2. 使用

gradle中增加依赖库。

    implementation('org.yunchen.gb:gb-plugin-neo4j:1.4.0.0')
    implementation('org.grails:grails-datastore-gorm-neo4j:8.1.0')

在application.yml文件中加入如下的配置

grails:
  neo4j:
    url: bolt://localhost:7687
    username: neo4j
    password: neo4j123456

3. neo4j 配置

3.1. server端

使用docker 运行neo4j server

docker pull neo4j:3.5.8
docker run --publish=7473:7473 --publish=7474:7474 --publish=7687:7687  neo4j

访问http://localhost:7474 ,使用neo4j/neo4j 登录服务器后,首次访问会提示修改密码,将密码改为 neo4j123456

4. 规约

4.1. domain设置

需要在domain类中设置静态属性mapWith

static mapWith = "neo4j"

例子:(映射Node)

import grails.neo4j.Node
...

@Entity
@Title(zh_CN = "个人信息表")
@JsonIgnoreProperties(["errors", "metaClass", "dirty", "attached", "dirtyPropertyNames","handler","target","session","entityPersisters","hibernateLazyInitializer","initialized","proxyKey","children"])
class Person  implements Node<Person>{    (1)
    static mapWith = "neo4j"              (2)
    String name;
    String sex;
    static hasMany = [friends: Person]       (3)
    static constraints = {
        name(nullable: false,blank:false,size:0..100)
        sex(nullable: false,blank:false,size:0..10)
    }
    static mapping = {
    }
}
1 标记domain类实现Node这个trait
2 标记domain类使用neo4j映射
3 标记这个有好友这个关系

4.2. 插入数据

如下在一个事务空间,插入node数据和关系

    private void initPersonWithNeo4j(){
        Person.withNewTransaction {
            Person joe = Person.findByName('Joe')
            if(!joe){
                joe = new Person(name: "Joe",sex:'male').save(flush:true)
            }
            Person barney = Person.findByName('Barney')
            if(!barney){
                barney =new Person(name: "Barney",sex:'female').save(flush:true)
            }
            barney.addToFriends(joe)
            Person fred = Person.findByName('Fred')
            if(!fred){
                fred = new Person(name: "Fred",sex:'male').save(flush:true)
            }
            fred.addToFriends(barney)
            //查询node间的关系
            Path<Person, Person> path = Person.findShortestPath(fred, joe, 15)
            for(Path.Segment<Person, Person> segment in path) {
                println segment.start().name
                println segment.end().name
            }
        }
    }
在neo4j可控制台输入如下命令删除所有数据 match (n) detach delete n

4.3. 查询数据

在domain类上,扩展出findShortestPath等方法

            //查询node间的关系
            Path<Person, Person> path = Person.findShortestPath(fred, joe, 15)
            for(Path.Segment<Person, Person> segment in path) {
                println segment.start().name
                println segment.end().name
            }

5. 其他示例

映射 Releationship

5.1. Person

@Entity
@Title(zh_CN = "个人信息表")
@JsonIgnoreProperties(["errors", "metaClass", "dirty", "attached", "dirtyPropertyNames","handler","target","session","entityPersisters","hibernateLazyInitializer","initialized","proxyKey","children"])
class Person  implements Node<Person>{
    static mapWith = "neo4j"
    String name
    String sex
    static hasMany = [friends: Person,appearances:CastMember]
    static constraints = {
        name(nullable: false,blank:false,size:0..100)
        sex(nullable: false,blank:false,size:0..10)
    }
    static mapping = {
    }
}

5.2. Movie

@Entity
@Title(zh_CN = "电影表")
@JsonIgnoreProperties(["errors", "metaClass", "dirty", "attached", "dirtyPropertyNames","handler","target","session","entityPersisters","hibernateLazyInitializer","initialized","proxyKey","children"])
class Movie implements Node<Movie> {
    static mapWith = "neo4j"
    String title
    static hasMany = [cast:CastMember]
}

5.3. CastMember

@Entity
@Title(zh_CN = "演职员表")
@JsonIgnoreProperties(["errors", "metaClass", "dirty", "attached", "dirtyPropertyNames","handler","target","session","entityPersisters","hibernateLazyInitializer","initialized","proxyKey","children"])
class CastMember  implements Relationship<Person, Movie> {
    static mapWith = "neo4j"
    List<String> roles = []
}

5.4. 查询操作

        def person=Person.findByName('Keanu')
        if(!person){
            person=new Person(name:'Keanu',sex:'male').save(flush:true)
        }
        def movie=Movie.findByTitle('The Matrix')
        if(!movie){
            movie=new Movie(title:'The Matrix').save(flush:true)
        }

        def castMember = new CastMember(
                type: "ACTED_IN", // "DIRECTED",
                from: person,
                to: movie,
                roles: ["Neo"])

        castMember['realName'] = "Thomas Anderson"
        castMember.save(flush:true)

        List keanuCastings = CastMember.where {
            from.name == "Keanu"
        }.list()
        keanuCastings.each {
            println it.from.name + " played " + it.roles + " in " + it.to.title
        }
默认项目在hibernate的transaction中,neo4j的写操作会报错"org.springframework.transaction.NoTransactionException: Cannot flush write operations without an active transaction!" 解决办法是在neo4j的事务下操作如:
FilmPerson.withNewTransaction {
    .....
    //do something with neo4j write operations
}

6. 经典电影示例

neo4j 默认携带的电影示例

6.1. 初始化数据

默认neo4j提供cypher语句进行数据初始化,提供了对象的初始化语句如下:

        Movie.withNewTransaction {
            //TheMatrix
            Movie matrix1= new Movie(title:'The Matrix', released:1999, tagline:'Welcome to the Real World').save(flush:true)
            Person Keanu =new Person(name:'Keanu Reeves', born:1964).save(flush:true)
            Person Carrie =new Person(name:'Carrie-Anne Moss', born:1967).save(flush:true)
            Person Laurence =new Person(name:'Laurence Fishburne',born:1961).save(flush:true)
            Person Hugo =new Person(name:'Hugo Weaving', born:1960).save(flush:true)
            Person LillyW =new Person(name:'Lilly Wachowski', born:1967).save(flush:true)
            Person LanaW =new Person(name:'Lana Wachowski', born:1965).save(flush:true)
            Person JoelS =new Person(name:'Joel Silver', born:1952).save(flush:true)

            new CastMember(from:Keanu, to: matrix1, roles: ["Neo"]).save(flush:true)
            new CastMember(from:Carrie, to: matrix1, roles: ["Trinity"]).save(flush:true)
            new CastMember(from:Laurence, to: matrix1, roles: ["Morpheus"]).save(flush:true)
            new CastMember(from:Hugo, to: matrix1, roles: ["Agent Smith"]).save(flush:true)

            new CastMember(from:LillyW, to: matrix1,type: CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:LanaW, to: matrix1,type: CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:JoelS, to: matrix1,type: CastMember.RoleType.PRODUCED).save(flush:true)
            //neo4j cto 自己加戏
            //Person Emil =new Person(name:"Emil Eifrem", born:1978).save(flush:true)
            //new CastMember(from:Emil, to: matrix1, roles: ["Emil"]).save(flush:true)

            //TheMatrixReloaded
            Movie matrix2= new Movie(title:'The Matrix Reloaded', released:2003, tagline:'Free your mind').save(flush:true)
            new CastMember(from:Keanu, to: matrix2, roles: ["Neo"]).save(flush:true)
            new CastMember(from:Carrie, to: matrix2, roles: ["Trinity"]).save(flush:true)
            new CastMember(from:Laurence, to: matrix2, roles: ["Morpheus"]).save(flush:true)
            new CastMember(from:Hugo, to: matrix2, roles: ["Agent Smith"]).save(flush:true)

            new CastMember(from:LillyW, to: matrix2,type: CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:LanaW, to: matrix2,type: CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:JoelS, to: matrix2,type: CastMember.RoleType.PRODUCED).save(flush:true)
            //TheMatrixRevolutions
            Movie matrix3= new Movie(title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end').save(flush:true)
            new CastMember(from:Keanu, to: matrix3, roles: ["Neo"]).save(flush:true)
            new CastMember(from:Carrie, to: matrix3, roles: ["Trinity"]).save(flush:true)
            new CastMember(from:Laurence, to: matrix3, roles: ["Morpheus"]).save(flush:true)
            new CastMember(from:Hugo, to: matrix3, roles: ["Agent Smith"]).save(flush:true)

            new CastMember(from:LillyW, to: matrix3,type: CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:LanaW, to: matrix3,type: CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:JoelS, to: matrix3,type: CastMember.RoleType.PRODUCED).save(flush:true)

            //theDevilsAdvocate
            Movie theDevilsAdvocate= new Movie(title:"The Devil's Advocate", released:1997, tagline:'Evil has its winning ways').save(flush:true)
            Person Charlize =new Person(name:'Charlize Theron', born:1975).save(flush:true)
            Person Al =new Person(name:'Al Pacino', born:1940).save(flush:true)
            Person Taylor =new Person(name:'Taylor Hackford', born:1944).save(flush:true)

            new CastMember(from:Keanu, to: theDevilsAdvocate, roles: ["Kevin Lomax" ]).save(flush:true)
            new CastMember(from:Charlize, to: theDevilsAdvocate, roles: ["Mary Ann Lomax" ]).save(flush:true)
            new CastMember(from:Al, to: theDevilsAdvocate, roles: ["John Milton" ]).save(flush:true)
            new CastMember(from:Taylor, to: theDevilsAdvocate, type: CastMember.RoleType.DIRECTED).save(flush:true)

            //AFewGoodMen
            Movie AFewGoodMen= new Movie(title:"A Few Good Men", released:1992, tagline:"In the heart of the nation's capital, in a courthouse of the U.S. government, one man will stop at nothing to keep his honor, and one will stop at nothing to find the truth.").save(flush:true)
            Person TomC =new Person(name:'Tom Cruise', born:1962).save(flush:true)
            Person JackN =new Person(name:'Jack Nicholson', born:1937).save(flush:true)
            Person DemiM =new Person(name:'Demi Moore', born:1962).save(flush:true)
            Person KevinB =new Person(name:'Kevin Bacon', born:1958).save(flush:true)
            Person KieferS =new Person(name:'Kiefer Sutherland', born:1966).save(flush:true)
            Person NoahW =new Person(name:'Noah Wyle', born:1971).save(flush:true)
            Person CubaG =new Person(name:'Cuba Gooding Jr.', born:1968).save(flush:true)
            Person KevinP =new Person(name:'Kevin Pollak', born:1957).save(flush:true)
            Person JTW =new Person(name:'J.T. Walsh', born:1943).save(flush:true)
            Person JamesM =new Person(name:'James Marshall', born:1967).save(flush:true)
            Person ChristopherG =new Person(name:'Christopher Guest', born:1948).save(flush:true)
            Person RobR =new Person(name:'Rob Reiner', born:1947).save(flush:true)
            Person AaronS =new Person(name:'Aaron Sorkin', born:1961).save(flush:true)

            new CastMember(from:TomC, to: AFewGoodMen, roles: ["Lt. Daniel Kaffee" ]).save(flush:true)
            new CastMember(from:JackN, to: AFewGoodMen, roles: ["Col. Nathan R. Jessup" ]).save(flush:true)
            new CastMember(from:DemiM, to: AFewGoodMen, roles: ["Lt. Cdr. JoAnne Galloway" ]).save(flush:true)
            new CastMember(from:KevinB, to: AFewGoodMen, roles: ["Capt. Jack Ross" ]).save(flush:true)
            new CastMember(from:KieferS, to: AFewGoodMen, roles: ["Lt. Jonathan Kendrick" ]).save(flush:true)
            new CastMember(from:NoahW, to: AFewGoodMen, roles: ["Cpl. Jeffrey Barnes" ]).save(flush:true)
            new CastMember(from:CubaG, to: AFewGoodMen, roles: ["Cpl. Carl Hammaker" ]).save(flush:true)
            new CastMember(from:KevinP, to: AFewGoodMen, roles: ["Lt. Sam Weinberg" ]).save(flush:true)
            new CastMember(from:JTW, to: AFewGoodMen, roles: ["Lt. Col. Matthew Andrew Markinson" ]).save(flush:true)
            new CastMember(from:JamesM, to: AFewGoodMen, roles: ["Pfc. Louden Downey" ]).save(flush:true)
            new CastMember(from:ChristopherG, to: AFewGoodMen, roles: ["Dr. Stone" ]).save(flush:true)
            new CastMember(from:AaronS, to: AFewGoodMen, roles: ["Man in Bar" ]).save(flush:true)
            new CastMember(from:RobR, to: AFewGoodMen,type: CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:AaronS, to: AFewGoodMen, type: CastMember.RoleType.WROTE).save(flush:true)

            //Top Gun
            Movie TopGun= new Movie(title:"Top Gun", released:1986, tagline:"I feel the need, the need for speed.").save(flush:true)
            Person KellyM =new Person(name:'Kelly McGillis', born:1957).save(flush:true)
            Person ValK =new Person(name:'Val Kilmer', born:1959).save(flush:true)
            Person AnthonyE =new Person(name:'Anthony Edwards', born:1962).save(flush:true)
            Person TomS =new Person(name:'Tom Skerritt', born:1933).save(flush:true)
            Person MegR =new Person(name:'Meg Ryan', born:1961).save(flush:true)
            Person TonyS =new Person(name:'Tony Scott', born:1944).save(flush:true)
            Person JimC =new Person(name:'Jim Cash', born:1941).save(flush:true)

            new CastMember(from:TomC, to: TopGun, roles: ["Maverick" ]).save(flush:true)
            new CastMember(from:KellyM, to: TopGun, roles: ["Charlie" ]).save(flush:true)
            new CastMember(from:ValK, to: TopGun, roles: ["Iceman" ]).save(flush:true)
            new CastMember(from:AnthonyE, to: TopGun, roles: ["Goose" ]).save(flush:true)
            new CastMember(from:TomS, to: TopGun, roles: ["Viper" ]).save(flush:true)
            new CastMember(from:MegR, to: TopGun, roles: ["Carole" ]).save(flush:true)
            new CastMember(from:TonyS, to: TopGun, type:CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:JimC, to: TopGun, type:CastMember.RoleType.WROTE).save(flush:true)

            //Jerry Maguire
            Movie JerryMaguire= new Movie(title:'Jerry Maguire', released:2000, tagline:'The rest of his life begins now.').save(flush:true)
            Person ReneeZ =new Person(name:'Renee Zellweger', born:1969).save(flush:true)
            Person KellyP =new Person(name:'Kelly Preston', born:1962).save(flush:true)
            Person JerryO =new Person(name:"Jerry O'Connell", born:1974).save(flush:true)
            Person JayM =new Person(name:'Jay Mohr', born:1970).save(flush:true)
            Person BonnieH =new Person(name:'Bonnie Hunt', born:1961).save(flush:true)
            Person ReginaK =new Person(name:'Regina King', born:1971).save(flush:true)
            Person JonathanL =new Person(name:'Jonathan Lipnicki', born:1996).save(flush:true)
            Person CameronC =new Person(name:'Cameron Crowe', born:1957).save(flush:true)

            new CastMember(from:TomC, to: JerryMaguire, roles: ["Jerry Maguire" ]).save(flush:true)
            new CastMember(from:CubaG, to: JerryMaguire, roles: ["Rod Tidwell" ]).save(flush:true)
            new CastMember(from:ReneeZ, to: JerryMaguire, roles: ["Dorothy Boyd" ]).save(flush:true)
            new CastMember(from:KellyP, to: JerryMaguire, roles: ["Avery Bishop" ]).save(flush:true)
            new CastMember(from:JerryO, to: JerryMaguire, roles: ["Frank Cushman" ]).save(flush:true)
            new CastMember(from:JayM, to: JerryMaguire, roles: ["Bob Sugar" ]).save(flush:true)
            new CastMember(from:BonnieH, to: JerryMaguire, roles: ["Laurel Boyd" ]).save(flush:true)
            new CastMember(from:ReginaK, to: JerryMaguire, roles: ["Marcee Tidwell" ]).save(flush:true)
            new CastMember(from:JonathanL, to: JerryMaguire, roles: ["Ray Boyd" ]).save(flush:true)
            new CastMember(from:CameronC, to: JerryMaguire, type: CastMember.RoleType.PRODUCED).save(flush:true)
            new CastMember(from:CameronC, to: JerryMaguire, type: CastMember.RoleType.DIRECTED).save(flush:true)
            new CastMember(from:CameronC, to: JerryMaguire, type: CastMember.RoleType.WROTE).save(flush:true)

            //Stand By Me
            Movie StandByMe= new Movie(title:"Stand By Me", released:1986, tagline:"For some, it's the last real taste of innocence, and the first real taste of life. But for everyone, it's the time that memories are made of.").save(flush:true)
            Person RiverP =new Person(name:'River Phoenix', born:1970).save(flush:true)
            Person CoreyF =new Person(name:'Corey Feldman', born:1971).save(flush:true)
            Person WilW =new Person(name:'Wil Wheaton', born:1972).save(flush:true)
            Person JohnC =new Person(name:'John Cusack', born:1966).save(flush:true)
            Person MarshallB =new Person(name:'Marshall Bell', born:1942).save(flush:true)

            new CastMember(from:WilW, to: StandByMe, roles: ["Gordie Lachance" ]).save(flush:true)
            new CastMember(from:RiverP, to: StandByMe, roles: ["Chris Chambers" ]).save(flush:true)
            new CastMember(from:JerryO, to: StandByMe, roles: ["Vern Tessio" ]).save(flush:true)
            new CastMember(from:CoreyF, to: StandByMe, roles: ["Teddy Duchamp" ]).save(flush:true)
            new CastMember(from:JohnC, to: StandByMe, roles: ["Denny Lachance" ]).save(flush:true)
            new CastMember(from:KieferS, to: StandByMe, roles: ["Ace Merrill" ]).save(flush:true)
            new CastMember(from:MarshallB, to: StandByMe, roles: ["Mr. Lachance" ]).save(flush:true)
            new CastMember(from:RobR, to: StandByMe, type: CastMember.RoleType.DIRECTED).save(flush:true)

            //As Good as It Gets
            Movie AsGoodAsItGets= new Movie(title:'As Good as It Gets', released:1997, tagline:'A comedy from the heart that goes for the throat.').save(flush:true)
            Person HelenH =new Person(name:'Helen Hunt', born:1963).save(flush:true)
            Person GregK =new Person(name:'Greg Kinnear', born:1963).save(flush:true)
            Person JamesB =new Person(name:'James L. Brooks', born:1940).save(flush:true)

            new CastMember(from:JackN, to: AsGoodAsItGets, roles: ["Melvin Udall" ]).save(flush:true)
            new CastMember(from:HelenH, to: AsGoodAsItGets, roles: ["Carol Connelly" ]).save(flush:true)
            new CastMember(from:GregK, to: AsGoodAsItGets, roles: ["Simon Bishop" ]).save(flush:true)
            new CastMember(from:CubaG, to: AsGoodAsItGets, roles: ["Frank Sachs" ]).save(flush:true)
            new CastMember(from:JamesB, to: AsGoodAsItGets, type: CastMember.RoleType.DIRECTED).save(flush:true)
        }

6.2. 页面

建立neo4j/index.html页面,使用D3进行数据展示

<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="http://neo4j-contrib.github.io/developer-resources/language-guides/assets/css/main.css">
    <title>Neo4j Movies</title>
</head>

<body>
<div id="graph">
</div>
<div role="navigation" class="navbar navbar-default navbar-static-top">
    <div class="container">
        <div class="row">
            <div class="col-sm-6 col-md-6">
                <ul class="nav navbar-nav">
                    <li>
                        <form role="search" class="navbar-form" id="search">
                            <div class="form-group">
                                <input type="text" value="Matrix" placeholder="Search for Movie Title" class="form-control" name="search">
                            </div>
                            <button class="btn btn-default" type="submit">Search</button>
                        </form>
                    </li>
                </ul>
            </div>
            <div class="navbar-header col-sm-6 col-md-6">
                <div class="logo-well">
                    <a href="http://neo4j.com/developer-resources">
                        <img src="http://neo4j-contrib.github.io/developer-resources/language-guides/assets/img/logo-white.svg" alt="Neo4j World's Leading Graph Database" id="logo">
                    </a>
                </div>
                <div class="navbar-brand">
                    <div class="brand">Neo4j Movies</div>
                </div>
            </div>
        </div>
    </div>
</div>

<div class="row">
    <div class="col-md-5">
        <div class="panel panel-default">
            <div class="panel-heading">Search Results</div>
            <table id="results" class="table table-striped table-hover">
                <thead>
                <tr>
                    <th>Movie</th>
                    <th>Released</th>
                    <th>Tagline</th>
                </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
        </div>
    </div>
    <div class="col-md-7">
        <div class="panel panel-default">
            <div class="panel-heading" id="title">Details</div>
            <div class="row">
                <div class="col-sm-4 col-md-4">
                    <img src="" class="well" id="poster"/>
                </div>
                <div class="col-md-8 col-sm-8">
                    <h4>Crew</h4>
                    <ul id="crew">
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>
<style type="text/css">
    .node { stroke: #222; stroke-width: 1.5px; }
    .node.actor { fill: #888; }
    .node.movie { fill: #BBB; }
    .link { stroke: #999; stroke-opacity: .6; stroke-width: 1px; }
</style>

<script type="text/javascript" src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script type="text/javascript">
    var contextPath = '[[${#httpServletRequest.contextPath}]]';
    $(function () {
        function showMovie(title) {
            $.get(contextPath+"/movie/show?title=" + encodeURIComponent(title),
                function (data) {
                    if (!data) return;
                    $("#title").text(data.title);
                    $("#poster").attr("src","http://neo4j-contrib.github.io/developer-resources/language-guides/assets/posters/"+encodeURIComponent(data.title)+".jpg");
                    var $list = $("#crew").empty();
                    data.cast.forEach(function (cast) {
                        $list.append($("<li>" + cast.name + " " +cast.job + (cast.job == "acted"?" as " + cast.role : "") + "</li>"));
                    });
                }, "json");
            return false;
        }
        function search() {
            var query=$("#search").find("input[name=search]").val();
            $.get(contextPath + "/movie/search?q=" + encodeURIComponent(query),
                function (data) {
                    var t = $("table#results tbody").empty();
                    if (!data || data.length == 0) return;
                    data.forEach(function (row) {
                        var movie = row;
                        $("<tr><td class='movie'>" + movie.title + "</td><td>" + movie.released + "</td><td>" + movie.tagline + "</td></tr>").appendTo(t)
                            .click(function() { showMovie($(this).find("td.movie").text());})
                    });
                    showMovie(data[0].title);
                }, "json");
            return false;
        }

        $("#search").submit(search);
        search();
    })
</script>

<script type="text/javascript">
    var width = 800, height = 800;

    var force = d3.layout.force()
        .charge(-200).linkDistance(30).size([width, height]);

    var svg = d3.select("#graph").append("svg")
        .attr("width", "100%").attr("height", "100%")
        .attr("pointer-events", "all");

    d3.json(contextPath+"/movie/graph", function(error, graph) {
        if (error) return;

        force.nodes(graph.nodes).links(graph.links).start();

        var link = svg.selectAll(".link")
            .data(graph.links).enter()
            .append("line").attr("class", "link");

        var node = svg.selectAll(".node")
            .data(graph.nodes).enter()
            .append("circle")
            .attr("class", function (d) { return "node "+d.label })
            .attr("r", 10)
            .call(force.drag);

        // html title attribute
        node.append("title")
            .text(function (d) { return d.title; })

        // force feed algo ticks
        force.on("tick", function() {
            link.attr("x1", function(d) { return d.source.x; })
                .attr("y1", function(d) { return d.source.y; })
                .attr("x2", function(d) { return d.target.x; })
                .attr("y2", function(d) { return d.target.y; });

            node.attr("cx", function(d) { return d.x; })
                .attr("cy", function(d) { return d.y; });
        });
    });
</script>
</body>
</html>

6.3. controller 类

建立Neo4jController类

import org.groovyboot.core.annotation.GbController

@GbController
class Neo4jController {
    public void index(){

    }
}

建立MovieController类

import org.groovyboot.core.PageParams
import org.groovyboot.core.annotation.GbRestController
import org.groovyboot.example.mongodemo.domain.movie.CastMember
import org.groovyboot.example.mongodemo.domain.movie.Movie
import org.groovyboot.example.mongodemo.service.movie.MovieService
import org.springframework.beans.factory.annotation.Autowired


@GbRestController
class MovieController {
    @Autowired MovieService movieService
    public Movie show(String title){
        Map map=[:]
        Movie.withSession {
            Movie movie=movieService.find(title)
            if(movie){
                map.title=movie.title
                map.released=movie.released
                List list=[]
                movie.cast.each{CastMember castMember->
                    Map one=[:]
                    one.job = castMember.type.tokenize("_")[0].toLowerCase()
                    one.name = castMember.from.name
                    one.role = castMember.roles.toString()
                    list << one
                }
                map.cast=list
            }
        }
        return map;
    }
    public List search(String q,PageParams pageParams){
        List result=[]
        Movie.withSession {
            List list=movieService.search(q,pageParams.max);
            list.each {Movie movie->
                Map map=[:]
                map.title =movie.title
                map.released =movie.released
                map.tagline =movie.tagline
                result << map
            }
        }
        return result

    }
    public Map<String, Object> graph(PageParams pageParams) {
        return movieService.graph(pageParams.max);
    }
}

6.4. service 类

建立MovieService类

import org.groovyboot.example.mongodemo.domain.movie.Movie
import org.groovyboot.example.mongodemo.domain.movie.Person
import org.neo4j.driver.internal.InternalStatementResult
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Transactional
@Service
class MovieService {

    public Movie find(String title){
        return Movie.findByTitle(title)
    }

    public List<Movie> search(String q, int limit = 100) {
        List<Movie> results
        if (q) {
            results = Movie.where {
                title ==~ "%${q}%"
            }.list(max:limit)
        }else {
            results = []
        }
        results
    }


    private List<Map<String, Iterable<String>>> findMovieTitlesAndCast(int limit){
        return Movie.executeQuery("""MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)
               RETURN m.title as movie, collect(p.name) as cast
               LIMIT ${limit}""")

    }

    public Map<String, Object> graph(int limit = 100) {
        toD3Format(findMovieTitlesAndCast(limit))
    }

    private static Map<String, Object> toD3Format(List<Map<String, Iterable<String>>> result) {
        List<Map<String,String>> nodes = []
        List<Map<String,Object>> rels= []
        int i = 0
        for (entry in result) {
            nodes << [title: entry.movie, label: 'movie']
            int target=i
            i++
            for (String name : (Iterable<String>) entry.cast) {
                def actor = [title: name, label: 'actor']
                int source = nodes.indexOf(actor)
                if (source == -1) {
                    nodes << actor
                    source = i++
                }
                rels << [source: source, target: target]
            }
        }
        return [nodes: nodes, links: rels]
    }
}