Tiếp nối series về CakePHP, sẽ là phần Liên kết model với nhau. 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? từ đó sẽ làm cho cơ sở dữ liệu của chúng ta trở nên rõ ràng hơn trong các quan hệ.

Khi làm việc với cơ sở dữ liệu chúng ta đã biết được có các mối quan hệ: một – một, một – nhiều, nhiều – một và nhiều – nhiều và chúng tương ứng với các kiểu hasOne, hasMany, belongsTo và hasAndBelongsToMany trong CakePHP.

Một mối quan hệ trong model chúng ta có thể định nghĩa nó là một biến, một chuỗi hoặc một mảng.

Ví dụ:

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

Trong ví dụ trên có định danh(Alias) nó là duy nhất trong ứng dụng web, với ví dụ trên nếu khai báo định danh City trong một quan hệ khác sẽ có lỗi, không được chấp nhận.

CakePHP sẽ tự động tạo ra các liên kết giữa các đối tượng mô hình liên kết, 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ể gọi hàm của lớp City:


$this->City->someFunction();

Và ngay cả trong Controller chúng ta cũng có thể gọi:


$this->User->City->someFunction();

Chú ý: có thể thấy chúng ta có thể từ model User sử dụng các hàm trong model City, nhưng điều ngược lại thì không được CakePHP đồng ý, chỉ có thể truy cập một chiều. Nếu muốn truy xuất model User từ model City thì chúng ta phải sử dụng đến belongsTo trong model City.

hasOne

Hãy thiết lập một model User(/app/Model/User.php) với một mối quan hệ hasOne đến model Profile(/app/Model/Profile.php).

Đầu tiên, bảng dữ liệu chúng ta phải xác định chính xác các loại khóa. Model được liên kết phải có khóa ngoại, khi một User có một Profile thì nghĩa là trong bảng Profile có trường user_id làm khóa ngoại, tại sao lại là user_id? Đó là do quy ước của CakePHP, đương nhiên chúng ta hoàn toàn có thể thay đổi quy ước này và dùng một khóa ngoại khác. Tuy nhiên việc tuân theo quy tắc của CakePHP sẽ làm cho code của chúng ta thay đổi chỉnh sửa hơn.

Với model User.php


class User extends AppModel {
 public $hasOne = 'Profile';
}

với cách viết trên là cách đơn giản nhất để thiết lập thuộc tính $hasOne là một chuỗi chứa tên lớp của model cần liên kết, để hạn chế các liên kết nhiều hơn chúng ta sẽ dùng mảng:

class User extends AppModel {
  public $hasOne = array(
    'Profile' => array(
     'className' => 'Profile',
     'conditions' => array('Profile.status' => '1'),
     'dependent' => true
    )
  );
}

Danh sách các từ khóa(key) mà chúng ta 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, 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 các trường để order by.
  • dependent : nếu là true, thì khi xóa 1 User thì Profile cũng bị xóa theo.

Một khi quan hệ cho các model đã được định nghĩa, khi thực hiện lấy dữ liệu find() thì sẽ lấy được mảng giá trị dữ liệu bao gồm cả dữ liệu của bảng mà bạn liên kết tới, với cách viết trên chúng ta sẽ lấy được cả dữ liệu Profile khi lấy dữ liệu của User.

belongsTo

Như đã giới thiệu ở trên khi muốn truy xuất model User từ model Profile chúng ta phải khai báo belongsTo trong model Profile. Tức là khi User có một Profile thì bảng Profile sẽ giữ khóa ngoại user_id, cách khai báo như sau(Profile.php):

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

Với các từ khóa tương tự như hasOne, cộng thêm các từ khóa như:

  • type : là loại join sử dụng khi liên kết, mặc định sẽ là LEFT.
  • counterCache : 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
  • counterScope : đây là điều kiện cho việc update trường count.

Bây giờ chúng ta tìm hiểu thêm về counterCache. Key này giúp ta cache lại kết quả đếm của dữ liệu liên kết. Bình thường khi đếm dữ liệu CakePHP sẽ dụng  find(‘count’) nhưng với counterCache sẽ giúp chúng ta làm điều này dễ dàng hơn. Ví dụ, ta muốn đếm xem một Image đã có bao nhiêu comment trong bảng ImageComment. Ta sẽ thêm một trường trong bảng Image là image_comment_count, bạn hãy nhớ quy tắc đặt tên trường sẽ là [my_model]_count.

Một vài cách đặt:

Model Associated Model Example
User Image users.image_count
Image ImageComment images.image_comment_count
BlogEntry BlogEntryComment blog_entries.blog_entry_comment_count

Nội dung của ImageComment.php như sau:

  class ImageComment extends AppModel {
    public $belongsTo = array(
     'Image' => array(
        'counterCache' => true, // khi 1 Image được thêm 1 comment thì image_comment_count sẽ cộng 1 và ngược lại khi xóa nó sẽ bị trừ 1.
        'counterScope' => array(
           'ImageComment.active' => 1
        )
       )
    );
 }

counterScope cũng đã được sử dụng trong ví dụ trên, nó dùng để xác định điều kiện để count, như trên nghĩa là chỉ những comment nào có active là 1 mới được đếm, nếu không có counterScope thì nó sẽ đếm toàn bộ. Ngoài ra chúng ta cũng có thể sử dụng nhiều counterCache như sau:

Model Field Description
User users.messages_read Đếm tin nhắn đã đọc
User users.messages_unread Đếm tin nhắn chưa đợc
Message messages.is_read Xác định một tin nhắn đã được đọc hay chưa
class Message extends AppModel {
  public $belongsTo = array(
   'User' => array(
     'counterCache' => array(
       'messages_read' => array('Message.is_read' => 1),
       'messages_unread' => array('Message.is_read' => 0)
     )
   )
 );
}

hasMany

Tương tự như hasOne, khi khai báo là hasMany thì phải 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ự hasOne, nhưng hasMany  sẽ có thêm:

  • limit : số lượng tối đa dữ liệu được trả về.
  • offset : tương tự như offet mà ta hay dùng với các câu query
  • exclusive : là true sẽ rất có ích trong trường hợp bạn dùng deleteAll(),ví dụ như 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 hoàn chỉnh để CakePHP thực hiện.
class User extends AppModel {
 public $hasMany = array(
 'Comment' => array(
 'className' => 'Comment',
 'foreignKey' => 'user_id',
 'conditions' => array('Comment.status' => '1'),
 'order' => 'Comment.created DESC',
 'limit' => '5',
 'dependent' => true
 )
 );
}

hasAndBelongsToMany(HABTM)

Khác biệt so với hasMany là HABTM không có độc quyền(exclusive). Ví dụ, một món ăn A có nhiều nguyên liệu để hình thành trong đó có sử dụng đến cà chua, nhưng cũng có thể trong món ăn B thì cà chua cũng là thành phần không thể thiếu. Còn về hasMany sẽ là độc quyền như User có nhiều comment nhưng comment đó chỉ thuộc về một User.

Khi sử dụng HABTM, ta cần chuẩn bị một bảng phụ trong cơ sở dữ liệu, 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. Các trường của bảng phụ 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 phụ này, vậy ta sẽ có bảng ingredients_recipes.

Một số quan hệ HABTM

Relationship HABTM Table Fields
Recipe HABTM Ingredient ingredients_recipes.id, ingredients_recipes.ingredient_id,ingredients_recipes.recipe_id
Cake HABTM Fan cakes_fans.id, cakes_fans.cake_id, cakes_fans.fan_id
Foo HABTM Bar bars_foos.id, bars_foos.foo_id, bars_foos.bar_id

Ta có thể khai báo quan hệ như sau:

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

có một số từ khóa mới:

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

hasMany through(Join Table)

Chúng ta sẽ thêm quan hệ many-to-many:

Student hasAndBelongsToMany Course và Course hasAndBelongsToMany Student nghĩa là sinh viên có thể đăng ký nhiều khóa học và một khóa học có thể có nhiều sinh viên đăng ký.

Nếu dùng HABTM ta sẽ có:  id | student_id | course_id

Một câu hỏi đặt ra, nếu chúng ta muốn thêm một số trường để phát triển bảng phụ này? như bạn muốn biết sinh viên đó đến lớp bao nhiêu ngày và sinh viên này có vượt qua khóa học hay không? Với câu hỏi này bảng chúng ta cần là id | student_id | course_id | days_attended | grade

Vấn đề là HABTM không hổ trợ chúng ta làm việc này, ta sẽ mất dữ liệu thêm vào các cột cũng như nó không được cập nhật.

Cách để giải quyết vấn đề trên 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ệ sẽ là:

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

bindModel() và unbindModel()

Với các loại liên kết trên chúng ta đã tìm hiểu qua, và ở đây có vấn đề là trong trường hợp nào đó bạn muốn thêm liên kết vào hoặc không muốn sử dụng liên kết hiện có nữa? Hai hàm bindModel()unbindModel() sẽ là câu trả lờ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');

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

// giờ ta tìm tất cả User nhưng sẽ không có 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ọ.
 // unbindModel() chỉ có hiệu lực 1 lần duy nhất khi được gọi
 $this->User->find('all');
}

tương tự đối với bindModel()

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

 // Tạo liên kết
   $this->User->bindModel(
     array('hasOne' => array(
       'Profile' => array(
         'className' => 'Profile'
        )
      )
     )
   );
 // bây giờ ta sẽ thấy được Profile của tất cả User
   $this->User->find('all');
}

Nhiều quan hệ trong Model

Cũng có trường hợp một Model có nhiều hơn một mối quan hệ với Model khác.

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

Recipient của model User. Và ở đây

class User extends AppModel {
  public $hasMany = array(
   'MessageSent' => array(
      'className' => 'Message',
      'foreignKey' => 'user_id'
   ),
   'MessageReceived' => array(
      'className' => 'Message',
      'foreignKey' => 'recipient_id'
   )
  );
}

Cũng có thể tạo ra các liên kết dạng như:

class Post extends AppModel {
   public $belongsTo = array(
     'Parent' => array(
        'className' => 'Post',
        'foreignKey' => 'parent_id'
      )
   );
   public $hasMany = array(
      'Children' => array(
         'className' => 'Post',
         'foreignKey' => 'parent_id'
       )
   );
}

Joining Tables

Trong SQL, bạn có thể kết hợp bảng có liên quan sử dụng câu lệnh JOIN. Điều này cho phép bạn thực hiện tìm kiếm phức tạp trên nhiều bảng (ví dụ, bài viết tìm kiếm cho một vài tag).

Trong CakePHP, belongsTo và hasOne thực hiện tự động tham gia để lấy dữ liệu, do đó bạn có thể gửi các truy vấn để lấy dữ liệu của các model liên quan.

Còn một cách khác. Chỉ cần phải xác định sự cần thiết tham gia để kết hợp bảng và nhận được kết quả mong muốn đó là joins

Nên nhớ rằng cần phải set recursion là -1 thì mới thực hiện được: $this->Channel->recursive = -1;

Ví dụ:

  $options['joins'] = array(
     array('table' => 'channels',
           'alias' => 'Channel',
           'type' => 'LEFT',
           'conditions' => array(
                'Channel.id = Item.channel_id',
            )
      )
  );
  $Item->find('all', $options);

Ý nghĩa các key:

  • table: Bảng tham gia join.
  • alias: Định dạng của table. Tên của model liên kết.
  • type: kiểu join: inner, left or right.
  • conditions: điều kiện

Lưu ý là joins này là một mảng không có key

Cách Joins này phải được sử dụng cẩn thận vì nó có thể gây xung đột với các liên kết ở trên làm cho ứng dụng của chúng ta lỗi.

Bài hướng dẫn này xin được kết thúc tại đây.

Kết luận

  1. Nếu có thắc mắc gì các bạn để lại comment bên dưới mình sẽ trả lời sớm nhất có thể.
  2. Cảm ơn các bạn đã đọc.

Nongdanit.info
[Cakephp] Bài 15 – Liên kết model trong CakePHP