<?php /***** yii guide 关系查询详解(翻译) yii framework relational query (relation ar) translated by phpgcs.com partly (part 4, 5, 6, 7, 8, 9,10) Date: 2013-09-10 Author: phpgcs (php攻城师) blog: http://blog.csdn.net/phpgcs ***/ 1, 声明关系 2, 执行关系查询 3, 执行关系查询(在不获取相关models的情况下) 4, 关系型查询 选项 5, 消除列名歧义 6, 动态 关系查询 选项 7, 关系查询 行为 8, 统计查询 9, 带命名空间的关系查询 10, 使用through进行关系查询 ******/ /****** 1, 声明关系 *****/ 为了使用关系型 AR,我们建议在需要关联的表中定义主键-外键约束。这些约束可以帮助保证相关数据的一致性和完整性。 example ER : entity-relation map http://www.yiiframework.com/tutorial/image?type=guide&version=1.1&lang=en&file=er.png table_name PK FK PK,FK ... tbl_post id user_id title content createTime tbl_user id username passwordemail tbl_category idname tbl_profile id photo website tbl_post_category post_id,category_id 从数据库的角度来说,表 A 和 B 之间有三种关系: 一对多(one-to-many,例如 tbl_user 和 tbl_post) 一对一( one-to-one 例如 tbl_user 和 tbl_profile) 多对多(many-to-many 例如 tbl_category 和 tbl_post)。 在 AR 中,有四种关系: BELONGS_TO(属于): 如果表 A 和 B 之间的关系是一对多,则 表 B 属于 表 A (例如 Post 属于 User); HAS_MANY(有多个): 如果表 A 和 B 之间的关系是一对多,则 A 有多个 B (例如 User 有多个 Post); HAS_ONE(有一个): 这是 HAS_MANY 的一个特例,A 最多有一个 B (例如 User 最多有一个 Profile); MANY_MANY: 这个对应于数据库中的 多对多 关系。 由于多数 DBMS 不直接支持 多对多 关系,因此需要有一个关联表将 多对多 关系分割为 一对多 关系。 在我们的示例数据结构中,tbl_post_category 就是用于此目的的。 在 AR 术语中,我们可以解释 MANY_MANY 为 BELONGS_TO 和 HAS_MANY 的组合。 例如,Post 属于多个(belongs to many) Category ,Category 有多个(has many) Post. AR 中定义关系需要覆盖 CActiveRecord 中的 relations() 方法。 此方法返回一个关系配置数组。每个数组元素通过如下格式表示一个单一的关系。 'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options) 其中 VarName 是关系的名字; RelationType 指定关系类型,可以是一下四个常量之一: self::BELONGS_TO self::HAS_ONE self::HAS_MANY self::MANY_MANY ClassName 是此 AR 类所关联的 AR 类的名字; ForeignKey 指定关系中使用的外键(一个或多个)。 额外的选项可以在每个关系的最后指定(稍后详述)。 class Post extends CActiveRecord { ...... public function relations() { return array( 'author'=>array(self::BELONGS_TO, 'User', 'author_id'), 'categories'=>array(self::MANY_MANY, 'Category', 'tbl_post_category(post_id, category_id)'), ); } } /***** yii guide 关系查询详解(翻译) yii framework relational query (relation ar) translated by phpgcs.com partly (part 4, 5, 6, 7, 8, 10) Date: 2013-09-10 Author: phpgcs (php攻城师) blog: http://blog.csdn.net/phpgcs ***/ class User extends CActiveRecord { ...... public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id'), 'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'), ); } } 信息: 外键可能是复合的,包含两个或更多个列。 这种情况下,我们应该将这些外键名字链接,中间用空格或逗号分割。 对于 MANY_MANY 关系类型, 关联表的名字必须也必须在外键中指定。 例如, Post 中的 categories 关系由外键 tbl_post_category(post_id, category_id) 指定。 AR 类中的关系定义为每个关系向类中隐式添加了一个属性。 在一个关系查询执行后,相应的属性将将被以关联的 AR 实例填充。 例如,如果 $author 代表一个 User AR 实例, 我们可以使用 $author->posts 访问其关联的 Post 实例。 /***** 2, 执行关系查询 ******/ 执行关系查询最简单的方法是读取一个 AR 实例中的关联属性。 如果此属性以前没有被访问过,则一个关系查询将被初始化,它将两个表关联并使用当前 AR 实例的主键过滤。 查询结果将以所关联 AR 类的实例的方式保存到属性中。 这就是传说中的 懒惰式加载(lazy loading,也可译为 迟加载) 方式,例如,关系查询只在关联的对象首次被访问时执行。 下面的例子演示了怎样使用这种方式: // 获取 ID 为 10 的帖子 $post=Post::model()->findByPk(10); // 获取帖子的作者(author): 此处将执行一个关系查询。 $author=$post->author; 信息: 如果关系中没有相关的实例,则相应的属性将为 null 或一个空数组。 BELONGS_TO 和 HAS_ONE 关系的结果是 null, HAS_MANY 和 MANY_MANY 的结果是一个空数组。 注意, "HAS_MANY 和 MANY_MANY 关系返回对象数组,你需要在访问任何属性之前先遍历这些结果。" 否则,你可能会收到 "Trying to get property of non-object(尝试访问非对象的属性)" 错误。 懒惰式加载用起来很方便,但在某些情况下并不高效。 如果我们想获取 N 个帖子的作者,使用这种懒惰式加载将会导致执行 N 个关系查询。 这种情况下,我们应该改为使用 渴求式加载(eager loading)方式。 渴求式加载方式会在获取主 AR 实例的同时获取关联的 AR 实例。 这是通过在使用 AR 中的 find 或 findAll 方法时配合使用 with 方法完成的。 例如: $posts=Post::model()->with('author')->findAll(); 上述代码将返回一个 Post 实例的数组。 与懒惰式加载方式不同,在我们访问每个 Post 实例中的 author 属性之前,它就已经被关联的 User 实例填充了。 渴求式加载通过 一个 关系查询返回所有帖子及其作者,而不是对每个帖子执行一次关系查询。 /***** yii guide 关系查询详解(翻译) yii framework relational query (relation ar) translated by phpgcs.com partly (part 4, 5, 6, 7, 8, 10) Date: 2013-09-10 Author: phpgcs (php攻城师) blog: http://blog.csdn.net/phpgcs ***/ 我们可以在 with() 方法中指定多个关系名字,渴求式加载将一次性全部取回他们。 例如,如下代码会将帖子连同其作者和分类一并取回。 $posts=Post::model()->with('author','categories')->findAll(); 我们也可以实现嵌套的渴求式加载。 像下面这样, 我们传递一个分等级的关系名表达式到 with() 方法,而不是一个关系名列表: $posts=Post::model()->with( 'author.profile', 'author.posts', 'categories')->findAll(); 上述示例将取回所有帖子及其作者和所属分类。 它还同时取回每个作者的简介(author.profile)和帖子(author.posts)。 从版本 1.1.0 开始,渴求式加载也可以通过指定 CDbCriteria::with 的属性执行,就像下面这样: $criteria=new CDbCriteria; $criteria->with=array( 'author.profile', 'author.posts', 'categories', ); $posts=Post::model()->findAll($criteria); 或者 $posts=Post::model()->findAll(array( 'with'=>array( 'author.profile', 'author.posts', 'categories', ) ); /***** 3, Performing Relational query without getting related models ******/ 有时候我们需要关系查询, 但是有不想得到关联的models 假设我们有很多用户,他们发表了很多帖子。 帖子可以是发布的或者是草稿,2种状态由 post model 中的 publised 列决定。 现在我们需要获得所有已经发布过帖子的用户 但是不希望得到任何 posts 自己的信息。 我们可以这样做: $users=User::model()->with(array( 'posts'=>array( // we don't want to select posts 'select'=>false, // but want to get only users with published posts 'joinType'=>'INNER JOIN', 'condition'=>'posts.published=1', ), ))->findAll(); /***** 4, 关系型查询选项 ******/ 我们提到在关系声明时可以指定附加的选项。这些 名-值 对形式的选项用于自定义关系型查询。概括如下: select: 关联的 AR 类中要选择(select)的列的列表。 默认为 '*',即选择所有列。此选项中的列名应该是已经消除歧义的。 condition: 即 WHERE 条件。默认为空。此选项中的列名应该是已经消除歧义的。 params: 要绑定到所生成的 SQL 语句的参数。应该以 名-值 对数组的形式赋值。此选项从 1.0.3 版起有效。 on: 即 ON 语句。此处指定的条件将会通过 AND 操作符附加到 join 条件中。此选项中的列名应该是已经消除歧义的。 此选项不会应用到 MANY_MANY 关系中。此选项从 1.0.2 版起有效。 order: 即 ORDER BY 语句。默认为空。 此选项中的列名应该是已经消除歧义的。 with: a list of child related objects that should be loaded together with this object. Be aware that using this option inappropriately may form an infinite relation loop. joinType: type of join for this relationship. It defaults to LEFT OUTER JOIN. alias: the alias for the table associated with this relationship. This option has been available since version 1.0.1. It defaults to null, meaning the table alias is the same as the relation name. together: whether the table associated with this relationship should be forced to join together with the primary table and other tables. This option is only meaningful for HAS_MANY and MANY_MANY relations. If this option is set false, the table associated with the HAS_MANY or MANY_MANY relation will be joined with the primary table in a separate SQL query, which may improve the overall query performance since less duplicated data is returned. If this option is set true, the associated table will always be joined with the primary table in a single SQL query, even if the primary table is paginated. If this option is not set, the associated table will be joined with the primary table in a single SQL query only when the primary table is not paginated. For more details, see the section "Relational Query Performance". This option has been available since version 1.0.3. join: the extra JOIN clause. It defaults to empty. This option has been available since version 1.1.3. group: the GROUP BY clause. It defaults to empty. Column names referenced in this option should be disambiguated. having: the HAVING clause. It defaults to empty. Column names referenced in this option should be disambiguated. Note: option has been available since version 1.0.1. index: the name of the column whose values should be used as keys of the array that stores related objects. Without setting this option, an related object array would use zero-based integer index. This option can only be set for HAS_MANY and MANY_MANY relations. This option has been available since version 1.0.7. In addition, the following options are available for certain relationships during lazy loading: limit: limit of the rows to be selected. This option does NOT apply to BELONGS_TO relation. offset: offset of the rows to be selected. This option does NOT apply to BELONGS_TO relation. Below we modify the posts relationship declaration in the User by including some of the above options: class User extends CActiveRecord { public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id', 'order'=>'posts.create_time DESC', 'with'=>'categories'), 'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'), ); } } Now if we access $author->posts, we would obtain the author s posts sorted according to their creation time in descending order. Each post instance also has its categories loaded. /**** 5, Disambiguating Column Names 歧义列名 ******/ 当一个列在2个以上的表连接时出现的话, 需要消除歧义。一般是用表前缀来实现。 在关联AR查询中,主表的别名固定是 t , 关联表的别名默认是跟对应的关系名一样。 例如,在下面的描述中,Post 的别名 和 Coment 的别名分别是 t 和 comments $posts=Post::model()->with('comments')->findAll(); 现在我们假设Post 和Comment 都有一列 create_time ,我们想要先按post时间排序再按comment时间排序的 posts, 我们就需要消除 create_time 的歧义了。如下: $posts=Post::model()->with('comments')->findAll(array( 'order'=>'t.create_time, comments.create_time' )); 注意, 从 1.1.0 版本以后 消除歧义的行为改变了。 在之前, 默认的Yii会自动生成一个表别名为每一个关系表,而且我们必须使用前缀?? 来指向这个自动生成的别名;而且,之前的版本,主表的别名就是表名本身,而不是 t。 /*** 6, 动态关系查询选项 Dynamic Relational Query Options ***/ 从1.0.2版本开始,我们可以 在 with() 和 with option 中使用动态的关系查询选项。 动态选项会重写已经存在于 relations() 方法中的 options 例如, 用上面的User Model , 如果我们想要用 eager loading 方法找到 属于某一个作者升序排列(in ascending order)的帖子 (前提是我们在 关系选项中已经定义 为降序 了 descending order) 我们可以临时这样做: User::model()->with(array( 'posts'=>array('order'=>'posts.create_time ASC'), 'profile', ))->findAll(); 从1.0.5开始, 动态查询选项 也可以被用于 lazy loading 方法 进行关系查询的场合。 为了达到这样的目的, 我们应该调用一个名字跟关系名一样的方法 并且 把动态查询选项 作为方法的参数传递。 例如,如下的代码返回了一个 status = 1 的用户的帖子: $user=User::model()->findByPk(1); $posts=$user->posts(array('condition'=>'status=1')); /**** 7, Relational Query Performance 关系查询行为 ****/ 如上所述, eager loading 方法 主要用于 我们需要很多关系对象的场合。 它通过联合所有需要的表 生成了一个复杂的SQL 表述。 一个复杂的SQL 表述在很多场合是 更合适的 因为它基于一个关联表的列 简化了数据的筛选 但是有些情况下, 复杂SQL 还是不够高效的。 考虑这样一个例子, 我们需要找到最新的帖子,同时还有它们的评论。 假设,每个帖子有10个评论, 用一个简单的大的SQL , 我们找到许多冗余的帖子数据因为每一个帖子会为它所拥有的每一个评论而重复。 现在让我们试试另一种方法: 首先查询最新的帖子, 在查询他们的评论。这样,我们需要执行 2个SQL 查询。好处是,没有冗余查询结果啦。 因此, 哪个方法更高效? 没有固定的答案。 执行一个简单的大SQL 可能更高效是因为它让DBMS在解析和执行SQL时开销较少; 另一方面, 使用简单大SQL , 我们最终得到更多的冗余数据, 并且因此需要更多的时间来读取和处理。 基于这个原因,我们提供了 together query option 查询选项, 因此我们可以根据需要在两个方法中选择。 默认地, yii 采用了第一种方法, 即, 生成一个单独的SQL 来执行 eager loading。 我们可以在relation declarations 中设置 together option 为 false , 从而达到 一些表在不同的SQL 中被joined 的目的。 还是这个例子: public function relations() { return array( 'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false), ); } 我们也可以动态地在我们执行 eager loading 时设置选项: $posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll(); Note: In version 1.0.x, the default behavior is that Yii will generate and execute N+1 SQL statements if there are N HAS_MANY or MANY_MANY relations. Each HAS_MANY or MANY_MANY relation has its own SQL statement. By calling the together() method after with(), we can enforce only a single SQL statement is generated and executed. For example, $posts=Post::model()->with( 'author.profile', 'author.posts', 'categories')->together()->findAll(); /***** 8, Statistical Query 统计查询 ****/ 从1.0.4开始支持统计查询 除了上面提到的关系查询, yii 也支持所谓的统计查询(或者叫聚合查询)。 指的是检索相关对象的聚合信息, 如每一篇帖子的评论数量, 每一个产品的平均分数, 等等。 只有在一对多的关系下 才可以用统计查询。 (例如,一个帖子有很多评论)或者 MANY_MANY 多对多的情况下(例如一个帖子有多个类别并且一个类别有多个帖子) 执行统计查询跟之前说的关系查询非常类似。 我们首先需要 在 CActiveRecord relations() 中像声明关系查询一样 声明 统计查询 class Post extends CActiveRecord { public function relations() { return array( 'commentCount'=>array(self::STAT, 'Comment', 'post_id'), 'categoryCount'=>array(self::STAT, 'Category', 'post_category(post_id, category_id)'), ); } } 这里,我们声明了2个统计查询: commentCount 计算属于一个帖子的评论数量; categoryCount 计算一个帖子所归属的类目的数量。 注意这里 post 和 comment 是 HAS_MANY 的关系, 而post 和category 是MANY_MANY 的关系 可以看出, 这里的声明跟我们在之前声明关系查询的适合非常相似, 不同的只是 relation type 为 STAT 而不是 BELONG_TO & MANY_MANY 有了上面的声明, 我们可以获得一个帖子评论的数量 $post->commentCount 当我们第一次使用这个属性的时候, 一个SQL 会默默地执行来统计对应的结果。 我们知道, 这是lazy loading 方法。 我们也可以用 eager loading 方法 ,如果我们需要 多个帖子的评论数量的时候。 $posts=Post::model()->with('commentCount', 'categoryCount')->findAll(); 上面的语句会执行 3个SQL 并带回 所有的帖子以及相关的评论数量和类别数量。 使用lazy loading 方法,如果我们有N 个帖子的花, 最终会 使用 2*N+1 个 SQL 查询。 默认地, 一个统计查询会计算 COUNT 表达式。 我们可以通过在 relations() 中声明统计查询的时候 定义几个额外的 选项 来定制这个计算。 可用的选项如下: select: the statistical expression. Defaults to COUNT(*), meaning the count of child objects. defaultValue: the value to be assigned to those records that do not receive a statistical query result. For example, if a post does not have any comments, its commentCount would receive this value. The default value for this option is 0. condition: the WHERE clause. It defaults to empty. params: the parameters to be bound to the generated SQL statement. This should be given as an array of name-value pairs. order: the ORDER BY clause. It defaults to empty. group: the GROUP BY clause. It defaults to empty. having: the HAVING clause. It defaults to empty. /***** 9, Relational Query with Named Scopes 带命名空间的关系查询 ****/ 版本支持: from 1.0.5 有2中形式的命名空间关系查询。 1, 命名空间应用于main model 2, 命名空间应用于related model 下面的代码展示如何让命名空间应用与 main model $posts=Post::model()->published()->recently()->with('comments')->findAll(); 这非常类似于 非关系查询。 唯一的不同是我们在 命名空间链 后面 用了with() 方法。 这个方法带回最近发布的帖子连同它们的评论。 下面的代码展示如何让命名空间应用与 related model $posts=Post::model()->with('comments:recently:approved')->findAll(); 这个查询返回的是 所有帖子连同它们被审批通过的评论。 这里的 comments 是 relation 的名字, 而recently 和 approved 是 2个声明在 Comment model Class 中的命名空间 ;它们之间应该用 colons 链接。 命名空间也可以在声明于 CActiveRecord::relations()中的 relation rules 中的 with option 中定制。 下面的例子中,我们用 $user->posts, 将带回所有帖子被审批通过的评论。 class User extends CActiveRecord { public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id', 'with'=>'comments:approved'), ); } } 注意: 应用与 related models 的命名空间必须在CActiveRecord::scopes 中定制,也因为这样, 它们不可以被参数化parameterized。 /***** 10, Relational Query with through ****/ 用了 through 的 relation definition : 'comments'=>array(self::HAS_MANY,'Comment',array('key1'=>'key2'),'through'=>'posts'), 上面的 array('key1'=>'key2'): key1 is a key defined in relation specified in through (posts is this case). key2 is a key defined in a model relation points to (Comment in this case). through 可用于 HAS_ONE, BELONGS_TO and HAS_MANY 3种关系 10.1 HAS_MANY through HAS_MANY through ER http://www.yiiframework.com/tutorial/image?type=guide&version=1.1&lang=en&file=has_many_through.png PK FKPK,FK ... group group_id name role user_id group_id user user_id username password comment id user_id content 一个HAS_MANY with through 的例子是 当 users 通过 roles 进入groups 的时候 从这个 group 中得到 users。 一个复杂点的例子是 获得 某个 group 的所有的 users 的所有的 comments。 这种情况下, 我们必须 在一个单独的 model 中 使用 几个relations with through class Group extends CActiveRecord { ... public function relations() { return array( 'roles'=>array(self::HAS_MANY, 'Role','group_id'), 'users'=>array(self::HAS_MANY, 'User',array('user_id'=>'id'),'through'=>'roles'), 'comments'=>array(self::HAS_MANY, 'Comment', array('id'=>'user_id'),'through'=>'users'), ); } } 使用案例: // get all groups with all corresponding users $groups=Group::model()->with('users')->findAll(); // get all groups with all corresponding users and roles $groups=Group::model()->with('roles','users')->findAll(); // get all users and roles where group ID is 1 $group=Group::model()->findByPk(1); $users=$group->users; $roles=$group->roles; // get all comments where group ID is 1 $group=Group::model()->findByPk(1); $comments=$group->comments; 10.2 HAS_ONE through HAS_ONE through ER http://www.yiiframework.com/tutorial/image?type=guide&version=1.1&lang=en&file=has_one_through.png 一个 HAS_ONE with through 的例子是 当 user 在profile 中 绑定自己的地址时, 获取 user 的地址。 所有的 实体 (user, profile, and address) 确实有想对应的 models: class User extends CActiveRecord { ... public function relations() { return array( 'profile'=>array(self::HAS_ONE,'Profile','user_id'), 'address'=>array(self::HAS_ONE,'Address',array('id'=>'profile_id'),'through'=>'profile'), ); } } Usage examples // get address of a user whose ID is 1 $user=User::model()->findByPk(1); $address=$user->address; 10.3 through on self through 也可以通过一个桥梁 model 用于绑定到自己的model 这次的案例是 一个user 选择其他 users 做自己的老师。 through self ER http://www.yiiframework.com/tutorial/image?type=guide&version=1.1&lang=en&file=through_self.png 这种情况下我们可以这样定义 relations class User extends CActiveRecord { ... public function relations() { return array( 'mentorships'=>array( self::HAS_MANY,'Mentorship','teacher_id','joinType'=>'INNER JOIN' ), 'students'=>array( self::HAS_MANY,'User',array('student_id'=>'id'), 'through'=>'mentorships','joinType'=>'INNER JOIN' ), ); } } /***** yii guide 关系查询详解(翻译) yii framework relational query (relation ar) translated by phpgcs.com partly (part 4, 5, 6, 7, 8, 10) Date: 2013-09-10 Author: phpgcs (php攻城师) blog: http://blog.csdn.net/phpgcs ***/ Usage examples // get all students taught by teacher whose ID is 1 $teacher=User::model()->findByPk(1); $students=$teacher->students;