# ORM
<p class="uk-article-lead">Pagekit 的对象关系映射工具(Pagekit Object-relational mapper ,ORM)帮助你创建应用程序的模型类,在模型类中每个属性都被自动映射到了相关数据表的列。你还可以定义你的实体(entities)与 Pagekit 中现有实体之间的关系(比如用户)</p>
## 设置
### 创建表
比如在扩展的 `scripts.php` 的 `install` 钩子中运行以下代码。了解更多关于创建表的一般信息,查看[数据库](224140)。
Example:
```php
$util = $app['db']->getUtility();
if ($util->tableExists('@forum_topics') === false) {
$util->createTable('@forum_topics', function ($table) {
$table->addColumn('id', 'integer', ['unsigned' => true, 'length' => 10, 'autoincrement' => true]);
$table->addColumn('user_id', 'integer', ['unsigned' => true, 'length' => 10, 'default' => 0]);
$table->addColumn('title', 'string', ['length' => 255, 'default' => '']);
$table->addColumn('date', 'datetime');
$table->addColumn('modified', 'datetime', ['notnull' => false]);
$table->addColumn('content', 'text');
$table->addIndex(['user_id'], 'FORUM_TOPIC_USER_ID');
$table->setPrimaryKey(['id']);
});
}
```
### 定义一个 Model 类
Example:
```
<?php
namespace Pagekit\Forum\Model;
use Pagekit\Database\ORM\ModelTrait;
/**
* @Entity(tableClass="@forum_topics")
*/
class Topic
{
use ModelTrait;
/** @Column(type="integer") @Id */
public $id;
/** @Column */
public $title = '';
/** @Column(type="datetime") */
public $date;
/** @Column(type="text") */
public $content = '';
/** @Column(type="integer") */
public $user_id;
/**
* @BelongsTo(targetEntity="Pagekit\User\Model\User", keyFrom="user_id")
*/
public $user;
}
```
模型(model)是使用 `Pagekit\Database\ORM\ModelTrait` 特性的简单 PHP类。特性(trait)允许将某些行为包含到类中 - 类似简单的类的继承。主要区别在于类可以使用多个特性,但只能从一个类继承。
**Note** 如果你不熟悉特性,赶紧看一下[PHP 关于特性的官方文档](http://php.net/manual/en/language.oop5.traits.php).
注释 `@Entity(tableClass="@my_table")` 将模型与数据表 `pk_my_table` 绑定 (`@` 会自动替换为安装时填写的数据表前缀)
注释的代码只会在以两个星号开头的多行注释中工作,只有一个星号没法工作。
```
// will NOT work:
/* @Column */
// will work:
/** @Column */
// will work:
/**
* @Column
*/
```
在类中定义一个属性时,可以将变量绑定到数据表的列上,在属性定义代码上面加上 `/** @Column(type="string") */` 注释就行。你可以使用由 [Doctrine DBAL](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html) 支持的任意类型。
在模型类中引用的类也必须在数据库中存在。
## 关系
表现在数据库模型中的应用程序数据包含其实例之间的关系。一篇博客文章有与之相关的若干评论,博客文章也恰好归属于一个用户实例。Pagekit ORM 提供了定义这些关系的机制,并且也以程序化的方式来查询它们。
### 归属关系
用在不同的关系类型上的基础注释,即模型属性上方的 `@BelongsTo` 注释。在下面的例子中(从博客的 `Post`模型中获取)我们指定了一个 `$user` 属性,它被定义用来指向 Pagekit `User` 模型的实例。
`keyFrom` 参数指定哪些源属性是用来指向用户 ID的。注意,为了通过一个查询分辨关系,我们还需要定义相应的 `user_id` 属性。
Example:
```
/** @Column(type="integer") */
public $user_id;
/**
* @BelongsTo(targetEntity="Pagekit\User\Model\User", keyFrom="user_id")
*/
public $user;
```
### 一对多关系
在这种关系中,单个模型实例被引用到了任意多个其他模型示例。经典的例子是,一篇 `Post` 拥有任意数量的归属于它的 `Comment` 实例。 反过来看,一个评论恰好归属于一篇 `Post`。
在 `Pagekit\Blog\Model\Post` 中,源自博客包的例子:
```
/**
* @HasMany(targetEntity="Comment", keyFrom="id", keyTo="post_id")
*/
public $comments;
```
在 `Pagekit\Blog\Model\Comment` 中定义逆向关系:
```
/** @Column(type="integer") */
public $post_id;
/** @BelongsTo(targetEntity="Post", keyFrom="post_id") */
public $post;
```
要查询模型(Model),可以使用 ORM 类:
```
use Pagekit\Blog\Post;
// ...
// 获取文章,不包含相关评论
$posts = Post::findAll();
var_dump($posts);
```
输出:
```
array (size=6)
1 =>
object(Pagekit\Blog\Model\Post)[4513]
public 'id' => int 1
public 'title' => string 'Hello Pagekit' (length=13)
public 'comments' => null
// ...
2 =>
object(Pagekit\Blog\Model\Post)[3893]
public 'id' => int 2
public 'title' => string 'Hello World' (length=11)
public 'comments' => null
// ...
// ...
```
```
use Pagekit\Blog\Post;
// ...
// 获取文章,包括相关评论
$posts = Post::query()->related('comments')->get();
var_dump($posts);
```
输出:
```
array (size=6)
1 =>
object(Pagekit\Blog\Model\Post)[4512]
public 'id' => int 1
public 'title' => string 'Hello Pagekit' (length=13)
public 'comments' =>
array (size=0)
empty
// ...
2 =>
object(Pagekit\Blog\Model\Post)[3433]
public 'id' => int 2
public 'title' => string 'Hello World' (length=11)
public 'comments' =>
array (size=1)
6 =>
object(Pagekit\Blog\Model\Comment)[4509]
...
// ...
// ...
```
### 一对一关系
一对一是非常简单的关系。一个 `ForumUser` 可能恰好有一个 `Avatar` 指定给它。虽然只是简单地将关于头像的所有信息包含在 `ForumUser` 模型中,有时将它们分到不同模型也是在情理之中。
要实现一对一关系,可以在每个模型类中使用 `@BelongsTo` 注释。
`/** @BelongsTo(targetEntity="Avatar", keyFrom="avatar_id", keyTo="id") */`
- `targetEntity`: 模目标模型类
- `keyFrom`: 在这个表中指向关联模型的外键
- `keyTo`: 关联模型的主键
示例模型 `ForumUser`:
```php
<?php
namespace Pagekit\Forum\Model;
use Pagekit\Database\ORM\ModelTrait;
/**
* @Entity(tableClass="@forum_user")
*/
class ForumUser
{
use ModelTrait;
/** @Column(type="integer") @Id */
public $id;
/** @Column */
public $name = '';
/** @Column(type="integer") */
public $avatar_id;
/** @BelongsTo(targetEntity="Avatar", keyFrom="avatar_id", keyTo="id") */
public $avatar;
}
```
示例模型 `Avatar`:
```php
<?php
namespace Pagekit\Forum\Model;
use Pagekit\Database\ORM\ModelTrait;
/**
* @Entity(tableClass="@forum_avatars")
*/
class Avatar
{
use ModelTrait;
/** @Column(type="integer") @Id */
public $id;
/** @Column(type="string") */
public $path;
/** @Column(type="integer") */
public $user_id;
/** @BelongsTo(targetEntity="ForumUser", keyFrom="user_id", keyTo="id") */
public $user;
}
```
要确保被关联的模型包含在查询结果中,从模型类中获取 `QueryBuilder` 实例,并在 `related()` 方法中显式列出关系属性。
```php
<?php
use Pagekit\Forum\Model\ForumUser;
use Pagekit\Forum\Model\Avatar;
// ...
// 获取所有用户,包括相关的 $avatar 对象
$users = ForumUser::query()->related('avatar')->get();
foreach ($users as $user) {
var_dump($user->avatar->path);
}
// 获取所有用户,包括相关的 $user 对象
$avatars = Avatar::query()->related('user')->get();
foreach ($avatars as $avatar) {
var_dump($avatar->user);
}
```
### 多对多关系
有时,一个关系中的两个模型可能各自都有*许多实例*。比如文章和标签之间的关系:一篇文章可以有多个指定给它的标签,同时一个标签也可以被指定给多篇文章。
下面有一个不同的例子,是讨论区论坛中“喜欢的话题”这个场景。一个用户可以有多个喜欢的话题。一个话题也可以被多个用户喜欢。
要实现多对多关系,需要一个额外的数据表。表中的每个实体表示从一个 `Topic` 实例到一个 `ForumUser` 实例的连接关系,反之亦然。在数据库建模时,这被称为 [联接表(junction table)](https://en.wikipedia.org/wiki/Associative_entity)。
数据表示例 (比如在 `scripts.php` 中):
```
$util = $app['db']->getUtility();
// 论坛用户表
if ($util->tableExists('@forum_users') === false) {
$util->createTable('@forum_users', function ($table) {
$table->addColumn('id', 'integer', ['unsigned' => true, 'length' => 10, 'autoincrement' => true]);
$table->addColumn('name', 'string', ['length' => 255, 'default' => '']);
$table->setPrimaryKey(['id']);
});
}
// 话题表
if ($util->tableExists('@forum_topics') === false) {
$util->createTable('@forum_topics', function ($table) {
$table->addColumn('id', 'integer', ['unsigned' => true, 'length' => 10, 'autoincrement' => true]);
$table->addColumn('title', 'string', ['length' => 255, 'default' => '']);
$table->addColumn('content', 'text');
$table->setPrimaryKey(['id']);
});
}
// 联接表
if ($util->tableExists('@forum_favorites') === false) {
$util->createTable('@forum_favorites', function ($table) {
$table->addColumn('id', 'integer', ['unsigned' => true, 'length' => 10, 'autoincrement' => true]);
$table->addColumn('user_id', 'integer', ['unsigned' => true, 'length' => 10, 'default' => 0]);
$table->addColumn('topic_id', 'integer', ['unsigned' => true, 'length' => 10, 'default' => 0]);
$table->setPrimaryKey(['id']);
});
}
```
关系本身是在你希望在每个模型类中可以查询它时定义的。如果指向为特定用户列出最喜爱的话题,但不列出喜欢了某个给定文章的所有用户,你只需在一个模型中定义这个关系就行了。下面的例子中, `@ManyToMany` 注释在两个模型类中都标注了。
`@ManyToMany` 可以接受这些参数:
|参数 | 描述|
|---------------- | -----------|
|`targetEntity` | 目标模型类|
|`tableThrough` | 联接表的名称|
|`keyThroughFrom` | "from" 方向的外键名称|
|`keyThroughTo` | "to" 方向的外键名称|
|`orderBy` | (可选)列表的排序|
注释示例:
```php
/**
* @ManyToMany(targetEntity="ForumUser", tableThrough="@forum_favorites", keyThroughFrom="topic_id", keyThroughTo="forum_user_id")
*/
public $users;
```
模型示例 `Topic`:
```php
<?php
namespace Pagekit\Forum\Model;
use Pagekit\Database\ORM\ModelTrait;
/**
* @Entity(tableClass="@forum_topics")
*/
class Topic
{
use ModelTrait;
/** @Column(type="integer") @Id */
public $id;
/** @Column */
public $title = '';
/** @Column(type="text") */
public $content = '';
/**
* @ManyToMany(targetEntity="ForumUser", tableThrough="@forum_favorites", keyThroughFrom="topic_id", keyThroughTo="forum_user_id")
*/
public $users;
}
```
模型示例 `ForumUser`:
```php
<?php
namespace Pagekit\Forum\Model;
use Pagekit\Database\ORM\ModelTrait;
/**
* @Entity(tableClass="@forum_user")
*/
class ForumUser
{
use ModelTrait;
/** @Column(type="integer") @Id */
public $id;
/** @Column */
public $name = '';
/**
* @ManyToMany(targetEntity="Topic", tableThrough="@forum_favorites", keyThroughFrom="forum_user_id", keyThroughTo="topic_id")
*/
public $topics;
}
```
查询示例:
```php
// 在查询中解决多对多关系
// 获取特定用户喜欢的话题
$user_id = 1;
$user = ForumUser::query()->where('id = ?', [$user_id])->related('topics')->first();
foreach ($user->topics as $topic) {
//
}
// 获取喜欢特定话题的所有用户
$topic_id = 1;
$topic = Topic::query()->where('id = ?', [$topic_id])->related('users')->first();
foreach ($topic->users as $user) {
// ...
}
```
## ORM 查询
用给定的 id 获取模型实例。
```
$post = Post::find(23)
```
获取模型的所有实例。
```
$posts = Post::findAll();
```
使用以上查询,关系不会扩大到包括相关的实例。在上面的例子中,`Post` 实例不会使它的 `$comments` 属性被初始化。
```
// 默认地,相关的对象不会被获取
$post->comments == null;
```
这样做是为性能考虑。默认地,被包含的子查询都不会执行,这样就能节省执行时间。所以,如果你需要被关联的对象,可以在 `QueryBuilder` 上使用 `related()` 方法,并明确地声明在此查询中要用到的关系。
所以,要获取一个 `Post` 实例并包含相关的 `Comment` 实例,你需要构建一个获取相关对象的查询。
```
// fetch all, including related objects
$posts = Post::query()->related('comments')->get();
// fetch single instance, include related objects
$id = 23;
$post = Post::query()->related('comments')->where('id = ?', [$id])->first();
```
注意 `find(23)` 是如何被 `->where('id = ?', [$id])->first()` 替代的。这是因为 `find()` 是定义在模型上的方法。然而在第二个例子中,我们拥有一个 `Pagekit\Database\ORM\QueryBuilder` 的实例。
了解更多关于 ORM 查询和常规查询的信息,查阅文档[数据库](224140)中查询相关部分。)
## 新建模型实例
你可以在新的模型实例上通过调用 `save()` 方法创建和保存一个新的模型。
```php
$user = new ForumUser();
$user->name = "bruce";
$user->save();
```
作为一种选择,可以在模型类上直接调用 `create()` 方法,并提供一个现有数据的数组来初始化实例。然后调用 `save()` 将实例存储到数据库。
```php
$user = ForumUser::create(["name" => "peter"]);
$user->save();
```
## 修改现有的实例
获取现有实例,对对象执行任意修改,然后调用 `save()` 方法将这些修改存储到数据库。
```php
$user = ForumUser::find(2);
$user->name = "david";
$user->save();
```
## 删除现有的实例
获取现有的实例,并调用 `delete()` 方法将此实例从数据库中移除。
```php
$user = ForumUser::find(2);
$user->delete();
```