>  기사  >  백엔드 개발  >  Yii 프레임워크 공식 가이드 시리즈 26 - 데이터베이스 사용: 관계형 활성 레코드

Yii 프레임워크 공식 가이드 시리즈 26 - 데이터베이스 사용: 관계형 활성 레코드

黄舟
黄舟원래의
2017-02-15 09:09:291094검색



AR(Active Record)을 사용하여 단일 데이터 테이블에서 데이터를 가져오는 방법을 살펴보았습니다. 이 섹션에서는 AR을 사용하여 여러 관련 데이터 테이블을 연결하고 조인된 데이터 세트를 검색하는 방법을 설명합니다.

관계형 AR을 사용하려면 관련이 필요한 테이블에 기본 키-외래 키 제약 조건을 정의하는 것이 좋습니다. 이러한 제약 조건은 관련 데이터의 일관성과 무결성을 보장하는 데 도움이 될 수 있습니다.

간결함을 위해 아래 표시된 엔터티-관계(ER) 다이어그램의 데이터 구조를 사용하여 이 섹션의 예를 보여줍니다.

Yii 프레임워크 공식 가이드 시리즈 26 - 데이터베이스 사용: 관계형 활성 레코드

정보: 외래 키 제약 조건에 대한 지원은 DBMS마다 다릅니다. SQLite

1. 관계 선언

AR을 사용하여 관련 쿼리를 수행하기 전에 AR 클래스가 다른 AR 클래스와 어떻게 관련되어 있는지 AR에 알려야 합니다.

두 AR 클래스 간의 관계는 AR 클래스가 표현하는 데이터 테이블 간의 관계를 통해 직접적으로 연관됩니다. 데이터베이스 관점에서 테이블 A와 B 사이에는 일대다(예: tbl_user 및 tbl_post), 일대일(예: tbl_user 및 tbl_profile) 및 다대다(예: 다대다) 관계의 세 가지 유형이 있습니다. 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의 관계() 메서드를 재정의해야 합니다. 이 메서드는 관계 구성의 배열을 반환합니다. 각 배열 요소는 다음 형식으로 단일 관계를 나타냅니다.

'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는 관계에 사용되는 외래 키(하나 이상)를 지정합니다. 각 관계가 끝날 때 추가 옵션을 지정할 수 있습니다(나중에 자세히 설명).

다음 코드는 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'),
        );
    }
}

정보: 외래 키는 두 개 이상의 열을 포함하는 복합 키일 수 있습니다. 이 경우 공백이나 쉼표로 구분하여 외래 키 이름을 연결해야 합니다. MANY_MANY 관계 유형의 경우 관련 테이블의 이름도 외래 키에 지정되어야 합니다. 예를 들어 Post의 카테고리 관계는 외래 키 tbl_post_category(post_id, Category_id)로 지정됩니다.

AR 클래스의 관계 정의는 암시적으로 각 관계의 클래스에 속성을 추가합니다. 상관 관계 쿼리가 실행되면 해당 속성이 연결된 AR 인스턴스로 채워집니다. 예를 들어 $author가 사용자 AR 인스턴스를 나타내는 경우 $author->posts를 사용하여 연결된 Post 인스턴스에 액세스할 수 있습니다.

2. 관련 쿼리 수행

관련 쿼리를 수행하는 가장 쉬운 방법은 AR 인스턴스에서 관련 속성을 읽는 것입니다. 이전에 이 속성에 액세스한 적이 없는 경우 현재 AR 인스턴스의 기본 키를 사용하여 두 테이블을 조인하고 필터링하는 조인 쿼리가 초기화됩니다. 쿼리 결과는 연결된 AR 클래스의 인스턴스로 속성에 저장됩니다. 이는 전설적인 지연 로딩(늦은 로딩이라고도 번역됨) 방법입니다. 예를 들어 관련 쿼리는 관련 개체에 처음 액세스할 때만 실행됩니다. 다음 예에서는 이 접근 방식을 사용하는 방법을 보여줍니다.

// 获取 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 관계는 객체 배열을 반환하므로 속성에 액세스하기 전에 이러한 결과를 반복해야 합니다. 그렇지 않으면 "객체가 아닌 속성을 가져오는 중" 오류가 나타날 수 있습니다.

지연 로딩은 편리하지만 어떤 경우에는 효율적이지 않습니다. N개의 게시물 작성자를 얻으려는 경우 이 지연 로딩을 사용하면 N개의 조인 쿼리가 실행됩니다. 이 경우 대신 Eager 로딩을 사용해야 합니다.

eager loading 메서드는 기본 AR 인스턴스를 가져오는 동시에 관련 AR 인스턴스도 가져옵니다. 이는 AR에서 find 또는 findAll 메소드를 사용할 때 with 메소드를 사용하여 수행됩니다. 예:

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

위 코드는 Post 인스턴스 배열을 반환합니다. 지연 로딩과 달리 각 Post 인스턴스의 작성자 속성에 액세스하기 전에 이미 연결된 User 인스턴스로 채워져 있습니다. 즉시 로드는 각 게시물에 대해 조인 쿼리를 수행하는 대신 단일 조인 쿼리를 통해 모든 게시물과 해당 작성자를 반환합니다.

with() 메소드에 여러 관계 이름을 지정할 수 있으며, Eager 로딩은 해당 이름을 모두 한 번에 검색합니다. 예를 들어 다음 코드는 작성자 및 카테고리와 함께 게시물을 검색합니다.

$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. 关系型查询选项

我们提到在关系声明时可以指定附加的选项。这些 名-值 对形式的选项用于自定义关系型查询。概括如下:

select: 关联的 AR 类中要选择(select)的列的列表。 默认为 '*',即选择所有列。此选项中的列名应该是已经消除歧义的。

condition: 即 WHERE 条件。默认为空。此选项中的列名应该是已经消除歧义的。

params: 要绑定到所生成的 SQL 语句的参数。应该以 名-值 对数组的形式赋值。此选项从 1.0.3 版起有效。

on: 即 ON 语句。此处指定的条件将会通过 AND 操作符附加到 join 条件中。此选项中的列名应该是已经消除歧义的。此选项不会应用到 MANY_MANY 关系中。此选项从 1.0.2 版起有效。

order: 即 ORDER BY 语句。默认为空。 此选项中的列名应该是已经消除歧义的。

with: 应该和该对象一起加载的一些列子相关对象. 注意不适当的使用该选项可能造成无限关系循环.

joinType: 该关系的join类型. 默认是LEFT OUTER JOIN.

alias: 和该关系关联的表的别名. 这个选项从yii版本1.0.1起有效. 默认是null, 意味着表别名和关系名称一样.

together: 该关系所关联的表是否应该强制和主表和其他表联接. 这个选项只对HAS_MANY 和 MANY_MANY 这两种关系有意义. 如果这个选项设置为false, 
那么HAS_MANY或者 MANY_MANY 关系所关联的表将会和主表在相互隔离的SQL查询中联接, 这将会提高整个查询的性能,因为这会返回较少的重复数据. 
如果这个选项设置为true, 关联的表总会和主表联接在一个SQL查询中, 即使主表是分页的. 如果这个选项没有设置,
关联表只有主表不是分页的情况下才会和主表联接在一个SQL查询中. 更多细节,请查看章节 "关系查询性能". 这个选项从版本1.0.3开始支持.

join: 额外的 JOIN 条款. 默认是空. 这个选项从版本1.1.3开始支持.

group: GROUP BY 条款. 默认是空. 在该选项中列名的使用应该是无歧义的.

having: HAVING 条款. 默认是空. 在该选项中列名的使用应该是无歧义的. 注意: 从版本1.0.1开始支持该选项.

index: 列名被用于存储关系对象数组的键值. 如果表不设置这个选项, 关系对象数组将会使用从0开始的整型索引.这个选项只能用于设置HAS_MANY 和 MANY_MANY 关系类型. 
yii框架从版本1.0.7以后开始支持该选项

此外, 下面这些选项在懒惰式加载中对特定关系是有效的:

limit: 被查询的行数限制. 这个选项不能用于 BELONGS_TO 关系.

offset: 被查询的起始行.这个选项不能用于 BELONGS_TO 关系.

下面我们通过加入上述的一些选项来修改 User中的posts 关系声明:

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, 就会得到基于创建时间排序的author's posts,并且每一个post 实例都会加载其分类.

4. 列名歧义

当两表或多表联接出现同一个列名时, 需要排除歧义.这可以通过给列名加上表别名前缀来实现.

在关系 AR 查询中, 主表别名默认是 t, 同时关系表的别名默认是相应的关系名称.例如, 在下面的语句中, Post 和 Comment的别名分别是t 和 comments:

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

现在假设 Post 和 Comment 都有一个表明其创建时间的列叫做create_time,并且我们想要将posts和其对应的comments放在一起查询,排序方式首先是posts的创建时间,然后是comments的创建时间。我们需要按照如下方式消除列名歧义:

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

注意: 列歧义的行为从版本1.1.0起有所改变. 在版本1.0.x中,默认Yii会为每一个关系表自动生成表别名, 并且我们必须使用前缀??. 来引用自动生成的别名. 此外,在1.0.x版本中, 主表别名就是表名本身

5. 动态关系查询选项

从版本1.0.2开始,我们可以在with()和with选项中使用动态关系查询选项. 动态选项会覆盖已存在的如relations()方法中所指定的选项. 例如,在上面的User 模型中, 如果我们想要使用渴求式加载方式以升序为每一个author附带 posts (在关系定义中默认排序是降序), 可以按照如下方式:

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

从版本1.0.5开始, 动态查询选项可以在使用懒惰式加载方式进行关系查询的时候使用. 我们可以调用一个关系名称相同的方法并且传入动态查询选项作为参数. 例如, 下面的代码返回status为1的 user's posts:

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

6. 关系查询性能

As we described above, the eager loading approach is mainly used in the scenario when we need to access many related objects. It generates a big complex SQL statement by joining all needed tables. A big SQL statement is preferrable in many cases since it simplifies filtering based on a column in a related table. It may not be efficient in some cases, however.

Consider an example where we need to find the latest posts together with their comments. Assuming each post has 10 comments, using a single big SQL statement, we will bring back a lot of redundant post data since each post will be repeated for every comment it has. Now let's try another approach: we first query for the latest posts, and then query for their comments. In this new approach, we need to execute two SQL statements. The benefit is that there is no redundancy in the query results.

So which approach is more efficient? There is no absolute answer. Executing a single big SQL statement may be more efficient because it causes less overhead in DBMS for yparsing and executing the SQL statements. On the other hand, using the single SQL statement, we end up with more redundant data and thus need more time to read and process them.

For this reason, Yii provides the together query option so that we choose between the two approaches as needed. By default, Yii adopts the first approach, i.e., generating a single SQL statement to perform eager loading. We can set the together option to be false in the relation declarations so that some of tables are joined in separate SQL statements. For example, in order to use the second approach to query for the latest posts with their comments, we can declare the comments relation in Post class as follows,

public function relations()
{
    return array(
        'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false),
    );
}

We can also dynamically set this option when we perform the 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();

7. 统计查询

Note: Statistical query has been supported since version 1.0.4.

Besides the relational query described above, Yii also supports the so-called statistical query (or aggregational query). It refers to retrieving the aggregational information about the related objects, such as the number of comments for each post, the average rating for each product, etc. Statistical query can only be performed for objects related in HAS_MANY (e.g. a post has many comments) or MANY_MANY (e.g. a post belongs to many categories and a category has many posts).

Performing statistical query is very similar to performing relation query as we described before. We first need to declare the statistical query in the relations() method of CActiveRecord like we do with relational query.

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

In the above, we declare two statistical queries: commentCount calculates the number of comments belonging to a post, and categoryCount calculates the number of categories that a post belongs to. Note that the relationship between Post and Comment is HAS_MANY, while the relationship between Post and Category is MANY_MANY (with the joining table post_category). As we can see, the declaration is very similar to those relations we described in earlier subsections. The only difference is that the relation type is STAT here.

With the above declaration, we can retrieve the number of comments for a post using the expression $post->commentCount. When we access this property for the first time, a SQL statement will be executed implicitly to retrieve the corresponding result. As we already know, this is the so-called lazy loading approach. We can also use the eager loading approach if we need to determine the comment count for multiple posts:

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

The above statement will execute three SQLs to bring back all posts together with their comment counts and category counts. Using the lazy loading approach, we would end up with 2*N+1 SQL queries if there are N posts.

By default, a statistical query will calculate the COUNT expression (and thus the comment count and category count in the above example). We can customize it by specifying additional options when we declare it in relations(). The available options are summarized as below.

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.

8.使用命名范围进行关系查询

Note: The support for named scopes has been available since version 1.0.5.

Relational query can also be performed in combination with named scopes. It comes in two forms. In the first form, named scopes are applied to the main model. In the second form, named scopes are applied to the related models.

The following code shows how to apply named scopes to the main model.

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

This is very similar to non-relational queries. The only difference is that we have the with() call after the named-scope chain. This query would bring back recently published posts together with their comments.

And the following code shows how to apply named scopes to the related models.

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

The above query will bring back all posts together with their approved comments. Note that comments refers to the relation name, while recently and approved refer to two named scopes declared in the Comment model class. The relation name and the named scopes should be separated by colons.

Named scopes can also be specified in the with option of the relational rules declared in CActiveRecord::relations(). In the following example, if we access $user->posts, it would bring back all approved comments of the posts.

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
           'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
           'with'=>'comments:approved'),
        );
    }
}

Note: Named scopes applied to related models must be specified in CActiveRecord::scopes. As a result, they cannot be parameterized.

以上就是Yii框架官方指南系列26——使用数据库:关系型 Active Record的内容,更多相关内容请关注PHP中文网(www.php.cn)!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.