07/09/2018, 16:07

[CakePHP] Model : Liên kết model.

Tiếp nối phần bài về Model, tôi sẽ trình bày tiếp về phần Liên kết model với nhau - nguyên bản tiếng anh là Linking Models together. Chúng ta sẽ cùng tìm hiểu cách CakePHP định nghĩa, liên kết và tận dụng mối quan hệ giữa các models như thế nào. 1 ) Các mối quan hệ Như các bạn đã biết, khi làm ...

Tiếp nối phần bài về Model, tôi sẽ trình bày tiếp về phần Liên kết model với nhau - nguyên bản tiếng anh là Linking Models together. Chúng ta sẽ cùng tìm hiểu cách CakePHP định nghĩa, liên kết và tận dụng mối quan hệ giữa các models như thế nào.

1 ) Các mối quan hệ

Như các bạn đã biết, khi làm các ứng dụng sử dụng cơ sở dữ liệu quan hệ, chúng ta có các kiểu quan hệ sau : một - một, một - nhiều, nhiều - một và nhiều - nhiều. Và trong CakePHP chúng ta lần lượt có hasOne, hasMany, belongsTo và hasAndBelongsToMany (viết tắt là HABTM) theo thứ tự trên. Chúng ta định nghĩa một mối quan hệ bằng một biến class, đôi khi nó chỉ là 1 chuỗi hoặc có thể phức tạp bằng một mảng đa chiều. Hãy xem ví dụ :

class User extends AppModel {
        public $hasOne = 'Profile'; // chuỗi đơn giản : Profile là chỉ tên class
        // sử dụng một mảng đa chiều
        public $hasMany = array(
            'Recipe' => array( // Ở đây, Recipe là một định danh và có thể là bất kỳ từ nào bạn muốn, như MyRecipe...
                'className' => 'Recipe', // bạn cần chỉ tên class có quan hệ với User
                'conditions' => array('Recipe.approved' => '1'), // điều kiện cho quan hệ
                'order' => 'Recipe.created DESC' // chỉ định ra order
            )
        );
    }

Có một điều mà bạn cần chú ý, định danh phải là đơn nhất trong ứng dụng bạn viết. Với ví dụ trên, bạn khai báo một định danh là 'Recipe' trong một quan hệ khác sẽ không được phép. Như vậy, khi khai báo như trên mối quan hệ đã được thiết lập, và trong model User bạn hoàn toàn có thể dùng như bên dưới để gọi hàm của lớp Recipe :
$this->Recipe->functionName();
Và trong controller, bạn có thể truy cập tới Recipe model :
$this->User->Recipe->someFunction();
Nhưng tới đây một điều chú ý nữa cho bạn là CakePHP chỉ tạo quan hệ một chiều, tức là với khai báo như trên bạn không thể truy cập User model từ Recipe model được. Nếu bạn muốn làm thế, hãy dùng belongsTo trong Recipe model.

1.1) hasOne

OK, vậy là chúng ta đã hiểu CakePHP tạo mối liên kết giữa các models ra sao. Giờ chúng ta đi vào tìm hiểu từng loại quan hệ. Trước tiên là hasOne, điều cơ bạn cần nhớ là model được liên kết sẽ phải có khóa ngoại. Tức là khi nói một User có một Profile, thì ta sẽ hiểu trong bảng Profile sẽ có trường user_id. Đó là theo quy ước của CakePHP nhưng bạn hoàn toàn có thể bỏ quy ước này và dùng một khóa ngoại khác. Nếu làm vậy, code của bạn sẽ khó đọc, khó chỉnh sửa hơn. Với đoạn code ở đầu, bạn thấy chỉ có một string 'Profile' nhưng để hạn chế liên kết bạn có thể dùng mảng như sau :

    class User extends AppModel {
        public $hasOne = array(
            'Profile' => array(
                'className' => 'Profile',
                'conditions' => array('Profile.published' => '1'), //dùng conditions để hạn chế bản ghi
            )
        );
    }

Dưới đây là dánh sách các key mà bạn có thể dùng với hasOne :

  • **className **: tên model có quan hệ với model hiện tại
  • **foreignKey **: khóa ngoại của model liên kết đến
  • **conditions **: tương thích với hàm find() chúng ta đã biết, đây là một mảng các điều kiện.
  • **fields **: đây là danh sách các trường của model liên kêt mà bạn muốn lấy, mặc định sẽ lấy tất cả.
  • **order **: tương thích với hàm find(), nó sẽ chứa 1 mảng trường để order.
  • **dependent **: nếu bạn set là true, thì với code ví dụ trên khi xóa 1 User thì Profile cũng bị xóa theo.

Đến đây bạn cần biết rằng khi định nghĩa quan hệ cho các models thì khi find, bạn sẽ thu được mảng các giá trị bao gồm cả dữ liệu của bảng mà bạn liên kết tới. Với ví dụ trên, bạn sẽ nhận được cả dữ liệu Profile khi tìm kiếm User.

** 1.2) belongsTo**

Như đã nói ở trên, ta cần phải định nghĩa để từ Profile model có thể truy cập đến User model bằng belongsTo. Cơ bản, điều bạn cần nắm được là ngược lại với hasOne thì khóa ngoại sẽ do model hiện tại nắm giữ. Tức nếu nói một Profile của một User, thì bảng Profile sẽ có một trường có khóa ngoại là user_id. Và ta định nghĩa dạng như sau :

    class Profile extends AppModel {
        public $belongsTo = array(
            'User' => array(
                'className' => 'User',
                'foreignKey' => 'user_id'
            )
        );
    }

Tương tự với hasOne, belongsTo cũng có các key như className, foreignKey, conditions, fields, order. Ngoài ra, nó còn có :

  • **type **: là loại join sử dụng khi liên kết, mặc định sẽ là LEFT.
  • **counterCache **: chúng ta chỉ hiểu đơn giản nếu nó được set là true thì khi dùng hàm save() hoặc delete() thì nó sẽ tăng hoặc giảm count đi (ta sẽ tìm hiều sau)
  • counterScope : đây là lựa chọn cho việc update trường count.

Chúng ta sẽ cùng tìm hiểu counterCache sâu hơn một chút. Tính nằng này giúp bạn cache lại kết quả đếm của dữ liệu liên kết. Bình thường ta dùng find('count') nhưng counterCache sẽ giúp chúng ta làm điều này tiện lợi hơn. Ví dụ, ta muốn đếm xem một User đã up bao nhiều hình ảnh trong bảng Image. Ta sẽ đặt tên một trường trong bảng User là image_count, bạn hãy nhớ quy tắc đặt tên trường sẽ là [model_name]count. Giờ bạn sẽ code như sau :

    class Image extends AppModel {
        public $belongsTo = array(
            'User' => array(
                'counterCache' => true, // vậy là giờ khi User up 1 bức hình, image_count sẽ cộng 1 và ngược lại khi xóa nó sẽ bị trừ 1.
                'counterScope' => array(
                  'Image.active' => 1
            )
        );
    }

Bạn thấy rằng tôi sử dụng counterScope ở đoạn code trên, vậy nó có nghĩa gì ? Đơn giản counterScope cho phép bạn xác định những điều kiện để count, tức là scope để đếm là gì. Giờ chắc bạn hiểu với dòng code trên, tôi chỉ muốn đếm những hình có trạng thái là active (có thể trong trường hợp hình up lên nhưng cần admin phê duyệt chẳng hạn). Ngoài ra, bạn hoàn toàn có thể dùng nhiều counterCache như bạn muốn đếm User có bao nhiêu bức hình up lên đã được duyệt, bao nhiêu bức đang chờ duyệt ...

1.3)hasMany

Tương tự như hasOne, điều bạn sẽ nên nhớ là hasMany có khóa ngoại ở model liên kết tới. Và cấu trúc khai báo cũng tương tự nhưng hasMany có thêm vài key sau ngoài các key className, foreignKey, conditions, order, dependent ra.

  • **limit **: số bản ghi sẽ trả về
  • **offset **: tương tự như offet mà ta hay dùng với các câu query
  • **exclusive **: nếu set là true sẽ rất có ích trong trường hợp bạn dùng deleteAll(), như bạn xóa 1 user thì cũng muốn xóa tất cả hình ảnh user đó đã up chẳng hạn.
  • finderQuery: một câu query để CakePHP có thể dùng để fetch các bản ghi của model liên kết
    Dưới đây là một ví dụ sử dụng hasMany.
    class User extends AppModel {
        public $hasMany = array(
            'Image' => array(
                'className' => 'Image',
                'foreignKey' => 'user_id',
                'conditions' => array('Image.active' => '1'),
                'order' => 'Image.uploaded DESC',
                'limit' => '5',
                'dependent' => true
            )
        );
    }

1.4) hasAndBelongsToMany (HABTM)

Điểm khác biệt so với hasMany là HABTM không có tính loại trừ. Ví dụ, một công thức nấu ăn A có nhiều nguyên liệu, và có dùng đến hành tây nhưng cũng có thể trong công thức B thì hành tây cũng được sử dụng. Còn về hasMany, một User có nhiều hình ảnh nhưng một hình ảnh đó chỉ thuộc về một User.

Khi sử dụng liên kết này, ta cần chuẩn bị một bảng với tên bao gồm cả tên hai model liên kết với nhau theo thứ tự alphabe và ngăn cách bơi dấu gạch dưới. Ví dụ, ta có quan hệ HABTM : một món ăn (Recipe) có nhiều nguyên liệu (Ingredent) và nguyên liệu có thể dùng trong nhiều món ăn. Nội dung của bảng đó tối thiểu sẽ bao gồm 2 khóa ngoại là 2 khóa chính của 2 bảng liên kết với nhau, ngoài ra cũng có thể có thêm trường khác như trường 'id' làm khóa chính cho bảng đó. Cụ thể với ví dụ sẽ dạng như sau, bảng ingredients_recipes :

    ingredients_recipes.id, ingredients_recipes.ingredient_id, ingredients_recipes.recipe_id

Vậy là ta có thể sử dụng HABTM

    class Recipe extends AppModel {
        public $hasAndBelongsToMany = array(
            'Ingredient' =>
                array(
                    'className' => 'Ingredient',
                    'joinTable' => 'ingredients_recipes',
                    'foreignKey' => 'recipe_id',
                    'associationForeignKey' => 'ingredient_id',
                    'unique' => true,
                    'conditions' => ',
                    'fields' => ',
                    'order' => ',
                    'limit' => ',
                    'offset' => ',
                    'finderQuery' => ',
                    'with' => '
                )
        );
    }

Như chúng ta thấy có vài key mới được sử dụng.

  • **joindTable **: dùng để chỉ ra bảng ta đã chuẩn bị ở trên, ingredients_recipes
  • **with **: định nghĩa tên model của join table. Mặc định CakePHP sẽ tạo ra nhưng bạn có thể override lại bằng key này.
  • **associationForeignKey **: chính là khóa ngoại của model được liên kết tới.

1.5) Join Table

Tuy nhiên, hãy xem xét trường hợp : sinh viên có thể tham nhiều khóa học và khóa hoc có thể được đăng kí bới nhiều sinh viên. Nếu bạn dùng HABTM, bạn sẽ có thông tin
id | student_id | course_id
Nhưng nếu bạn muốn thêm thông tin thì sao? như bạn muốn biết số ngày sinh viên đó đã đến học và tình trạng sinh viên có đạt hay không ? bạn sẽ muốn bảng như sau
id | student_id | course_id | days_attended | grade

Bạn thấy lúc này HABTM không hỗ trợ, nhưng có một cách là dùng Join Table với hasMany, hay được biết đến là hasMany through. Ý nghĩa của cách này là model có liên kết trong chính nó. Với tình huống trên, ta sẽ tạo model là CourseMembership, và mối liên hệ được tạo như code bên dưới :

    // Student.php
    class Student extends AppModel {
        public $hasMany = array(
            'CourseMembership'
        );
    }

    // Course.php
    class Course extends AppModel {
        public $hasMany = array(
            'CourseMembership'
        );
    }

    // CourseMembership.php
    class CourseMembership extends AppModel {
        public $belongsTo = array(
            'Student', 'Course'
        );
    }

2) bindModel() và unbindModel()

Bốn loại liên kết chúng ta đã tìm hiểu xong nhưng có một câu hỏi đặt ra là : đôi khi chúng ta cần thêm hoặc không cần đến liên kết nữa thì làm thế nào? Câu trả lời sẽ là dùng đến hai hàm bindModel()unbindModel(). OK, hãy cùng xem code bên dưới :

    public function some_action() {
        // tìm tất cả User đồng thời kèm theo cả Profile của họ
        $this->User->find('all');

        // ta sẽ bỏ liên kêt đi
        $this->User->unbindModel(
            array('hasOne' => array('Profile'))
        );

        // giờ ta tìm tất cả User nhưng sẽ ko còn Profile trả về nữa
        $this->User->find('all');

        // nếu tiếp tục tìm thì kết quả sẽ trả về là tất cả User kèm Profile của họ.
        // suy ra, unbindModel() chỉ có hiệu lực 1 lần duy nhất khi được gọi
        $this->User->find('all');
    }

Và tương tự như trên, đôi khi bạn sẽ sử dụng đến bindModel() khi cần thiết.

    public function another_action() {
        // ta chưa có liên kết hasOne nên kết quả sẽ là User ko có Profile
        $this->User->find('all');

        // OK, không sao. hãy tạo liên kết
        $this->User->bindModel(
            array('hasOne' => array(
                    'Profile' => array(
                        'className' => 'Profile'
                    )
                )
            )
        );

        // giờ ta sẽ thấy của Profile cả tất cả User
        $this->User->find('all');
    }

Tôi xin kết thúc bài này ở đây. Ngoài những kiến thức ở trên, bạn có thể tìm hiểu thêm về 'đa liên kết' tới cùng một model hay cách dùng JOIN để làm việc hiệu quả và với đa dạng trường hợp áp dụng hơn.

0