gohze / neoeloquent

Laravel 对 Neo4j 图形数据库 REST 接口的包装


README

SensioLabsInsight

Build Status

NeoEloquent

Neo4j 图形 Eloquent 驱动程序 for Laravel,此包是原始包 vinelab/neoeloquent 的分支。由于包已过时,我们决定分支以进行一些更新。

快速参考

安装

将包添加到您的 composer.json 并运行 composer update

laravel 8.*

composer.json

{
    "require": {
        "gohze/neoeloquent": "^1.7"
    }
}

laravel 7.*

composer.json

{
    "require": {
        "gohze/neoeloquent": "^1.6"
    }
}

laravel 6.*

composer.json

{
    "require": {
        "gohze/neoeloquent": "^1.5"
    }
}

Laravel 5

5.6

{
    "require": {
        "vinelab/neoeloquent": "^1.4.6"
    }
}

5.5

{
    "require": {
        "vinelab/neoeloquent": "^1.4.5"
    }
}

5.4

{
    "require": {
        "vinelab/neoeloquent": "1.4.3"
    }
}

5.3

{
    "require": {
        "vinelab/neoeloquent": "1.4.2"
    }
}

5.2

{
    "require": {
        "vinelab/neoeloquent": "1.3.*"
    }
}

5.1

{
    "require": {
        "vinelab/neoeloquent": "1.2.*"
    }
}

5.0

{
    "require": {
        "vinelab/neoeloquent": "1.2.5"
    }
}

Laravel 4

{
    "require": {
        "vinelab/neoeloquent": "1.1.*"
    }
}

app/config/app.php 中添加服务提供者

'Vinelab\NeoEloquent\NeoEloquentServiceProvider',

服务提供者将注册此包所需的所有类,并将 Model 类别名到 NeoEloquent,因此您可以在模型中简单地 extend NeoEloquent

配置

连接

app/config/database.php 中或在没有基于环境的配置的情况下在 app/config/[env]/database.php 中,将 neo4j 设置为默认连接

'default' => 'neo4j',

添加连接默认值

'connections' => [
    'neo4j' => [
        'driver' => 'neo4j',
        'host'   => env('DB_HOST', 'localhost'),
        'port'   => env('DB_PORT', '7474'),
        'username' => env('DB_USERNAME', null),
        'password' => env('DB_PASSWORD', null)
    ]
]

Lumen

对于 Lumen,您需要在应用程序根目录中创建一个名为 config 的新文件夹,并在其中添加一个名为 database.php 的文件。在那里,您将添加以下代码。

<?php

return ['connections' => [
            'neo4j' => [
                'driver' => 'neo4j',
                'host'   => env('DB_HOST', 'localhost'),
                'port'   => env('DB_PORT', '7474'),
                'username' => env('DB_USERNAME', null),
                'password' => env('DB_PASSWORD', null)
            ]
        ]
    ];

并在 bootstrap/app.php 中添加以下行

$app->configure('database');

这是为了使 Lumen 能够读取除默认配置之外的配置。

在添加服务提供者时,您必须在 bootstrap/app.php 的“Register Providers”部分中添加它。您可以如此添加

$app->register('Vinelab\NeoEloquent\NeoEloquentServiceProvider');

迁移设置

如果您愿意有迁移

  • 创建 app/database/labels 文件夹
  • 修改 composer.json 并将 app/database/labels 添加到 classmap 数组
  • 运行 composer dump-autoload

文档

模型

class User extends NeoEloquent {}

正如它的简单性一样,NeoEloquent 将从类名生成默认节点标签,在这种情况下将是 :User。有关节点标签的更多信息,请参阅 此处

命名空间模型

当您在模型中使用命名空间时,标签将考虑完整的命名空间。

namespace Vinelab\Cms;

class Admin extends NeoEloquent { }

从该关系生成的标签将是 VinelabCmsAdmin,这是为了确保在引入另一个 Admin 实例(如 Vinelab\Blog\Admin)时标签不会冲突,这会导致数据库中的 :Admin 出现混乱。

自定义节点标签

您可以指定您希望使用的标签(而不是默认生成的),它们也是大小写敏感的,因此它们将按此方式存储。

class User extends NeoEloquent {

    protected $label = 'User'; // or array('User', 'Fan')

    protected $fillable = ['name', 'email'];
}

$user = User::create(['name' => 'Some Name', 'email' => 'some@email.com']);

NeoEloquent 对 $table 变量的回退支持,如果找到且模型上没有定义 $label,则将使用它。

class User extends NeoEloquent {

    protected $table = 'User';

}

不要担心标签格式化,您可以指定它们为 array('Label1', 'Label2') 或通过冒号分隔它们,并在前面添加冒号是可选的。

软删除

Laravel 5

要启用软删除,您需要使用 use Vinelab\NeoEloquent\Eloquent\SoftDeletes 而不是 Illuminate\Database\Eloquent\SoftDeletes,并且就像 Eloquent 一样,您需要在模型中使用 $dates,如下所示

use Vinelab\NeoEloquent\Eloquent\SoftDeletes;

class User extends NeoEloquent {

    use SoftDeletes;

    protected $dates = ['deleted_at'];

}

Laravel 4

要启用软删除,您需要使用 Vinelab\NeoEloquent\Eloquent\SoftDeletingTrait 而不是 Illuminate\Database\Eloquent\SoftDeletingTrait,并且就像 Eloquent 一样,您需要在模型中添加 $dates,如下所示

use Vinelab\NeoEloquent\Eloquent\SoftDeletingTrait;

class User extends NeoEloquent {

    use SoftDeletingTrait;

    protected $dates = ['deleted_at'];

}

关系

让我们来看一些节点之间关系示例。

一对一

class User extends NeoEloquent {

    public function phone()
    {
        return $this->hasOne('Phone');
    }

这表示从 :User 节点到 :PhoneOUTGOING 关系方向。

保存
$phone = new Phone(['code' => 961, 'number' => '98765432'])
$relation = $user->phone()->save($phone);

此语句执行的 Cypher 如下所示

MATCH (user:`User`)
WHERE id(user) = 1
CREATE (user)-[:PHONE]->(phone:`Phone` {code: 961, number: '98765432', created_at: 7543788, updated_at: 7543788})
RETURN phone;
定义此关系的反向
class Phone extends NeoEloquent {

    public function user()
    {
        return $this->belongsTo('User');
    }
}

这表示从 :User 节点到该 :Phone 节点的 INCOMING 关系方向。

关联模型

由于我们不处理 外键,在这种情况下,这不仅仅是设置父模型上的外键属性。在 Neo4j(以及通用图)中,关系本身就是一个实体,也可以有自己的属性,因此引入了

注意:关联模型在调用 associate() 时不会自动持久化关系。

$account = Account::find(1986);

// $relation will be Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn
$relation = $user->account()->associate($account);

// Save the relation
$relation->save();

此语句执行的 Cypher 如下所示

MATCH (account:`Account`), (user:`User`)
WHERE id(account) = 1986 AND id(user) = 9862
MERGE (account)<-[rel_user_account:ACCOUNT]-(user)
RETURN rel_user_account;

一对多

class User extends NeoEloquent {

    public function posts()
    {
        return $this->hasMany('Post', 'POSTED');
    }
}

这表示从 :User 节点到 :Post 节点的 OUTGOING 关系方向。

$user = User::find(1);
$post = new Post(['title' => 'The Title', 'body' => 'Hot Body']);
$user->posts()->save($post);

类似于 One-To-One 关系,save() 语句返回的值是一个 Edge[In|Out]

此语句执行的 Cypher 如下所示

MATCH (user:`User`)
WHERE id(user) = 1
CREATE (user)-[rel_user_post:POSTED]->(post:`Post` {title: 'The Title', body: 'Hot Body', created_at: '15-05-2014', updated_at: '15-05-2014'})
RETURN rel_user_post;
定义此关系的反向
class Post extends NeoEloquent {

    public function author()
    {
        return $this->belongsTo('User', 'POSTED');
    }
}

这表示从 :User 节点到该 :Post 节点的 INCOMING 关系方向。

多对多

class User extends NeoEloquent {

    public function followers()
    {
        return $this->belongsToMany('User', 'FOLLOWS');
    }
}

这表示一个 :User 节点与另一个 :User 节点之间的 INCOMING 关系。

$jd = User::find(1012);
$mc = User::find(1013);

$jd 跟随 $mc

$jd->followers()->save($mc);

或使用 attach() 方法

$jd->followers()->attach($mc);
// Or..
$jd->followers()->attach(1013); // 1013 being the id of $mc ($mc->getKey())

此语句执行的 Cypher 如下所示

MATCH (user:`User`), (followers:`User`)
WHERE id(user) = 1012 AND id(followers) = 1013
CREATE (followers)-[:FOLLOWS]->(user)
RETURN rel_follows;

$mc 返回跟随 $jd

$mc->followers()->save($jd);

此语句执行的 Cypher 如下所示

MATCH (user:`User`), (followers:`User`)
WHERE id(user) = 1013 AND id(followers) = 1012
CREATE (user)-[rel_user_followers:FOLLOWS]->(followers)
RETURN rel_follows;

获取 $jd 的关注者

$followers = $jd->followers;

此语句执行的 Cypher 如下所示

MATCH (user:`User`), (followers:`User`), (user)-[rel_user_followers:FOLLOWS]-(followers)
WHERE id(user) = 1012
RETURN rel_follows;

动态属性

class Phone extends NeoEloquent {

    public function user()
    {
        return $this->belongsTo('User');
    }

}

$phone = Phone::find(1006);
$user = $phone->user;
// or getting an attribute out of the related model
$name = $phone->user->name;

多态

多态关系的概念在本质上是对象的关系,但在图表示中,我们将其表示为 HyperEdge

HyperEdge 涉及三个模型,即 模型、 模型和 相关 模型,如图所示

HyperEdges

在代码中,这将通过以下三个模型 User CommentPost 来表示,其中 id 为 1 的 User 发布了一个 Post,而 id 为 6 的 User 对该 PostComment 进行了评论,如下所示

class User extends NeoEloquent {

    public function comments($morph = null)
    {
        return $this->hyperMorph($morph, 'Comment', 'COMMENTED', 'ON');
    }

}

为了保持简单但仍然涉及三个模型,我们还需要传递 $morph,这是一个任何 commentable 模型,在我们的例子中是 VideoPost 模型。

注意:请确保将其默认设置为 null,这样我们就可以稍后通过 $user->comments 动态或懒加载。

使用 create() 方法创建 Comment

$user = User::find(6);
$post = Post::find(2);

$user->comments($post)->create(['text' => 'Totally agree!', 'likes' => 0, 'abuse' => 0]);

如往常一样,我们将返回一个边,但这次它不是有方向的,它是一个 HyperEdge 实例,更多关于 HyperEdges 的信息。

或者您也可以保存一个 Comment 实例

$comment = new Comment(['text' => 'Magnificent', 'likes' => 0, 'abuse' => 0]);

$user->comments($post)->save($comment);

此外,所有在 BelongsToMany 关系中找到的功能都得到了支持,如通过 Ids 关联模型

$user->comments($post)->attach([$id, $otherId]);

或取消关联模型

$user->comments($post)->detach($comment); // or $comment->id

同步

$user->comments($post)->sync([$id, $otherId, $someId]);

检索多态关系

从我们之前的示例中,我们将使用 Video 模型来检索它们的评论

class Video extends NeoEloquent {

    public function comments()
    {
        return $this->morphMany('Comment', 'ON');
    }

}
动态加载多态模型
$video = Video::find(3);
$comments = $video->comments;
懒加载多态模型
$video = Video::with('comments')->find(3);
foreach ($video->comments as $comment)
{
    //
}

检索多态关系的反向

class Comment extends NeoEloquent {

    public function commentable()
    {
        return $this->morphTo();
    }

}
$postComment = Comment::find(7);
$post = $comment->commentable;

$videoComment = Comment::find(5);
$video = $comment->commentable;

// You can also eager load them
Comment::with('commentable')->get();

您还可以指定要返回的 morph 类型

class Comment extends NeoEloquent {

    public function post()
    {
        return $this->morphTo('Post', 'ON');
    }

    public function video()
    {
        return $this->morphTo('Video', 'ON');
    }

}

多态关系简述

为了更深入地了解,以下是参与多态关系的三个模型是如何连接的

class User extends NeoEloquent {

    public function comments($morph = null)
    {
        return $this->hyperMorph($morph, 'Comment', 'COMMENTED', 'ON');
    }

}
class Post extends NeoEloquent { // Video is the same as this one

    public function comments()
    {
        return $this->morphMany('Comment', 'ON');
    }

}
class Comment extends NeoEloquent {

    public function commentable()
    {
        return $this->morphTo();
    }

}

懒加载

class Book extends NeoEloquent {

    public function author()
    {
        return $this->belongsTo('Author');
    }
}

以尽可能少的性能开销加载作者及其书籍。

foreach (Book::with('author')->get() as $book)
{
    echo $book->author->name;
}

循环中只会运行两个 Cypher 查询

MATCH (book:`Book`) RETURN *;

MATCH (book:`Book`), (book)<-[:WROTE]-(author:`Author`) WHERE id(book) IN [1, 2, 3, 4, 5, ...] RETURN book, author;

简介

由于图中的关系与其他数据库类型大不相同,因此我们必须相应地处理它们。关系具有方向,可以是分别指向父节点的方向。

EdgeIn

表示从相关模型到父模型的方向为INCOMING的关系。

class Location extends NeoEloquent {

    public function user()
    {
        return $this->belongsTo('User', 'LOCATED_AT');
    }

}

要将一个User关联到一个Location

$location = Location::find(1922);
$user = User::find(3876);
$relation = $location->associate($user);

在Cypher中映射为(:Location)<-[:LOCATED_AT]-(:User),其中$relation是代表指向父节点的入关系的EdgeIn实例。

您还可以从边缘访问模型

$relation = $location->associate($user);
$location = $relation->parent();
$user = $relation->related();

EdgeOut

表示从父模型到相关模型的OUTGOING方向关系。

class User extends NeoEloquent {

    public function posts()
    {
        return $this->hasMany('Post', 'POSTED');
    }

}

要保存从:User:Post的出边,操作如下

$post = new Post(['...']);
$posted = $user->posts()->save($post);

在Cypher中将是(:User)-[:POSTED]->(:Post),其中$posted是代表出关系的EdgeOut实例。

并获取相关模型

$edge = $user->posts()->save($post);
$user = $edge->parent();
$post = $edge->related();

HyperEdge

此边缘是通过一个表示涉及两个其他边缘()的边缘的多态关系生成的,可以通过left()right()方法访问。

由于这不是两个模型之间的直接关系,因此此边缘与其他边缘的处理略有不同,这意味着它没有特定的方向。

$edge = $user->comments($post)->attach($comment);
// Access the left and right edges
$left = $edge->left();
$user = $left->parent();
$comment = $left->related();

$right = $edge->right();
$comment = $right->parent();
$post = $right->related();

与边缘一起工作

如前所述,边缘是图中的实体,与SQL中的外键不同,外键在所属模型上作为父模型的属性,或者在文档中作为嵌入或id作为引用。因此,我们将其开发为轻量级模型,这意味着您可以像处理一个Eloquent实例一样处理它们 - 在一定程度上,除了超边缘之外。

// Create a new relationship
$relation = $location->associate($user); // Vinelab\NeoEloquent\Eloquent\Edges\EdgeIn

// Save the relationship to the database
$relation->save(); // true

对于HyperEdge,您可以如下访问所有三个模型

$edge    = $user->comments($post)->save($comment);
$user    = $edge->parent();
$comment = $edge->hyper();
$post    = $edge->related();

边缘属性

默认情况下,边缘将自动设置和更新时间戳created_atupdated_at,前提是已经通过在父模型上将$timestamps设置为true启用了时间戳。

$located_at = $location->associate($user);
$located_at->since = 1966;
$located_at->present = true;
$located_at->save();

// $created_at and $updated_at are Carbon\Carbon instances
$created_at = $located_at->created_at;
$updated_at = $located_at->updated_at;
从关系检索边缘

与创建关联的方式相同,我们可以在belongsTo关系上调用edge($model)方法来检索两个模型之间的边缘。

$location = Location::find(1892);
$edge = $location->user()->edge();

您还可以指定边缘另一侧的模型。

注意:默认情况下,NeoEloquent将尝试执行$location->user,根据关系函数名称确定边缘的相关侧面,在这种情况下是user()

$location = Location::find(1892);
$edge = $location->user()->edge($location->user);

仅在 Neo 中

在这里,您将找到NeoEloquent特定的方法和实现,这些方法和Eloquent的奇妙方法一起使处理图和Neo4j变得非常愉快!

创建与

此方法“某种程度上”填补了关系数据库和文档数据库之间的差距,它允许在一次数据库查询中创建多个相关模型。

创建新记录和关系

以下是一个创建带有附件照片和视频的帖子的示例

class Post extends NeoEloquent {

    public function photos()
    {
        return $this->hasMany('Photo', 'PHOTO');
    }

    public function videos()
    {
        return $this->hasMany('Video', 'VIDEO');
    }
}
Post::createWith(['title' => 'the title', 'body' => 'the body'], [
    'photos' => [
        [
            'url'      => 'http://url',
            'caption'  => '...',
            'metadata' => '...'
        ],
        [
            'url' => 'http://other.url',
            'caption' => 'the bay',
            'metadata' => '...'
        ]
    ],

    'videos' => [
        'title' => 'Boats passing us by',
        'description' => '...'
    ]
]);

photosvideos必须与Post模型中的关系方法名称相同。

上述示例执行了以下Cypher查询

CREATE (post:`Post` {title: 'the title', body: 'the body'}),
(post)-[:PHOTO]->(:`Photo` {url: 'http://url', caption: '...', metadata: '...'}),
(post)-[:PHOTO]->(:`Photo` {url: 'http://other', caption: 'the bay', metadata: '...'}),
(post)-[:VIDEO]->(:`Video` {title: 'Boats passing us by', description: '...'});

我们将以以下方式获得创建的节点及其关系

CreateWith

您还可以将模型和属性作为关系值混合,但不是必需的,因为NeoEloquent将通过$fillable过滤器管道传递提供的属性。

$videos = new Video(['title' => 'foo', 'description' => 'bar']);
Post::createWith($info, compact('videos'));

您还可以使用单个属性数组

class User extends NeoEloquent {

    public function account()
    {
        return $this->hasOne('Account');
    }
}

User::createWith(['name' => 'foo'], ['account' => ['guid' => 'bar', 'email' => 'some@mail.net']]);

将现有记录作为关系附加

createWith足够智能,能够区分您是传递现有模型、模型ID还是需要创建的新记录,这使得您可以混合新记录和现有记录。

class Post extends NeoEloquent {

    public function tags()
    {
        return $this->hasMany('Tag', 'TAG');
    }
}
$tag1 = Tag::create(['title' => 'php']);
$tag2 = Tag::create(['title' => 'dev']);

$post = Post::createWith(['title' => 'foo', 'body' => 'bar'], ['tags' => [$tag1, $tag2]]);

我们将获取与现有Tag节点相关的Post

或者使用模型的id

Post::createWith(['title' => 'foo', 'body' => 'bar'], ['tags' => 1, 'privacy' => 2]);

附加记录的查询的Cypher语句将是

CREATE (post:`Post` {title: 'foo', 'body' => 'bar'})
WITH post
MATCH (tag:`Tag`)
WHERE id(tag) IN [1, 2]
CREATE (post)-[:TAG]->(tag);

迁移

为了使迁移工作,请执行以下操作

  • 创建 app/database/labels 文件夹
  • 修改 composer.json 并将 app/database/labels 添加到 classmap 数组

由于Neo4j是一个无模式的数据库,因此您不需要预先定义标签的属性类型。但是,您可以使用NeoEloquent的简单Schema来执行索引约束

命令

NeoEloquent在neo4j命名空间下引入了新的命令,因此您仍然可以并行使用Eloquent的迁移命令。

迁移命令与Eloquent相同,形式为neo4j:migrate[:command]

neo4j:make:migration                 Create a new migration file
neo4j:migrate                        Run the database migrations
neo4j:migrate:reset                  Rollback all database migrations
neo4j:migrate:refresh                Reset and re-run all migrations
neo4j:migrate:rollback               Rollback the last database migration

创建迁移

就像在Laravel中一样,您可以通过使用Artisan的make命令来创建新的迁移。

php artisan neo4j:migrate:make create_user_label

标签迁移将放在app/database/labels

您可以为命令添加额外的选项,例如

php artisan neo4j:migrate:make foo --path=app/labels
php artisan neo4j:migrate:make create_user_label --create=User
php artisan neo4j:migrate:make create_user_label --label=User

运行迁移

运行所有挂起的迁移
php artisan neo4j:migrate
运行指定路径下所有挂起的迁移
php artisan neo4j:migrate --path=app/foo/labels
运行指定包下所有挂起的迁移
php artisan neo4j:migrate --package=vendor/package

注意:如果在运行迁移时收到“找不到类”的错误,请尝试运行composer dump-autoload命令。

在生产中强制运行迁移

要强制在生产数据库上运行迁移,您可以使用

php artisan neo4j:migrate --force

回滚迁移

回滚最后一个迁移操作
php artisan neo4j:migrate:rollback
回滚所有迁移
php artisan neo4j:migrate:reset
回滚所有迁移并再次运行它们
php artisan neo4j:migrate:refresh

php artisan neo4j:migrate:refresh --seed

模式

NeoEloquent将自动将Neo4jSchema外观别名为您,以便您可以在操作标签时使用。

Neo4jSchema::label('User', function(Blueprint $label)
{
    $label->unique('uuid');
});

如果您决定手动编写迁移类(而不是使用生成器),请确保有这些use语句

  • use Vinelab\NeoEloquent\Schema\Blueprint;
  • use Vinelab\NeoEloquent\Migrations\Migration;

目前Neo4j支持属性上的UNIQUE约束和INDEX。您可以在以下位置了解更多信息

http://docs.neo4j.org/chunked/stable/graphdb-neo4j-schema.html

Schema Methods

删除标签

Neo4jSchema::drop('User');
Neo4jSchema::dropIfExists('User');

重命名标签

Neo4jSchema::renameLabel($from, $to);

检查标签是否存在

if (Neo4jSchema::hasLabel('User')) {

} else {

}

检查关系是否存在

if (Neo4jSchema::hasRelation('FRIEND_OF')) {

} else {

}

您可以在以下位置了解有关迁移和模式的信息

https://laravel.net.cn/docs/schema

https://laravel.net.cn/docs/migrations

聚合

除了Eloquent构建器聚合之外,NeoEloquent还支持Neo4j特定的聚合,如percentilestandard deviation,以方便起见,保留相同的函数名称。有关更多信息,请参阅文档

table()代表模型的标签

$users = DB::table('User')->count();

$distinct = DB::table('User')->countDistinct('points');

$price = DB::table('Order')->max('price');

$price = DB::table('Order')->min('price');

$price = DB::table('Order')->avg('price');

$total = DB::table('User')->sum('votes');

$disc = DB::table('User')->percentileDisc('votes', 0.2);

$cont = DB::table('User')->percentileCont('votes', 0.8);

$deviation = DB::table('User')->stdev('sex');

$population = DB::table('User')->stdevp('sex');

$emails = DB::table('User')->collect('email');

更新日志

请参阅版本以获取详细信息。

避免

以下是某些约束和Graph特定的问题,这是一份既不支持也不推荐的功能列表。

连接 😖

  • 它们在图上没有意义,图还讨厌它们!这使得它们故意不受支持。如果您从基于SQL的应用程序迁移,它们将是您的噩梦。

多对多关系中的透视表

这不受支持,我们将使用来处理模型之间的关系。

嵌套数组和对象

  • 由于对象映射类型对单个对象的限制,您无法在单个模型中拥有嵌套的数组对象,请确保它是平的。示例:
// Don't
User::create(['name' => 'Some Name', 'location' => ['lat' => 123, 'lng'=> -123 ] ]);

请参阅createWith()方法,了解如何在图上实现此操作。

测试

  • 安装Neo4j实例并使用默认配置localhost:7474运行它
  • 确保数据库图形为空以避免冲突
  • 运行 composer install 后,应该有 /vendor/bin/phpunit
  • 在确认 Neo4j 实例正在运行后,运行 ./vendor/bin/phpunit

标记为不完整的测试意味着它们是已知问题或不受支持的特性,请检查包含的消息以获取更多信息。