# 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();
```