phpixie / orm
PHPixie 的 ORM 库
Requires
- phpixie/database: ~3.11
- phpixie/slice: ~3.0
Requires (Dev)
- phpixie/test: ~3.2
This package is auto-updated.
Last update: 2024-08-26 09:35:14 UTC
README
PHPixie ORM 库
初始化
$slice = new \PHPixie\Slice(); $database = new \PHPixie\Database($slice->arrayData(array( 'default' => array( 'driver' => 'pdo', 'connection' => 'sqlite::memory:' ) ))); $orm = new \PHPixie\ORM($database, $slice->arrayData(array( // We will add some configuration later )));
如果您使用 PHPixie 框架,ORM 组件已经为您设置好了。您可以通过
$frameworkBuilder->components()->orm()
访问它,并且可以在您包的config/orm.php
文件中进行配置。
模型
模型由存储库、查询和实体组成,通常映射到关系数据库中的一个表或在 MongoDB 中的一个文档集合。
- 实体 - 数据库中存储的单个项目,例如文章
- 存储库 - 用于创建和保存实体
- 查询 - 用于选择、更新或删除实体
您可以直接开始使用模型,无需任何配置,它将使用基于模型名称的默认设置。
$repository = $orm->repository('article'); $newArticle = $repository->createEntity(); $articleQuery = $repository->query(); // shorthands $newArticle = $orm->createEntity('article'); $articleQuery = $orm->query('article');
配置
默认情况下,ORM 假设表名是模型名称的复数形式,主键名称为 'id'。对于 MongoDB 数据库,默认的 id 字段为 '_id'。您可以在配置文件中为特定模型覆盖这些设置
return array( 'models' => array( 'article' => array( 'type' => 'database', 'connection' => 'default', 'id' => 'id' ), // you can also define embedded models // if you are using MongoDB, // more on that later ) );
实体
// Saving an entity $article->title = 'Welcome'; $article->save(); // Getting a field with a default value $article->getField('title', 'No Title'); // Getting a required field // Will throw an Exception if it is not set $article->getRequiredField('title'); // Convert to a simple PHP object // Usefull for serializing $object = $article->asObject(); // Deleting $article->delete();
查询
ORM 查询与数据库查询共享许多语法,以下是一些查询示例
// Find article by name $article = $orm->query('article') ->where('title', 'Welcome') ->findOne(); // Find by id $article = $orm->query('article') ->in($id) ->findOne(); // Query by multiple ids $articles = $orm->query('article') ->in($ids) ->findOne(); // Actually the in() method can be used // to also include subqueries and entities. // // This will select articles with id '1', // or the id same as $someArticle, and // anything matched by $subQuery $articles = $orm->query('article') ->in(array( 1, $someArticle, $subQuery )); // Multiple conditions $articles = $orm->query('article') ->where('viewsTotal', '>', 2) ->or('viewsDone', '<', 5) ->find(); // Ordering // Supports multiple fields $articles ->orderAscendingBy('name') ->orderDescendingBy('id'); // Limit and offset $articles ->limit(1) ->offset(2); // It continues in the same way // as in the Database component $articles = $orm->query('article') ->where('name', 'Welcome') ->or(function($query) { $querty ->where('viewsTotal', '>', 2) ->or('viewsDone', '<', 5); }) ->find(); // Alternative syntax for // nested conditions $articles = $orm->query('article') ->where('name', 'Welcome') ->startWhereConditionGroup('or') ->where('viewsTotal', '>', 2) ->or('viewsDone', '<', 5) ->endGroup() ->find();
如果使用 findOne
,则查询将返回单个项目或 null
。当使用 find
时,将返回一个加载器,可以按以下方式使用
// Iterate over it // Note: this can be done only once foreach($articles as $article) { // ... } // Convert into an array // to allow multiple iterations $articles = $articles->asArray(); // Convert it into plain objects, // useful for serializing $data = $articles->asArray(true); // Convert it into associative array $data = $articles->asArray(true, 'id');
如果您将要访问多个您正在选择的项目的关系,您应该预加载它们的关系以避免多次数据库查询。这称为 预加载。
// To load relationships eagerly // just pass their names to find() or findOne() $articles = $query->find(array( 'author', 'tags' )); // You can also preload nested relationships // using the dot notation $articles = $query->find(array( 'author.friends', ));
查询也可用于更新和删除项目
// Count matched items $count = $articleQuery->count(); // Delete matched items $articleQuery->delete(); // Update a field in all matched items $articleQuery ->update(array( 'status' => 'published' )); // Some more advanced updating $articleQuery ->getUpdateBuilder() ->increment('views', 1) ->set('status', 'published') // Removing a field in MongoDB ->remove('isDraft') ->execute();
扩展模型
扩展 ORM 模型通常会导致开发人员将业务逻辑与数据库耦合,这使得它们难以测试和调试。在这些情况下,进行任何类型的测试都需要在数据库中插入一些示例测试数据。PHPixie ORM 通过允许类似装饰器的行为并使用包装器来解决这个问题。您不是扩展类,而是提供包装器,这些包装器可以用于包装 ORM 实体、查询和存储库,并为其添加功能。
class UserEntity extends \PHPixie\ORM\Wrappers\Type\Database\Entity { // Get users full name public function fullName() { // You can access the actual entity // Using $this->entity; return $this->entity->firstName.' '.$this->entity->lastName; } } class UserQuery extends \PHPixie\ORM\Wrappers\Type\Database\Query { // Extending queries is useful // For adding bulk conditions public function popular() { // Access query with $this->query $this->query ->where('viewsPerDay', '>', 5000) ->orWhere('friendCount' '>=', 100); } } // You will rarely need to extend repositories class UserRepository extends \PHPixie\ORM\Wrappers\Type\Database\Repository { // Overriding a save method // can be used for validation public function save($entity) { if($entity->getField('name') === null) { throw new \Exception("You must provide a user name"); } $this->repository->save($entity); } }
现在我们必须将这些类注册到 ORM 中。
class ORMWrappers extends \PHPixie\ORM\Wrappers\Implementation { // Model names of database entities to wrap protected $databaseEntities = array( 'user' ); // Model names of queries to wrap protected $databaseQueries = array( 'user' ); // Model names of repositories to wrap protected $databaseRepositories = array( 'user' ); // Model names of embedded entities to wrap // We cover them later in this manual protected $embeddedEntities = array( 'post' ); // Provide methods to build the wrappers public function userEntity($entity) { return new UserEntity($entity); } public function userQuery($query) { return new UserQuery($query); } public function userRepository($repository) { return new UserRepository($repository); } public function postEntity($entity) { return new PostEntity($entity); } }
在创建 ORM 实例时传递此类的实例
$wrappers = new ORMWrappers(); $orm = new \PHPixie\ORM($database, $slice->arrayData(array( // Configuration options )), $wrappers);
当使用 PHPixie 框架时,您已经注册并存在于您的包中的
ORMWrappers
类。
关系
PHPixie 支持一对一、一对多、多对多关系,以及嵌入式模型的一对一和一对多。每种关系都定义了一组属性,这些属性会被添加到实体和查询中,并可用于访问相关数据。您可以通过配置文件中的 relationships
键来配置它们
return array( 'models' => array( // ... ), 'relationships' => array( array( 'type' => 'manyToMany', 'left' => 'article', 'right' => 'tag' ) ) );
可以定义同一数据库中表之间的关系,不同数据库之间的关系,甚至是关系数据库和 MongoDB 之间的关系
查询关系
在查看实际的关系类型之前,让我们看看如何定义关系上的条件
// Lets say we have a one to many relationship // between categories and articles // Finding all categories with // at lest one article // // Note that you use the property name, // and not the model name here. // You can modify property names // in your config file, more on them later $categoryQuery->relatedTo('articles'); // or all articles with a category $articleQuery->relatedTo('category'); // Use logic operators // like with where() $articleQuery->orNotRelatedTo('category'); // Find categories related to // particular articles $categoryQuery->relatedTo('articles', $articles); // $articles can be an id of an article, // an article entity or an article query, // or an array of them, e.g. $categoryQuery->relatedTo('articles', $articleQuery); $categoryQuery->relatedTo('articles', $someArticle); $categoryQuery->relatedTo('articles', 4); //id // Will find all categories related // to any of the defined articles $categoryQuery->relatedTo('articles', array( 4, 3, $articleQuery, $articleEntity )); // Relationship conditions // Find categories related to articles // that have a title 'Welcome' $categoryQuery->relatedTo('articles', function($query) { $query ->where('title', 'Welcome'); }); // Or a shorthand // You'll be using this a lot $categoryQuery->where('articles.title', 'Welcome'); // You can use the '.' // to go deeper in the relationships // Find categories that have at least one article // written by the author 'Dracony' $categoryQuery->where('articles.author.name', 'Dracony'); // Or combine the verbose approach // with the shorthand one $categoryQuery->relatedTo('articles.author', function($query) { $query ->where('name', 'Dracony'); });
一对一
这是最常见的关系。一个类别可以拥有多个文章,一个主题可以有多个回复等。
// Configuration return array( 'models' => array( // ... ), 'relationships' => array( //... array( // mandatory options 'type' => 'oneToMany', 'owner' => 'category', 'items' => 'article', // The following keys are optional // and will fallback to the defaults // based on model names 'ownerOptions' => array( // the name of the property added to the owner // e.g. $category->articles(); // // defaults to the plural of the item model 'itemsProperty' => 'articles' ), 'itemsOptions' => array( // the name of the property added to items // e.g. $article->category() // // defaults to the owner model 'ownerProperty' => 'category', // the field that is used to link items // defaults to '<property>Id' 'ownerKey' => 'categoryId', // The default behavior is to set // the field defined in ownerKey to null // when the owner gets deleted // // changed it to 'delete' to delete // the articles when their category is deleted 'onOwnerDelete' => 'update' ) ) ) );
现在我们已经定义了关系属性,我们可以开始使用它们了
// Using with entities // Getting articles category // The property names used are // the ones defined in the config $category = $article->category(); // Get category articles $articles = $category->articles(); // Add article to category $category->articles->add($article); // Remove article from category $category->articles->remove($article); // Remove categories from all articles $category->articles->removeAll(); // Or you can do the same // from the article side $article->category->set($category); $article->category->remove();
注意使用属性而不是像
addArticle
、removeAllArticles
这样的传统方法可以使您的实体更整洁,并且当定义多个关系时,不会导致单个类中添加大量方法
// You can use queries, ids and arrays // anywhere you can use an entity. // This allows performing bulk operations faster // Assign first 5 articles // to a category $articleQuery ->limit(5) ->offset(0); $category->articles->add($articleQuery);
查询也具有属性,就像实体一样。这使得您可以使用更少的数据库调用执行更多操作。
// Assign articles to a category $articlesQuery->category->set($category); // Assign articles to a category with a single // database call, without the need to select // rows from database $categoryQuery->where('title', 'PHP'); $articlesQuery->category->set($category); // Unset categories for all articles $orm->query('aricle') ->category->remove(); // Unset only some ctaegories $categoryQuery->where('title', 'PHP'); $orm->query('aricle') ->category->remove($category);
使用查询而不是遍历实体可以为您的性能提供巨大的提升
// You can also use query properties // for query building $categoryQuery = $articleQuery ->where('title', 'Welcome') ->categories(); // is the same as $categoryQuery = $orm->query('category') ->relatedTo('articles.title', 'Welcome'); // or $articleQuery->where('title', 'Welcome'); $categoryQuery = $orm->query('category') ->relatedTo('articles', $articleQuery);
在此阶段可能看起来有很多选项,但您只需坚持您最喜欢的任何语法即可。生成的查询保持完全相同。
一对多
一对一关系与一对多关系非常相似。主要区别在于,一旦将新项目附加到拥有者,之前的项目就会被解除。一个很好的例子是拍卖品与最高出价人的关系。一旦我们设置了新的最高出价人,旧的一个就会被取消。另一个例子是任务和工人。每个任务可以由单个工人执行。
// Configuration return array( 'models' => array( // ... ), 'relationships' => array( //... array( // mandatory options 'type' => 'oneToOne', 'owner' => 'worker', 'item' => 'task', // The following keys are optional // and will fallback to the defaults // based on model names 'ownerOptions' => array( // the name of the property added to the owner // e.g. $worker->task(); // // Unlike manyToMany this uses // a singular case by default 'itemProperty' => 'task' ), // note it's 'itemOptions' here // but 'itemsOptions' for One To Many 'itemOptions' => array( // the name of the property added to items // e.g. $task->worker() 'ownerProperty' => 'worker', // the field that is used to link items // defaults to '<property>Id' 'ownerKey' => 'workerId', // The default behavior is to set // the field defined in ownerKey to null // when the owner gets deleted // // changed it to 'delete' to delete // the task when its worker is deleted 'onOwnerDelete' => 'update' ) ) ) );
// Using with entities // Getting task worker $worker = $task->worker(); // Getting worker tasks $worker = $task->worker(); // assign worker to a task $task->worker->set($worker); $worker->task->set($task); // Unset task $task->worker->remove(); $work->task->remove();
一对一关系中的接口在双方都是相同的,并且与一对多关系中的拥有者属性相同
多对多
最常见的多对多关系是文章和标签之间的关系。这些关系需要特殊的枢纽表(或MongoDB集合)来存储项目之间的链接。
// Configuration return array( 'models' => array( // ... ), 'relationships' => array( //... array( // mandatory options 'type' => 'manyToMany', 'left' => 'article', 'right' => 'tag', // The following keys are optional // and will fallback to the defaults // based on model names 'leftOptions' => array( 'property' => 'tags' ), 'rightOptions' => array( 'property' => 'articles' ), // depends on property names // defaults to <rightProperty><leftProperty> 'pivot' => 'articlesTags', 'pivotOptions' => array( // defaults to the connection // of the left model 'connection' => 'default', // columns in pivot table // default to '<property>Id' 'leftKey' => 'articleId', 'rightKey' => 'tagId', ) ) ) );
使用多对多关系类似于使用一对多的拥有者方。
// Add $article->tags->add($tag); // Remove a particular tag $article->tags->remove($tag); // Remove all tags from article $article->tags->removeAll(); // Remove all tags from multiple articles $orm->query('article') ->where('status', 'published') ->tags->removeAll(); // Construct a tag query from article query $tagQuery = $orm->query('article') ->where('status', 'published') ->tags(); // Everything else can be used // in the same way as categories // in the one-to-many examples
在这里使用查询进行批量操作,可以使得通过单个数据库调用分配和删除关系成为可能
// Link multiple articles // to multiple tags in one go $articleQuery->tags->add($tagQuery);
为优化这些查询操作投入了大量工作,目前没有其他PHP ORM支持在查询之间编辑关系。而不是需要m*n次查询来编辑多对多关系,查询方法可以一次完成。
MongoDB 中的嵌入式模型
MongoDB支持嵌套文档,这允许使用嵌入式模型。例如,文章的作者
{ "title" : "Welcome", //... "author" : { "name" : "Dracony", //... } }
嵌入式模型只包含实体,没有存储库或查询。子文档和子数组不是自动注册为嵌入式关系的,您需要在配置中完成
return array( 'models' => array( // Some database models 'article' => array(), 'topic' => array(), // Configuring an embedded model 'author' => array( 'type' => 'embedded' ), ), 'relationships' => array( array( 'type' => 'embedsOne', 'owner' => 'article', 'item' => 'author' ), array( 'type' => 'embedsOne', 'owner' => 'topic', 'item' => 'author' ), ) );
嵌入式和数据库模型之间也有一些使用上的差异
// Embedded entities cannot be saved on their own. // Instead you just save the database entity $article->author()->name = 'Dracony'; $article->save(); // Getting the parent model // Conditions are specified as usual $articleQuery ->where('author.name', 'Dracony'); // To specify a subdocument condition // use a '>' separator. // // This will require the article author // to have a stats.totalPost field 'Dracony' $articleQuery ->where('author>stats.totalPost', 'Dracony');
嵌入一个
这是一个与上述文章作者示例类似的子文档关系。
// Configuration return array( 'models' => array( // ... ), 'relationships' => array( //... array( // mandatory options 'type' => 'embedsOne', 'owner' => 'article', 'item' => 'author', // The following keys are optional // and will fallback to the defaults // based on model names 'ownerOptions' => array( // the name of the property added to the owner // e.g. $article->author(); // // Defaults to item model name 'property' => 'task', ), 'itemOptions' => array( // Dot separated path // to the document within owner // // Defaults to owner property name 'path' => 'author' // You can use nested paths // e.g. 'authors.editor' ) ) ) );
注意我们没有为从作者访问文章定义属性。这是因为嵌入式实体始终使用
$entity->owner()
来访问所有者。
// get author from article $author = $article->author(); // get author owner $article = $author->owner(); // remove author $article->author->remove(); // Set an author $article->author->set($author); // Check if an author is set $article->author->exists(); // Create and set new author $author = $article->author->create(); // Create with data $author = $article->author->create($data);
嵌入多个
定义与子文档数组的关系。例如,论坛主题回复
{ "title" : "Welcome", "replies" : [ { "message" : "Hello", "author" : "Dracony" }, { "message" : "World", "author" : "Dracony" } ] }
// Configuration return array( 'models' => array( // ... ), 'relationships' => array( //... array( // mandatory options 'type' => 'embedsMany', 'owner' => 'topic', 'items' => 'reply', // The following keys are optional // and will fallback to the defaults // based on model names 'ownerOptions' => array( // the name of the property added to the owner // e.g. $topic->replies(); // // Defaults to the plural // of the item model name 'property' => 'replies', ), 'itemOptions' => array( // Dot separated path // to the document within owner // // Defaults to owner property name 'path' => 'replies' // You can use nested paths // e.g. 'content.replies' ) ) ) );
// Get replies iterator $replies = $topic->replies(); // Get reply count $topic->replies->count(); // Get reply by offset $reply = $topic->replies->get(2); // Create and add a reply $reply = $topic->replies->create(); // Create reply from data $reply = $topic->replies->create($data); //Create reply at offset $reply = $topic->replies->create($data, 2); // Add reply $topic->replies->add($reply); // Add reply by offset $topic->replies->add($reply, 2); // Remove reply $topic->replies->remove($reply); // Remove multiple replies $topic->replies->remove($replies); // Remove reply by offset $topic->replies->offsetUnset(2); // Remove all replies $topic->replies->removeAll(); // Check if a reply exists $exists = $topic->replies->offsetExists(2); // array access $reply = $topic->replies[1]; $topic->replies[2] = $reply; $exists = isset($topic->replies[2]); unset($topic->replies[2]);
嵌套集合
嵌套集是存储SQL数据库中树的有效方法。一个很好的例子是存储类别和评论树。PHPixie是唯一一个通过命名空间子树来进一步优化它的PHP框架,这使得树插入和更新更快。其背后的代码比通常的嵌套集实现更复杂,但对于用户来说非常简单。
// Configuration return array( 'models' => array( // ... ), 'relationships' => array( //... array( // mandatory options 'type' => 'nestedSet', 'model' => 'category', // Nested Set requires additional // fields to be present in your table. // All of them are INTEGER // These are their default names 'leftKey' => 'left', 'rightKey' => 'right', 'depthKey' => 'depth', 'rootIdKey' => 'rootId', // You can also customize the // relationship property names 'parentProperty' => 'parent', 'childrenProperty' => 'children', // Defaults to "all<plural of parentProperty>" 'allParentsProperty' => 'allParents' // Defaults to "all<childrenProperty>" 'allChildrenProperty' => 'allChildren' ) ) );
这种关系定义了四个关系属性而不是通常的两个。`children`属性指向直接子代,而`allChildren`表示节点的所有子代。同样,`parent`是直接父代,而`allParents`与节点的所有父代相关。
基本用法简单,类似于一对一关系。
// Move child to parent $category->children->add($subcategory); // or $subcategory->parent->set($category); // Remove child from parent $subcategory->parent->remove(); // Remove all children from node $category->children->removeAll(); // Find all root nodes and preload // their children recursively. $categories = $orm->query('category') ->notRelatedTo('parent') // root nodes have no parents ->find(array('children'));
一些更高级的用法
// Get a query representing // all children recursively $allChildrenQuery = $category->children->allQuery(); $allChidlren = $allChildrenQuery->find(); // Same with getting all parents parents $allParents = $category->parent->allQuery()->find(); // Find a category with name 'Trees' that is // a descendant of 'Plants' $query ->where('name', 'Trees') ->relatedTo('allParents', function($query) { $query->where('name', 'Plants'); }) ->find(); // or like this $query ->where('name', 'Plants') ->allChildren() ->where('name', 'Trees') ->find();
删除节点时必须特别注意。PHPixie只允许您删除没有子代或子代也被删除的项目。例如
$plants->children->add($trees); $plants->children->add($flowers); // An exception will be thrown $plants->delete(); // move children away from the node $plants->children->removeAll(); //now it's safe to delete $plants->delete(); // or delete all three, $query->in(array($plants, $trees, $flowers))->delete();
重要的是要记住,`parent`和`children`只指直接父代和子代,而`allParents`和`allChildren`递归地指所有相关节点。