Bài 8: Thao tác với Database qua Eloquent Model - Học lập trình Laravel

Đăng bởi: Admin | Lượt xem: 6644 | Chuyên mục: Laravel


1. ORM là gì?

ORM (Object Relational Mapping) là một kĩ thuật lập trình dùng để chuyển đổi dữ liệu giữa một hệ thống không hướng đối tượng như cơ sở dữ liệu sang một hệ thống hướng đối tượng như lập trình hướng dối tượng trong PHP. Kỹ thuật này tạo ra các đối tượng CSDL ảo có thể được lập trình trong mã nguồn và có nhiều ưu điểm như mã nguồn trở lên rõ ràng và dễ bảo trì, dễ dàng thao tác với dữ liệu và thực hiện việc tối ưu hệ thống thông qua việc sử dụng bộ đệm... Các công việc khó hoặc không thể xử lý ở database layer sẽ được đưa lên lớp ứng dụng

2. Eloquent Model

Eloquent ORM là thư viện ORM được cài đặt kèm với Laravel, cung cấp loạt các phương thức truy xuất dữ liệu đơn giản, dễ triển khai. Mỗi bảng dữ liệu đều được mô tả thành một "model" trong Laravel và Eloquent ORM ánh xạ các dữ liệu này thành các đối tượng Active Record tương ứng với "model" đó, từ đó làm việc với dữ liệu trở nên đơn giản hơn với thao tác chính từ các "model" này
Với mỗi một Model chúng ta sẽ có 4 hành động mà chúng ta quan tâm, nó tương ứng với 4 hành động với các bản ghi dữ liệu (record):
  1. Create: tạo ra một Model, tương ứng với việc tạo ra một bản ghi dữ liệu trong bảng.
  2. Read: truy vấn dữ liệu từ database và map lên Model
  3. Update: chỉnh sửa một Model hay tương ứng là chỉnh sửa một bản ghi.
  4. Delete: xóa một Model và tương ứng là xóa một bản ghi dữ liệu.

3. Insert dữ liệu

Tạo một bảng ghi dữ liệu
Như đã nói ở trên, việc thêm một bản ghi dữ liệu sẽ tương ứng với tạo một thực thể hay một cài đặt (instance) từ Model.
<?php
use App\Product;
$product          = new Product;
$product->name    = $request->input('name');
$product->price   = $request->input('price');
$product->content = $request->input('content');
$product->active  = $request->has('active')? 1 : 0;
$product->save();
Tạo một thực thể bằng câu lệnh new Ten_model, ở đây tạo ra một thực thể là $product từ Model Product, sau đó gán giá trị cho các thuộc tính của chúng. Khi muốn lưu record này vào database chúng ta gọi đến phương thức save(). Chú ý, các trường created_at, updated_at sẽ được cập nhật các giá trị thời gian một cách tự động do đó bạn chỉ cần quan tâm đến các trường này khi sử dụng giá trị của chúng.
Chúng ta cùng xem lại nếu như không sử dụng Laravel Eloquent chúng ta cũng có thể sử dụng Query Builder để thực hiện, và khi đó chúng ta phải tự chủ động xử lý các công việc mà Laravel Eloquent đã ngầm định như đưa vào giá trị thời gian cho created_at, updated_at chẳng hạn.
// Code trích dẫn trong phương thức store của ProductController trong ví dụ Query Builder
$active = $request->has('active')? 1 : 0;
    $product_id = DB::table('products')->insertGetId([
        'name'       => $request->input('name'),
        'price'      => $request->input('price'),
        'content'    => $request->input('content'),
        'image_path' => $request->input('image_path'),
        'active'     => $active,
        'created_at' => \Carbon\Carbon::now(),
        'updated_at' => \Carbon\Carbon::now()
        ]);
Model Mass Assignment
Mass Assignment là gì? Mass Assignment xuất phát từ ngôn ngữ Ruby on Rails, là tính năng cho phép lập trình một cách tự động gán các tham số của một HTTP request vào các biến hoặc đối tượng trong lập trình. Ví dụ: chúng ta có một form đăng ký người dùng như sau, các tên trường nhập liệu trùng với tên cột trong bảng users trong CSDL.
<form>
     <input name='username' type='text'>
     <input name='password' type='text'>
     <input name='email' type='text'>
     <input type=submit>
  </form>
Khi đó form này POST dữ liệu lên chúng ta có thể ghi dữ liệu này vào CSDL bằng đoạn code sau:
$user = new User(Input::all());
Thật ngắn gọn và đơn giản đúng không, tính năng này gọi là Mass Assignment. Tuy nhiên, có một lỗ hổng bảo mật xảy ra, nếu một kẻ xấu gửi thêm dữ liệu user_type = ‘admin’, khi đó user mới được tạo sẽ có quyền admin, việc gắn thêm dữ liệu gửi lên server là rất đơn giản có thể thực hiện bằng các công cụ có sẵn trên trình duyệt như Chrome Developer Tools…
Để xử lý vấn đề lỗ hổng trong Mass Assignment, Laravel đưa ra thêm hai thuộc tính cho Model là $fillable và $guarded. Ví dụ:
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'password', 'email'];
}
$fillable cho phép thiết lập các cột trong một bảng có thể sử dụng tính năng Mass Assignment, khi đó ta có thể thực hiện:
$user = User::create(Input::all());
// Hoặc
$user = new User(Input::al());
Khi đó nếu kẻ xấu gửi thêm user_type là trường không có trong $fillable, các câu lệnh trên sẽ phát sinh một exception ngay. Như vậy lỗ hổng trong Mass Assignment đã được xử lý.
Trái ngược với $fillable, ta có thể định nghĩa các trường được bảo vệ khỏi Mass Assignment thông qua thuộc tính $guarded. Không khai báo cả 2 thuộc tính này đồng thời
Chú ý: $fillable và $guarded chỉ có tác dụng với các phương thức của Eloquent Model, với các phương thức của Query Builder nó không có tác dụng. Ví dụ sau minh chứng cho điều này:
// Phương thức Eloquent 
$user = User::find($id);
$user->update(Input::all());

// Phương thức Query Builder
User::where('id', $id)->update(Input::all());
Khi đó nếu kẻ xấu có tình chèn thêm user_type = ‘admin’ thì đoạn code đầu sẽ phát sinh exception còn đoạn code sau vẫn chạy bình thường.
Một số phương thức tạo bản ghi khác
Có hai phương thức tạo bản ghi mới sử dụng Mass Assignment khác là firstOrCreate và firstOrNew. Phương thức firstOrCreate() thử tìm các bản ghi sử dụng cặp cột và giá trị, nếu không tìm thấy, một bản ghi sẽ được tạo ra với các thuộc tính này. firstOrNew thì khác chút là nó không ghi dữ liệu vào CSDL mà trả về một instance của model, chỉ ghi dữ liệu xuống CSDL khi gọi phương thức save().
// Tìm user trong CSDL nếu không có thì insert bản ghi
$user = User::firstOrCreate(Input::all());

// Tìm user trong CSDL nếu không có thì trả về một instance của User và chỉ ghi xuống CSDL khi gọi phương thức save()
$user = User::firstOrCreate(Input::all());
$user->save();
Một phương thức nữa cũng rất hay gặp trong thực tế là updateOrCreate, nó sử dụng để update hoặc tạo mới một entry, ví dụ
$product = Product::updateOrCreate(
    ['name' => 'Bộ phát WiFi TENDA FH304', 'active' => 1],
    ['price' => 750000]
);
Truy vấn dữ liệu bằng Model
Bạn đã tạo ra các record, giờ là lúc đọc các record này và xử lý chúng. Trong database, chúng ta thường viết các câu truy vấn dữ liệu để lấy dữ liệu, ở trên lớp trên chúng ta cũng thực hiện các truy vấn thông qua Model. Để lấy tất cả các record trong table mà Model thể hiện tương ứng sử dụng phương thức all():
<?php
use App\Product;

$products = Product::all();
foreach ($products as $p) {
    echo 'Sản phẩm: ' . $p->name . ' có giá ' . number_format($p->price) . 'VNĐ' ;
}
Đoạn mã trên lấy tất cả các sản phẩm trong bảng products và duyệt qua các thực thể đó rồi in ra màn hình thông tin về sản phẩm. Chúng ta có thể thêm các điều kiện vào truy vấn:
<?php
use App\Product;

$products = Product::where('active', '=', 1)
		->where('price', '>', '350000')
		->orderBy('name')
		->take(10)
		->get();

foreach ($products as $p) {
    echo 'Sản phẩm: ' . $p->name . ' có giá ' . number_format($p->price) . 'VNĐ' ;
}
Các phương thức sử dụng để thêm điều kiện trong truy vấn như where, orderBy, take… bạn có thể xem thêm trong phần Xây dựng truy vấn database với Laravel Query Builder. Kết quả trả về của các phương thức get(), all() là một instance của lớp Illuminate\Database\Eloquent\Collection. Laravel Collection là một trong những phần cực hay về xử lý dữ liệu dạng tập hợp, nó xây dựng sẵn hàng trăm các hàm giúp bạn viết code cực nhanh với những yêu cầu tổng hợp dữ liệu phức tạp. Ví dụ: bạn muốn tính tổng theo các nhóm thỏa mãn một điều kiện trong một tập hợp chẳng hạn, chỉ cần 1 dòng code là bạn có thể thực hiện được. Khó tin nhỉ, xem phần Laravel Collection để kiểm chứng nhé. Bạn nào đã từng làm quen với Lodash một thư viện Javascript tuyệt vời cho xử lý các tập hợp thì Laravel Collection cũng tương tự như vậy.
Hơi dài dòng một chút vì có nhiều điều cần viết quá nhưng thôi chúng ta lại tập trung vào chủ đề của bài viết. Tình huống tiếp theo, chúng ta muốn tìm một sản phẩm khi biết id của sản phẩm hoặc muốn lấy một sản phẩm bất kỳ có giá 300k chẳng hạn, dễ dàng thực hiện với Eloquent Model:
<?php
use App\Product;

$product = Product::find($product_id);
echo 'Sản phẩm: ' . $product->name . ' có ID là ' . $product_id;

$product_300k = Product::where('price', '>', 300000)->first();
echo 'Sản phẩm: ' . $product->name . ' có giá ' . $product->price . ' VNĐ';
Chú ý, phương thức find() có thể truyền vào một mảng các id của sản phẩm
<?php

use App\Product;
// Trả về Collection 3 sản phẩm có ID 1,2,3
$products = Product::find([1,2,3]);
Các truy vấn cũng có thể đưa vào các hàm tổng hợp dữ liệu như với Laravel Query Builder như count(), max(), min()…
<?php
use App\Product;

// Số lượng sản phẩm đang đăng bán (có trạng thái active = 1)
$product_cnt = Product::where('active', '=', 1)->count();

// Giá sản phẩm rẻ nhất đang đăng bán
$min_price = Product::where('active', '=', 1)->min('price');

4. Cập nhật dữ liệu

Khi muốn update giá trị cột nào đó trong bảng, cũng đơn giản là tạo instance của Model tương ứng và sử dụng phương thức save() như ở trên:
<?php
use App\Product;

$product_id     = 5;
$product        = Product::find($product_id);
$product->name  = 'New product name';
$product->price = 500000;
$product->save();
Trên đây chúng ta update một bản ghi, vậy update cùng lúc nhiều bản ghi thì làm thế nào trong Eloquent Model? Ví dụ, tất cả các sản phẩm TENDA hiện đang hết hàng và chúng ta muốn chuyển chúng sang chế độ không đăng bán active = 0, chúng ta thực hiện như sau:
Product::where('active', 1)
          ->where('name', 'like', '%TENDA%')
          ->update(['active' => 0]);

5. Xóa bản ghi dữ liệu

Xóa bản ghi dữ liệu đơn giản bằng cách gọi phương thức delete() trên thực thể của Model:
$product = Product::find(1);
$product->delete();
Hoặc chúng ta có thể truy vấn dữ liệu và xóa dựa trên kết quả truy vấn: Xóa tất cả các sản phẩm đang không active.
$deletedProducts = Product::where('active', 0)->delete();
Xử lý bản ghi đã xóa
Trước đây, khi dung lượng và bộ xử lý trong máy chủ là một cái gì đó xa xỉ, các bản ghi cần được xóa triệt để khỏi cơ sở dữ liệu, hiện nay thì đã khác nhiều, chúng ta không cần quan tâm nhiều đến dung lượng lưu trữ. Các bản ghi cũng vì vậy chỉ được xóa mềm (soft delete), thực chất là thêm một trường đánh dấu là bản ghi này đã xóa. Các dữ liệu đã xóa là rất cần thiết cho việc phân tích hành vi người dùng hoặc kiểm tra debug các ứng dụng. Laravel hỗ trợ xóa mềm một bản ghi bằng cách thêm một thuộc tính deleted_at vào Model cũng như ở table trong database. Để cho phép một Model có thể thực hiện được đánh dấu bản ghi đã xóa, chúng ta sử dụng trait Illuminate\Database\Eloquent\SoftDeletes và thêm deleted_at vào thuộc tính $dates của nó:
<?php
namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use SoftDeletes;

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = ['deleted_at'];
}
Khi đó, nếu bạn thực hiện phương thức delete() thay vì nó sẽ xóa record đó đi thì nó sẽ cập nhật thời gian hiện tại vào trường deleted_at, và như vậy bản ghi này đã được đánh dấu là đã xóa. Chúng ta cũng có thể kiểm tra xem một thực thể của Model là được xóa mềm hay không bằng phương thức trashed()
if ($product->trashed()) {
    // Sản phẩm này đã được đánh dấu là đã xóa
}
Truy vấn các bản ghi được xóa "mềm"
Các bản ghi được đánh dấu là đã xóa sẽ không có kết quả trong các truy vấn dữ liệu thông thường, để thêm vào các kết quả từ các bản ghi đã xóa “mềm” chúng ta sử dụng phương thức withTrashed():
$comments = Comment::withTrashed()
                ->where('user_id', 1)
                ->get();
Nó sẽ trả về tất cả các bình luận của người dùng có id là 1 với cả các bình luận thông thường và bình luận đã được xóa “mềm”. Ngược lại nếu muốn chỉ truy vấn các kết quả trong các record đã được xóa mềm, sử dụng phương thức onlyTrashed():
$comments = Comment::onlyTrashed()
                ->where('user_id', 1)
                ->get();
Khôi phục các bản ghi đã xóa "mềm"
Xóa mềm là một cách rất hay do khi cần thiết chúng ta hoàn toàn có thể khôi phục được dữ liệu đã “xóa”, với phương thức restore(), bản ghi đã được khôi phục hoàn toàn:
// Khôi phục một bản ghi đã xóa "mềm"
$comment->restore();
// Khôi phục nhiều bản ghi đã xóa "mềm" thông qua truy vấn
Comment::onlyTrashed()->where('user_type', '=', 'admin')->restore();
Xóa vĩnh viễn bản ghi
Chúng ta có xóa “mềm” thì cũng phải có xóa “cứng” hay còn gọi là xóa vĩnh viễn bản ghi, tức là khi thực hiện bản ghi đó sẽ không còn trong database và như vậy các truy vấn thường lẫn truy vấn xóa “mềm” không còn kết quả gì liên quan. Thực hiện xóa “cứng” bằng phương thức forceDelete():
// Xóa cứng một bản ghi
$comment->forceDelete();
Query Scope - Phạm vi truy vẫn
Phạm vi truy vấn xuất phát từ một vấn đề khi chúng ta muốn thực hiện cùng một số điều kiện ràng buộc truy vấn với một hoặc nhiều các truy vấn, chúng ta không cần phải lặp lại chúng cho tất các truy vấn mà chỉ cần định nghĩa các Scope và sử dụng lại chúng trong định nghĩa Model. Tính năng xóa “mềm” (Soft delete) ở phần trên là một ví dụ, các ràng buộc về trường deleted_at được lặp lại cho tất cả các truy vấn, thay vì truy vấn nào chúng ta cũng đưa thêm các ràng buộc với trường deleted_at thì chúng ta tạo ra các Scope.
Phạm vi toàn cục
Với phạm vi toàn cục, định nghĩa Scope này sẽ được áp dụng cho một Model và tất cả các truy vấn liên quan đến Model đó sẽ được áp dụng thêm ràng buộc. Ví dụ chúng ta tạo ra một Scope có tên là AgeScope.php nằm trong thư mục app\scopes (nếu chưa có bạn tự tạo ra):
<?php
namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class AgeScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('age', '>', 18);
    }
}
Đây là một lớp kế thừa lại Illuminate\Database\Eloquent\Scope và sử dụng phương thức apply() để khai báo các ràng buộc thêm vào. Ở đây, chúng ta thêm vào một ràng buộc là tuổi phải lớn hơn 18. Khi muốn áp dụng Scope với phạm vi toàn cục này, thực hiện ghi đè phương thức boot với việc khai báo thêm phương thức addGlobalScope().
<?php

namespace App;

use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new AgeScope);
    }
}
Khi đó, mọi truy vấn bằng Model User sẽ được ràng buộc thêm là người dùng phải đủ 18 tuổi, khi đó nếu bạn sử dụng User::all() thì nó sẽ build ra truy vấn có dạng:
select * from `users` where `age` > 18
Laravel cũng cho phép sử dụng Closure, là một cách định nghĩa hàm nâng cao định nghĩa hàm trong một hàm:
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class User extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('age', function (Builder $builder) {
            $builder->where('age', '>', 18);
        });
    }
}
Nếu muốn truy vấn mà không bị ảnh hưởng bởi các Scope đã được áp dụng vào Model sử dụng phương thức withoutGlobalScopes(), phương thức này nếu không truyền tham số nào vào nó sẽ bỏ qua tất cả các Scope được áp dụng hoặc chúng ta có thể chỉ muốn bỏ các Scope nào đó thì truyền vào mảng tên các Scope đó.
// Bỏ đi toàn bộ các Scope được áp dụng trong Model User
User::withoutGlobalScopes()->get();

// Chỉ bỏ các Scope là FirstScope và SecondScope
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();
Phạm vi cục bộ
Các Scope phạm vi cục bộ cho phép bạn định nghĩa các tập ràng buộc để dễ dàng sử dụng cho chính Model đó, do vậy các Scope phạm vi cục bộ được định nghĩa ngay trong Model.
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Phạm vi truy vấn là các User đã bình chọn lớn hơn 100
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    /**
     * Phạm vi truy vấn là các User đang hoạt động
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $query->where('active', 1);
    }
}
Chú ý: Các Scope phạm vi cục bộ được định nghĩa bởi các hàm trong Model với tên bắt đầu bằng scope (scopePopular, scopeActive…). Khi đó bạn có thể sử dụng các Scope đã được định nghĩa này trong các truy vấn như sau:
$users = User::popular()->active()->orderBy('created_at')->get();
Lấy tất cả các user có lượng bình chọn lớn hơn 100 và đang hoạt động.
Phạm vi truy vấn động
Đôi khi bạn muốn các phạm vi truy vấn chấp nhận một tham số đầu vào, cùng xem ví dụ sau:
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include users of a given type.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param mixed $type
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeOfType($query, $type)
    {
        return $query->where('type', $type);
    }
}
Sau đó, chúng ta có thể truyền tham số khi gọi các Scope này:
$users = App\User::ofType('admin')->get();
Các sự kiện khi thao tác cơ sở dữ liệu
Eloquent Model sẽ tạo ra các sự kiện khi thao tác với cơ sở dữ liệu, các sự kiện này bao gồm: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored. Với các sự kiện này, bạn dễ dàng thực hiện được các công việc khác trước khi thực hiện một thao tác nào đó với database. Để khai báo sử dụng các sự kiện này với một Model, chúng ta sử dụng thuộc tính $events:
<?php

namespace App;

use App\Events\UserSaved;
use App\Events\UserDeleted;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The event map for the model.
     *
     * @var array
     */
    protected $events = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}
Khi đó Model User sẽ tạo ra các event saved và deleted khi các bản ghi được lưu và các bản ghi được xóa khỏi cơ sở dữ liệu. Công việc còn lại là lắng nghe các sự kiện và xử lý chúng khi sự kiện xảy ra.
Bài tiếp theo: Relationships trong Laravel >>
vncoder logo

Theo dõi VnCoder trên Facebook, để cập nhật những bài viết, tin tức và khoá học mới nhất!