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