Where collection laravel, hãy cẩn thận!

Đặt vấn đề

Trên cở sở dữ liệu mình có một bảng là hp_stores (lưu thông tin của dữ liệu của store), bảng gồm các trường như id, nameslug, cột slug là kiểu varchar. Mỗi store sẽ có các slug khác nhau khi lấy các store mình sẽ dựa và cột slug chứ không phải là cột id. Trong bảng hp_stores hiện có hai hàng với hai slug là 6969 và 06969. Yêu cầu đặt ra là lấy các store với slug là 6969 thì mình có hai cách lấy như sau:

  1. HpStore::where('slug', '6969')->get();
  2. HpStore::get()->where('slug', '6969');

Theo các bạn hai cách trên có cho ra kết quả giống nhau không :)

Giải đáp

Với cách 1 thì kết quả sẽ ra một store với slug là 6969 thì không có vẫn đề gì đúng như kết quả chúng ta mong muốn.

Với cách thứ hai thì kết quả lại ra hai store với 1 store có slug = 06969 và một store có slug = 6969 woowww, tại sao lại như vậy? Có phải laravel có vấn đề! Kết quả là không. Với cách thử nhất chúng ta đang sử điều kiện where trên database còn với cách thứ hai chúng ta sử dụng where trên collection và lúc này where trên collection sẽ có điểm khác. Vậy điểm khác ở đâu thì chúng ta sẽ vào xem laravel định nghĩa hàm where collection như thế nào. Sau một hồi đào bới và tìm kiếm thì mình cũng thấy định nghĩa của nó.

public function where($key, $operator, $value = null)
{
	return $this->filter($this->operatorForWhere(...func_get_args()));
}

Hàm `filter` thì dễ hiểu rồi vậy còn hàm operatorForWhere, chúng ta tiếp tục tìm hàm đó xem laravel định nghĩa như thế nào và đây là kết quả:

protected function operatorForWhere($key, $operator, $value = null)
    {
        if (func_num_args() == 2) {
            $value = $operator;

            $operator = '=';
        }

        return function ($item) use ($key, $operator, $value) {
            $retrieved = data_get($item, $key);

            $strings = array_filter([$retrieved, $value], function ($value) {
                return is_string($value) || (is_object($value) && method_exists($value, '__toString'));
            });

            if (count($strings) < 2 && count(array_filter([$retrieved, $value], 'is_object')) == 1) {
                return in_array($operator, ['!=', '<>', '!==']);
            }

            switch ($operator) {
                default:
                case '=':
                case '==':  return $retrieved == $value;
                case '!=':
                case '<>':  return $retrieved != $value;
                case '<':   return $retrieved < $value;
                case '>':   return $retrieved > $value;
                case '<=':  return $retrieved <= $value;
                case '>=':  return $retrieved >= $value;
                case '===': return $retrieved === $value;
                case '!==': return $retrieved !== $value;
            }
        };
    }

Thật phức tạp đúng không mọi người :). Để hiểu đơn giản thì các bạn chỉ cần nhớ hàm này trả về một Closure để phục vụ cho hàm filter ở bên ngoài và một điểm chú ý nữa là ở chỗ switch case chúng ta thấy nếu ($operator = null, =, ==) thì hàm sẽ dùng so sánh ==. Vậy vấn đề đã dần sáng tỏ, với cách hai chúng ta dùng where collection với điều kiện slug là 6969 chúng ta không truyền operator vào tức là mặc định nó là null mà là null thì sẽ vào case return $retrieved == $value. Mà như các bạn biết thì trong php chuỗi "6969" == "06969" sẽ trả về true => Lúc này sẽ có hai store được trả về.

Kết luận

Để tránh những trường hợp như trên thì các bạn hãy cận thận khi dùng điều kiện trên collection, thay vì where trên collection thì bạn có thể dùng điều kiện trên database sẽ cho kết quả chuẩn hơn và tốc độ xử lý trên database cũng nhanh hơn và chiếm ít bộ nhớ hơn (Cái này các bạn có thể sử dụng laravel debug bar để test).

Nếu trong các trường hợp đặc biệt bắt buộc các bạn phải where trên collection thì các bạn hay dùng whereStrict hoặc dùng where thì hãy chỉ định operator cụ thể.