Tiếp tục tìm hiểu về ActiveRecord xem vòng đời hoạt động như thế nào và chúng có những trigger gì, thể hiện các mối quan hệ dữ liệu ra sao. Trong bài viết này tại hạ xin được làm rõ thêm các vấn đề trên để hiểu rõ hơn về ActiveRecord.

Tùy chọn kết nối cơ sở dữ liệu 

Ở phần trước nói về ActiveRecord, nó sẽ được ứng dụng vào một model cụ thể để sử dụng với bảng dữ liệu trong cơ sở dữ liệu. Ngoài ra, các đồng code có thể tùy chọn được việc sử dụng cấu hình cơ sở dữ liệu nào để thao tác.

Mặc định ActiveRecord sử dụng thành phần db như một kết nôi đến cơ sở dữ liệu. Thành phần db này sẽ được cấu hình trong common/config/main-local.php

return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=advanced',
            'username' => 'root',
            'password' => '',
        ],
    ],
];

Nếu muốn tùy chọn trong việc sử dụng thêm các thành phần kết nối cơ sở dữ liệu thì có thể cấu hình như sau

return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=advanced',
            'username' => 'root',
            'password' => '',
        ],
        'db2' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=test',
            'username' => 'root',
            'password' => '',
        ],
    ],
];

Ở trên tại hạ thêm 1 kết nối nữa là db2 kết nối với 1 cơ sở dữ liệu khác. Trong model nếu các đồng code muốn dùng thành phần db2 này để sử dụng với cơ sở dữ liệu test thì sử dụng hàm getDb() để ghi đè lại như sau

class User extends ActiveRecord
{
    public static function tableName()
    {
        return 'user';
    }

    public function rules()
    {
        return [
            [['username', 'auth_key', 'password_hash', 'email', 'created_at', 'updated_at'], 'required'],
            [['status', 'created_at', 'updated_at'], 'integer'],
            [['username', 'password_hash', 'password_reset_token', 'email', 'verification_token'], 'string', 'max' => 255],
            [['auth_key'], 'string', 'max' => 32],
            [['username'], 'unique'],
            [['email'], 'unique'],
 
        ];
    }

    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'username' => 'Username',
            'auth_key' => 'Auth Key',
            'password_hash' => 'Password Hash',
            'password_reset_token' => 'Password Reset Token',
            'email' => 'Email',
            'status' => 'Status',
            'created_at' => 'Created At',
            'updated_at' => 'Updated At',
            'verification_token' => 'Verification Token',
        ];
    }
   

    public static function getDb()
    {
        return \Yii::$app->db2;  
    }
}

Chuyển đổi dữ liệu

Để định dạng dữ liệu trước khi lưu vào cơ sở dũ liệu, các đồng code khai báo hàm set<Tên phương thức>, ví dụ setNameUppercase. Lưu ý là nếu viết dạng setName thì sẽ không hoạt động vì trùng với trường dữ liệu, tên phương thức viết theo chuẩn CamelCase

class User extends ActiveRecord
{
    // ...

    public function setNameUppercase($value)
    {
        $this->name = ucfirst($value);
    }
}

Việc set định dạng như trên áp dụng khi thêm mới hoặc cập nhật dữ liệu

class UserController extends Controller
{
    //...........

   public function activeStore()
   {
      $user = User::findOne(1);
      $user->nameUppercase = 'abc'; -> sẽ chuyển thành Abc sau khi lưu vào csdl
      $user->save();
   }
}

- Để định dạng dữ liệu trước khi lấy ra để hiển thị, các đồng code sử dụng get<Tên phương thức>, ví dụ getDate

class User extends ActiveRecord
{
    // ...

    public function getDate()
    {
        return date('d/m/Y',$this->created_at);
    }
}

Sử dụng trong việc hiển thị dữ liệu 

class UserController extends Controller
{
    //...........

   public function activeShow()
   {
      $user = User::findOne(1);
      echo $user->date; // hiển thị định dạng d/m/Y
   }
}

Vòng đời ActiveRecord và Trigger

Ở mỗi vòng đời của ActiveRecord các sự kiện sẽ được gọi và có thể ghi đè lại các sự kiện này.

- Khi khởi tạo ActiveRecord sẽ khởi chạy qua hàm init() và kích hoạt sự kiện EVENT_INIT.

- Khi truy vấn dữ liệu, mỗi bản ghi sẽ trải qua sự kiện EVENT_INIT sau đó chạy tiếp qua trigger afterFind() và kích hoạt sự kiện EVENT_AFTER_FIND.

- Khi lưu trữ dữ liệu, trigger beforeValidate() được gọi -> dữ liệu chạy qua validate, nếu hợp lệ sẽ đi tiếp qua trigger afterValidate() -> chạy tiếp qua beforeSave()  -> thực hiện lưu trữ dữ liệu -> chạy tiếp đến trigger afterSave() để kết thúc vòng đời.

Ví dụ về trigger beforeSaveafterSave, 2 trigger mà tại hạ dùng nhiều =)))

class UserController extends Controller
{
    //...........

     public function beforeSave($insert)
    {
       //......
        $this->fullname = $this->first_name.' '.$this->last_name;
        return parent::beforeSave($insert); // TODO: Change the autogenerated stub
    }

     public function afterSave($insert, $changedAttributes)
    {
         //.......... upload
        $this->updateAttributes(['photo' => 'abc.jpg']);
        return parent::afterSave($insert, $changedAttributes);
        
    }
}

Ở trên chủ yếu 2 trigger làm nhiệm vụ chuyển đổi dữ liệu để lưu vào cơ sở dữ liệu. Đối với beforeSave thì có thể định dạng hoặc xử lý luồng dữ liệu trước khi lưu, còn với afterSave sẽ xử lý các luồng dữ liệu sau khi lưu trữ.

- Khi xóa dữ liệu, trigger beforeDelete() được gọi -> tiếp đến sẽ xóa dữ liệu -> sau đó sẽ gọi trigger afterDelete và kết thúc vòng đời. Vòng đời ActiveRecord khi xóa dữ liệu thường được sử dụng các trigger nhiều khi dữ liệu có quan hệ ràng buộc.

Mối quan hệ dữ liệu trong ActiveRecord

Bên cạnh việc thao tác với các dữ liệu riêng lẻ, ActiveRecord còn được sử dụng tập hợp các dữ liệu quan hệ với nhau, giúp dễ dàng truy cập.

Ví dụ tại hạ có 2 bảng là userblog có quan hệ là 1 - n thì sẽ khai báo như sau

class User extends ActiveRecord
{
    // ...

    public function getBlogs()
    {
        return $this->hasMany(Blog::class, ['user_id' => 'id']);
    }
}

class Blog extends ActiveRecord
{
    // ...

    public function getUser()
    {
        return $this->hasOne(User::class, ['id' => 'user_id']);
    }
}

Trong đoạn mã trên, tại hạ đã khai báo một quan hệ cho User và một quan hệ cho Blog.

Mỗi phương thức quan hệ phải được đặt tên là getXyz. Tên xyz(chữ cái đầu tiên viết thường) là tên quan hệ . Lưu ý rằng tên quan hệ có phân biệt chữ hoa chữ thường .

Sau khi khai báo quan hệ, các đồng code có thể truy cập dữ liệu quan hệ thông qua tên quan hệ. Đây là giống như truy cập vào thuộc tính của một đối tượng. Vì lý do này, chúng được gọi  là thuộc tính quan hệ .

// SELECT * FROM `user` WHERE `id` = 1
$user = User::findOne(1);

// SELECT * FROM `blog` WHERE `user_id` = 1
// $blogs is an array of blog objects
$blogs = $user->blogs; // lấy ra mảng các blog được tạo bởi user có id là 1



// SELECT * FROM `blog` WHERE `id` = 1
$blog = Blog::findOne(1);

// SELECT username FROM `user` WHERE `id` = 1
$user = $blog->user->username;

Nếu trong các hàm quan hệ dữ liệu các đồng code muốn thêm điều kiện thì có thể viết lại như sau

class User extends ActiveRecord
{
    // ...

    public function getBlogs()
    {
        return $this->hasMany(Blog::class, ['user_id' => 'id'])->where(['status' => 1])->orderBy(['sort' => SORT_ASC,'updated' => SORT_DESC,'created' => SORT_DESC]);
    }
}

class Blog extends ActiveRecord
{
    // ...

    public function getUser()
    {
        return $this->hasOne(User::class, ['id' => 'user_id'])->where(['status' => 1])->orderBy(['sort' => SORT_ASC,'updated' => SORT_DESC,'created' => SORT_DESC]);
    }
}

Lazy Loading và Eager Loading

Hai khái niệm này chắc hẳn cũng khá lạ cho các anh em đồng code, nhưng nó đóng một phần quan trọng trong việc tối ưu truy vấn dữ liệu.

Với Lazy loading rất tiện để sử dụng. Tuy nhiên, nó có thể gặp sự cố về hiệu suất khi cần truy cập vào cùng một thuộc tính quan hệ của nhiều đối tượng ActiveRecord. Ví dụ dưới đây cho thấy rất nhiều truy vấn được thực thi

// SELECT * FROM `user` LIMIT 100
$users = User::find()->limit(100)->all();

foreach ($users as $user) {
    // SELECT * FROM `blog` WHERE `user_id` = ...
    $blogs = $user->blogs;
}

Ở ví dụ trên, có thể thấy có 101 câu truy vấn được thực thi trong đó 1 câu query là lấy ra 100 user và trong vòng lặp cứ mỗi lần lặp 1 câu truy vấn lấy ra tất cả các blog của user đó. Điều này làm ảnh hưởng đến hiệu suất làm việc của ứng dụng khi phải thực thi quá nhiều truy vấn. Để giải quyết vấn đề này Yii2 đã cung cấp Eager Loading nhằm giảm số lượng truy vấn không đáng có xuống.

// SELECT * FROM `user` LIMIT 100;
// SELECT * FROM `blog` WHERE `user_id` IN (...)
$users = User::find()
    ->with('blog')
    ->limit(100)
    ->all();

foreach ($users as $user) {
    // no SQL executed
    $blogs = $user->blogs;
}

Bằng việc sử dụng with(), ActiveRecord sẽ lấy được danh sách các blog của mỗi user chỉ trong truy vấn đầu tiên, do đó trong vòng lặp sẽ không có thực thi truy vấn để lấy ra danh sách blog nữa, vì mỗi row user sẽ có thêm row danh sách blog rồi.

Tổng Kết 

Qua bài viết này, tại hạ cũng đã tổng hợp lại những phần hay sử dụng của ActiveRecord nhất. Để chi tiết hơn các đồng code có thể tìm hiểu thêm tại đây. Còn rất nhiều phần liên quan đến ActiveRecord tại hạ xin phép sẽ giới thiệu ở phần sau.