您的位置:首页 > 数据库

数据库之关系型AR

2012-11-17 03:13 537 查看
我们已经知道了如何用AR来读取单个数据表中的数据。在本节中,我们将介绍如何用AR来读取多个关联表的数据。

要用关系型AR的话,强烈建议那些需要连接的表,做好主-外键的约束关系。这种约束关系可以保证这些关联数据的一致性。为了方便易懂,我们用一张数据库的结构图来作为本节的关系图。

1.1 声明关系

在我们用AR来做关联查询之前,我们要让AR知道一个AR类是怎么关联另外一个类的。





两个AR类之间的关系,直接代表了数据库中两个表的关系。从数据库的角度看,两张表A与B之间有3中关系:one-to-many(e.g. tbl user and tblpost) , one-to-one (e.g. tbl user and tbl profile) 以及many-to-many (e.g. tbl categoryand tbl post)。 在AR中,有4中关系类型:

● BELONGS TO: 如果两个表之间的关系是 one-to-many,那么 B belongsto A (e.g. Post belongs to User);

● HAS MANY: if the relationship between table A and B is one-to-many, then A has manyB (e.g. User has many Post);

● HAS ONE: this is special case of HAS MANY where A has at most one B (e.g. User hasat most one Profile);

● MANY MANY: this corresponds to the many-to-many relationship in database. An as-sociative table is needed to break a many-to-many relationship into one-to-many relationships, as most DBMS do not surpport many-to-many relationship directly.In our example database schema, the tbl post category serves for this purpose. In AR terminology, we can explain MANY MANY as the combination of BELONGS TO and HAS MANY. For example, Post belongs to many Category and Category has many Post.

声明AR关系,涉及到重载CActiveRecord的relations()的方法。该方法返回一组关系配置。每个数组元素表示一条对应关系:

'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options)

VarName:关系的名称

RelationType:表明这条关系是哪个类型(之前提到的4种关系类型)

ClassName:关联的AR类名。

ForeignKey:关联的外键

下面的代码演示了如果定义User 和Post的关系。

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)'),
);
}
}
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'),
);
}
}

INFO:外键可能是一个联合组建。

1.2 执行关联查询

最简单的关联查询方法是读取一个关联属性的AR实例。如果该属性之前没被访问过,一个关联插叙会被初始化,用来关联这个AR实例所关联的表。查询结果会保存到对应的AR实例。这个就是著名的后装载方法。关联操作只有在对应的对象被访问时才执行操作。下面示范一下怎么使用这个方法:

// retrieve the post whose ID is 10
$post=Post::model()->findByPk(10);
// retrieve the post's author: a relational query will be performed here
$author=$post->author;

INFO:如果不存在对应关系的实例,那么当前的属性可能是null或者是一个空的数组。对于BELONG_TO和HAS_ONE,结果是null,对于HAS_MANY和MANY_MANY,结果是一个空的数组。请注意,HAS_MANY以及MANY_MANY返回的是一组对象,在想调用他们的属性之前,必须先遍历数组。否则,你会得到"Trying to get property of non-object"的错误提示。

后装载的方法使用很便捷,但是在某些场景下不是很高效。例如,我们想要知道N篇文章的作者,我们必须调用N个关联查询语句。在这种情况下,我们应该调用的是先装载的方法。

预装载的方法,事先就捕获了相关的AR实例,以及主的AR实例。这些是有whth()方法加上一个find()或者是findAll()的方法实现的。例如:

$posts=Post::model()->with('author')->findAll();

上面的代码会返回一个Post的对象数组。跟后装载不同的是,在我们调用author这个属性之前,他就已经被关联的User对象给装载好了。预装载的方法,在实例化的时候就把所有该作者的文章都给关联出来了,在一个但一个查询的语句中。我们可以在with()方法中指定多个关系,预装载会一次把他们给查询出来。例如,接下来的代码将实现返回文章,以及关联的作者,文章分类。

$posts=Post::model()->with('author','categories')->findAll();


我们也可以实现嵌套的预装载。我们不用一串关联名称的方法,我们通过向with方法传递一个结构来代表这些关系,如下:

$posts=Post::model()->with(
'author.profile',
'author.posts',
'categories')->findAll();

上面的示例中,将返回所有的文章(Post)以及他们的作者,分类。同时也返回了作者的配置信息以及他的所有文章。

预装载也可以用设定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',
)
));

1.3 不获取相关模型的情况下执行关联查询

有些时候,我们想执行关联查询的动作,但是又不想获取涉及的模型。假设我们的Users有很多的Posts。post可以是发布状态,也可能是草稿的状态。这取决于Posts表中的published字段。现在我们想得到发布过文章的Users,但是又对他所发布的文章一点兴趣都没有,这时候我们可以这样做:

$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();

1.4 关联查询选项

我们提到过,可选选项可以用来指明关系类型。这些选项,也是用name-value的格式。总结如下:

SELECT , CONDITION, PARAMS , PARAMS , ON , ORDER , WITH , JOINTYPE , ALIAS , TOGETHER , JOIN , GROUP , HAVING , INDEX , SCOPES ,

另外,后装载的选项:limit , offset , through

下例中,我们用上面的一些方法来修改posts的User关系:

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'),
);
}
}

现在,如果我们访问$author->posts, 我们会获取到该作者的所有文章,按照时间倒序。每篇文章实例的分类也都已经装载好了。

1.5 理清冗余的字段名

当一个字段名在连接的多个表中出现时,需要理清楚的。这些通过通过在字段名前加上表别名来实现。

在关联AR查询时,主表的别名一般为t ,而相关联的表别名就是默认名。例如,下面表的别名对应为Post t, Coments coments:

$posts=Post::model()->with('comments')->findAll();

好了,现在假设Post 以及Coment都有一个字段名为create_time,用来记录创建的时间,现在我们都想获得这两个时间,先按照Post的时间,然后再是Coment的时间。我们需要理清楚create_time这个字段:

$posts=Post::model()->with('comments')->findAll(array(
'order'=>'t.create_time, comments.create_time'
));

1.6 动态关联查询

我们可以用with()以及其选项的方法来实现动态查询。这些动态选项会覆盖现有的relations方法。例如,上例中User模型,如果我们想用预装载的方法,获取某个作者的所有Posts,并且按照时间升序排列(在relations方法中,是按照时间降序),我们可以这样做

User::model()->with(array(
'posts'=>array('order'=>'posts.create time ASC'),
'profile',
))->findAll();

动态选项也可以用在后装载的方法中。在这里,我们需要调用一个跟关系名同名的方法,向这个方法传递动态参数。例如,下例中返回user的状态为1的posts

$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

1.7 关联查询执行

如此前所说的,我们在查询关联多个对象的时候,一般采用预装载的方法。他生成了一个很长的语句,把所有用到的表都join起来。如果只是基于一个表字段的过滤,看起来是很合适的。但是,很多情况下他是很低效的。

考虑一种情况,我们想获得最近的一些Posts,同时还有他们的Coments。假设每篇Post有10条Coments,用一长句的SQL,会带来很多多余的Post,然后这些多余的Post还会很多coment。现在让我们来尝试另一种方法:我们先查询最新的Posts,然后查询他们的Coments。在这个新的方法中,我们需要执行2次SQL语句,好处是没有多余的结果出来。

问题来了,到底哪种方法才是最佳的?这个是没有决定的答案的。用一长句的SQL,对于底层的数据库来说,解析与执行都会比较高效。但另一方面,用这样单句SQL查询出来的结果,有很多多余的数据,要消耗时间来读取处理他们。

出于这个原因,Yii提供了together的方法,让我们选择要用哪种方法。默认情况下,Yii采用预装载的方式,生成一个单句的SQL, 希望在主模型中有LIMIT。我们可以再relation中,设置together的默认值为true。这样,就算LIMIT后,还是强制生成单句的SQL。设置together·为false,会生成这些表的各自SQL。例如,想用分开的语句查询最新的Posts以及他们的评论,我们可以在Post的relation中声明comments:

public function relations()
{
return array(
'comments' => array(self::HAS MANY, 'Comment', 'post id', 'together'=>false),
);
}

也可以在预装载的调用中动态设置这个选项:

$posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll();

1.8 静态查询

除了上述的关联查询,Yii也支持所谓的静态查询(聚集查询)。这个涉及到访问相关对象的聚集信息,例如每篇Post包含了多少comments,每个产品的平均级别等。静态查询只有在HAS_MANY(post has many coments)或者是MANY_MANY(e.g. a post belongs to many categories and a category has many posts).的关系中才适用。

执行静态查询跟我们之前说的关联查询非常的相似。首先,我们得像关联查询一样的,在CActiveRecord中的relation方法中声明这个静态查询。

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, categoryid)'),
);
}
}

在上面的代码中,我们声明了两个静态查询:commentCount,统计这篇文章的评论总数,categoryCount统计这篇文章所属分类的总数。请注意,Post跟Coment的关系是HAS_ONE, 跟Category的关系是MANY_MANY。可以看出,这个说明跟我们上一章节中所看到的非常相似。唯一的区别在于关系类型是STAT。

在上面的声明之后,我们可以通过调用$post->commentCount来获得这篇文章的评论数。当我们第一次调用这个属性的时候,一个SQL语句会被隐式的执行来获取这个结果。我们已经知道了,这种所谓的后装载方法。如果我们想获得很多文章的评论数,也可以用预装载的方式:

$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();

上述语句将会执行3条SQL语句,返回posts,以及他的评论数,所属分类数。如果用后装载的方式,如果有N条Post记录,我们要执行2*N+1的SQL语句来得到。

默认情况下,一个静态查询会计算统计数(如上例中的评论数以及分类数)。我们可以在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 emptyempty.
● 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 emptyempty.
● group: the GROUP BY clause. It defaults to emptyempty.
● having: the HAVING clause. It defaults to emptyempty.

1.9 用命名空间来关联查询

关联查询也可以结合命名空间来实现,有两种实现方式。第一种,命名空间是在主模型中,另外一种,命名空间在所关联的模型中。

下面的代码演示如何在主模型应用命名空间:

$posts=Post::model()->published()->recently()->with('comments')->findAll();

这非常像非关联查询,是吧?唯一的区别在于,在命名空间链后面,我们有with的方法。这条语句将返回最近发布的post以及他们的coments。

另外,下面的代码演示如何在被关联的模型应用命名空间。

$posts=Post::model()->with('comments:recently:approved')->findAll();
// or since 1.1.7
$posts=Post::model()->with(array(
'comments'=>array(
'scopes'=>array('recently','approved')
),
))->findAll();
// or since 1.1.7
$posts=Post::model()->findAll(array(
'with'=>array(
'comments'=>array(
'scopes'=>array('recently','approved')
),
),
));

上面的代码会返回Posts,以及他们的被通过的评论。注意到,coments是被关联的名称,而recently, approved是在coments里声明的命名空间。他们之间要用冒号分割。

命名空间也可以在CActiveRecord::relations()的规则中,用with选项来声明。下面的例子中,如果我们调用$user->posts, 他会返回posts的所有验证通过的评论。

class User extends CActiveRecord
{
public function relations()
{
return array(
'posts'=>array(self::HAS MANY, 'Post', 'author id',
'with'=>'comments:approved'),
);
}
}
// or since 1.1.7
class User extends CActiveRecord
{
public function relations()
{
return array(
'posts'=>array(self::HAS MANY, 'Post', 'author id',
'with'=>array(
'comments'=>array(
'scopes'=>'approved'
),
),
),
);
}
}

从1.1.7开始,yii可以向关联的命名空间传递参数。例如,你在Post模型中,有一个叫做rated的命名空间,用来规定只接受规定级别的文章,可以在User中调用:

$users=User::model()->findAll(array(
'with'=>array(
'posts'=>array(
'scopes'=>array(
'rated'=>5,
),
),
),
));

1.10 通过through关联查询

用through的时候,关系定义如下:

'comments'=>array(self::HAS MANY,'Comment',array('key1'=>'key2'),'through'=>'posts'),

在上面代码中的array('key1'=>'key2'):

key1:through中的relation定义的key(本例中的posts)

key2:关联模型的key(本例中的coment)

through可以用在HAS_ONE和HAS_MANY的关系中。





一个例子表明HAS_MANY的是,当用户通过角色分组是,选出特定组的用户。更复杂一点的例子,获取特定组用户的所有评论。下面的例子中,我们将示范在单一模型中,如何用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;






HAS_ONE的例子,例如,通过through,来调用user绑定的配置资料中的address。所有这些实体都有对应的模型。

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'),
);
}
}

// get address of a user whose ID is 1
$user=User::model()->findByPk(1);
$address=$user->address;

through 自己

through可以通过绑定一个桥接模型,用于 自身。在本例中是一个用户作为另一个用户的导师





我们按照以下的方法声明他们的关系:

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'),
);
}
}
// get all students taught by teacher whose ID is 1
$teacher=User::model()->findByPk(1);
$students=$teacher->students;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  AR builder query