Yii 中的relation

原文:http://www.yiiframework.com/wiki/428/drills-search-by-a-has_many-relation/

此文主要講述YII中的relation(HAS_MANY , BELONGS_TO, MANY_MANY)

Relation

BELONGS_TO: 多對一
HAS_MANY: 一對多
MANY_MANY: 多對多

BELONGS_TO 是相當簡單的,但有時候HAS_MANY和MANY_MANY用起來就有些費勁了,下面讓我們來通過幾個例子看一下:

Example of HAS_MANY

例如:一個作者(author)對應多篇博文(post),它就是一對多的關係,表示如下:

/**
 * Author model
 * @property integer $id
 * @property integer $name Author's name
...
 */
class Author extends CActiveRecord
{
    ...
    public function relations()
    {
        return array(
            'posts' => array(self::HAS_MANY, 'Post', 'author_id');
        );
    }
    ...

/**
 * Post model
 * @property integer $id
 * @property integer $author_id FK to Author's id
 * @property integer $title Title of Post
...
 */
class Post extends CActiveRecord
{
    ...
    public function relations()
    {
        return array(
            'author' => array(self::BELONGS_TO, 'Author', 'author_id');
        );
    }
    ...

Task #1

展示在title中包含某個關鍵字的所有博文(Show all posts that has a word in post title

public static function GetPostsByTitle($searchWord)
{
    // query criteria
    $criteria = new CDbCriteria();
    // compare title
    $criteria->compare('title', $searchWord, true);
    // find
    $posts = Post::model()->findAll($criteria);
    // show
    foreach($posts as $post)
    {
        echo "Title = " . $post->title . "\n";
    }
}

工作正常!!

Task #2

展示在title中包含某個關鍵字的所有博文以及該博文的作者(Show all posts with their authors that has a certain word in post title

public static function GetPostsWithAuthorByTitle($searchWord)
{
    // query criteria
    $criteria = new CDbCriteria();
    // with Author model
    $criteria->with = array('author');
    // compare title
    $criteria->compare('t.title', $searchWord, true);
    // find all posts
    $posts = Post::model()->findAll($criteria);
    // show all posts
    foreach($posts as $post)
    {
        echo "Title = " . $post->title . "\n";
        echo "Author = " . $post->author->name . "\n";
    }
}

然而,你可能會說:“$criteria->with = array('author');“這句code是不需要的,是的,你是對的!在不加這句代碼的情況下也是可以正常工作的!因爲post和author已經通過relation建立了關係。

在不使用$criteria->with = array('author');,而直接調用$post->author使用的是 lazy loading,這樣會造成每次調用$post->author的時候都會發一次sql語句去查找一遍。如果使用了$criteria->with = array('author');,則會只發一次sql語句去join,稱爲:eager loading

eager loading lazy loading更加的有效率,但要注意的是在使用with來join表的時候,要消除歧義,尤其是兩張表有相同名稱的字段時;

...
    // compare Author's name
    $criteria->compare('author.name', $searchName, true);
    ...

Task #3

展示所有的作者,條件是該作者只少有一篇博文的title包含某個關鍵詞(Show all authors who has at least one post that has a certain word in post title

下面,我們將要檢索author表

public static function GetAuthorsByPostTitle($searchWord)
{
    // query criteria
    $criteria = new CDbCriteria();
    // with Post model
    $criteria->with = array('posts');
    // compare title
    $criteria->compare('posts.title', $searchWord, true);
    // find all authors
    $authors = Author::model()->findAll($criteria);
    // show all authors
    foreach($authors as $author)
    {
        echo "Author = " . $author->name . "\n";
    }
}

OK,工作正常,簡直太簡單了!

Task #4

展示所有的作者及他的所有博文,條件是該作者只少有一篇博文的title包含某個關鍵詞(Show all authors with his/her all posts who has at least one post that has a certain word in post title

Hmmm. Just a small addition to the 3rd task. Something like this?

Wrong Answer 

public static function GetAuthorsWithPostsByPostTitle($searchWord)
{
    // query criteria
    $criteria = new CDbCriteria();
    // with Post model
    $criteria->with = array('posts');
    // compare title
    $criteria->compare('posts.title', $searchWord, true);
    // find all authors
    $authors = Author::model()->findAll($criteria);
    // show all authors and his/her posts
    foreach($authors as $author)
    {
        echo "Author = " . $author->name . "\n";
        foreach($author->posts as $post)
        {
            echo "Post = " . $post->title . "\n";
        }
    }
}

啊哈,你錯了!

爲什麼呢?

因爲該任務是某作者所有的博文,而你只是展示了包含有某關鍵字的博文,怎麼解決呢?此時就可以用lazy loading來解決:

Answer

public static function GetAuthorsWithPostsByPostTitle($searchWord)
{
    // query criteria
    $criteria = new CDbCriteria();
    // join Post model (without selecting)
    $criteria->with = array(
        'posts' => array(
            'select' => false,
        ),
    );
    // compare title
    $criteria->compare('posts.title', $searchWord, true);
    // find all authors
    $authors = Author::model()->findAll($criteria);
    // show all authors and his/her posts(查出來的用戶都是隻少有一篇博文中包含關鍵字)
    foreach($authors as $author)
    {
        echo "Author = " . $author->name . "\n";
        foreach($author->posts as $post)(連接生成的表中包含全部博文)
        {
            echo "Post = " . $post->title . "\n";
        }
    }
}

注意:這裏的lazy loading和上面所描述的lazy loading還是有區別的,上面的lazy loading是在沒有使用with的情況下產生的(根本沒有去連接author表),而這裏的lazy loading是指使用了with的情況下產生的(已經連接了post表,但是並沒有根據條件去刪除哪些沒有包含關鍵詞的post)

通過設置'select' => false來說實現lazy loading。


Task #5

通過名字排序作者,並展示前5個作者及他的所有博文,條件是該作者只少有一篇博文的title包含某個關鍵詞(Show top 5 authors in the order of name with his/her all posts who has at least one post that has a certain word in post title

貌似我們加上'LIMIT'和'ORDER'就可以了:

public static function GetTop5AuthorsWithPostsByPostTitle($searchWord)
{
    // query criteria
    $criteria = new CDbCriteria();
    // with Post model
    $criteria->with = array('posts');
    // compare title
    $criteria->compare('posts.title', $searchWord, true);
    // order by author name
    $criteria->order = 't.name ASC';
    // limit to 5 authors
    $criteria->limit = 5;
    // find all authors
    $authors = Author::model()->findAll($criteria);
    // show all authors and his/her posts
    foreach($authors as $author)
    {
        echo "Author = " . $author->name . "\n";
        foreach($author->posts as $post)
        {
            echo "Post = " . $post->title . "\n";
        }
    }
}

然後,卻報錯了:There's no column likeposts.title

The guide says:

Guide : By default, Yii uses eager loading, i.e., generating a single SQL statement, except when LIMIT is applied to the primary model.

It means that if LIMIT is applied to the primary model, then lazy loading will be used. This rule has been applied to our code above, and the query was executed without joining the author table. So, what to do then?

The guide proceeds to say:

Guide : We can set the together option in the relation declarations to be true to force a single SQL statement even when LIMIT is used.

OK. So we will modify the code to:

什麼意思呢?意思是:LIMIT是作用在主表上的,默認情況下, YII使用的是eager loading(除了使用LIMIT的情況下),所以纔會報上面的錯誤,如果想使用eager loading,則可以通過設置together=true來實現。(但這裏的默認情況貌似不包含在使用HAS_MANY和MANY_MANY時,因爲他們在不使用with的情況下是lazy loading)

...
    // force to join Post
    $criteria->with = array(
        'posts' => array(
            'together' => true,
        ),
    );
    ...

貌似可以工作了,但是,查找的數據確不正確:

[search word = foo]
Author = Andy
    Post = Don't use foo
    Post = Use yoo for foo
Author = Ben
    Post = foo is great
    Post = I love foo
Author = Charlie
    Post = What's foo?
[end]
你需要的是5個author,但是卻查找出來了5個post,3個author(因爲連接後的表刪除了一部分post)

通過'select' => false 來解決:

...
    // force to join Post (without selecting)
    $criteria->with = array(
        'posts' => array(
            'together' => true,
            'select' => false,
        ),
    );
    ...

But it still doesn't work. It will show the results like this:

[search word = foo]
Author = Andy
    Post = Don't use foo
    Post = Use yoo for foo
    Post = Don't use bar
    Post = Use yar for bar
Author = Ben
    Post = foo is great
    Post = I love foo
    Post = I also love bar
Author = Charlie
    Post = What's foo?
    Post = What's bar?
[end]
這是因爲LIMIT是作用在主表上的,對於生成的虛擬表並沒有對此行爲的解釋,因此它不知道怎麼去LIMIT數據!

不要放棄,我們可以使用$criteria->group = 't.id';來解決;

...
    // force to join Post (without selecting)
    $criteria->with = array(
        'posts' => array(
            'together' => true,
            'select' => false,
        ),
    );
    ...
    // group by Author's id
    $criteria->group = 't.id';
    ...

OK, 完美工作!

結果:

public static function GetTop5AuthorsWithPostsByPostTitle($searchWord)
{
    // query criteria
    $criteria = new CDbCriteria();
    // force to join Post (without selecting)
    $criteria->with = array(
        'posts' => array(
            'together' => true,
            'select' => false,
        ),
    );
    // compare title
    $criteria->compare('posts.title', $searchWord, true);
    // group by Author's id
    $criteria->group = 't.id';
    // order by author name
    $criteria->order = 't.name ASC';
    // limit to 5 authors
    $criteria->limit = 5;
    // find all authors
    $authors = Author::model()->findAll($criteria);
    // show all authors and his/her posts
    foreach($authors as $author)
    {
        echo "Author = " . $author->name . "\n";
        foreach($author->posts as $post)
        {
            echo "Post = " . $post->title . "\n";
        }
    }
}

Task #6

通過名字排序作者,並展示前5個作者及包含關鍵詞的有關博文,條件是該作者只少有一篇博文的title包含某個關鍵詞(Show top 5 authors in the order of name with his/her relevant posts who has at least one post that has a certain word in post title

也許通過使用eager  loading就可以完成,但是,下面是另一種方法:

public static function GetTop5AuthorsWithPostsByPostTitle($searchWord)
{
    // query criteria
    $criteria = new CDbCriteria();
    // force to join Post (without selecting)
    $criteria->with = array(
        'posts' => array(
            'together' => true,
            'select' => false,
        ),
    );
    // compare title
    $criteria->compare('posts.title', $searchWord, true);
    // group by Author's id
    $criteria->group = 't.id';
    // order by author name
    $criteria->order = 't.name ASC';
    // limit to 5 authors
    $criteria->limit = 5;
    // find all authors
    $authors = Author::model()->findAll($criteria);
    // show all authors and his/her posts
    foreach($authors as $author)
    {
        echo "Author = " . $author->name . "\n";
        // lazy loading posts with filtering
        $filteredPosts = $author->posts(
            array(
                'condition' => 'title LIKE :search_word',
                'params' => array(
                    ':search_word' => '%' . $searchWord . '%',
                ),
            )
        );
        foreach($filteredPosts as $post)
        {
            echo "Post = " . $post->title . "\n";
        }
    }
}

在這裏你可以動態的在lazy loading中定義查詢條件。

The guide says:

Guide : Dynamic query options can also be used when using the lazy loading approach to perform relational query. To do so, we should call a method whose name is the same as the relation name and pass the dynamic query options as the method parameter.

意思是說:$author調用的post()方法的名字應該和relation中定義的名字一樣。

發佈了9 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章