PDO trong PHP – Khái niệm và những thao tác cơ bản

Nếu bạn là một nhà phát triển PHP, chắc chắn bạn đã quen thuộc với việc truy xuất cơ sở dữ liệu bằng các extension MySQL và MySQLi. Từ phiên bản PHP 5.1, chúng ta có một cách tiếp cận tốt hơn là sử dụng PHP Data Objects. PDO cung cấp các cơ chế Prepared Statements, Stored Procedures và giúp thao tác cơ sở dữ liệu thông qua các đối tượng, làm công việc trở nên hiệu quả và dễ dàng hơn.

So sánh PDO và MySQLi

PDO MySQLi

Hỗ trợ hơn 12 loại cơ sở dữ liệu Chỉ hỗ trợ MySQL API

Hướng đối tượng (OOP) Hướng đối tượng (OOP) – Hướng thủ tục (Procedural)

Kết nối cơ sở dữ liệu Dễ dàng Dễ dàng

Đặt tên tham số Có Không

Object Mapping Có Có

Prepared Statements Có Không

Hiệu năng Cao Cao

Stored Procedures Có Có

1. Giới thiệu PDO – PHP Data Objects

PHP Data Objects (PDO) là một lớp truy xuất cơ sở dữ liệu cung cấp một phương pháp thống nhất để làm việc với nhiều loại cơ sở dữ liệu khác nhau. Khi làm việc với PDO, bạn không cần phải viết các câu lệnh SQL cụ thể mà chỉ sử dụng các phương thức mà PDO cung cấp, giúp tiết kiệm thời gian và dễ dàng chuyển đổi hệ quản trị cơ sở dữ liệu bằng cách thay đổi chuỗi kết nối CSDL.

Bạn chỉ cần nắm rõ API mà PDO cung cấp là có thể làm việc được với nhiều hệ quản trị cơ sở dữ liệu khác nhau như MySQL, SQLite, PostgreSQL, Microsoft SQL Server,… và có thể dễ dàng chuyển đổi chúng.

Các hệ quản trị cơ sở dữ liệu mà PDO hỗ trợ gồm có:

  • PDO_CUBRID: Cubrid
  • PDO_DBLIB: FreeTDS Microsoft SQL Server / Sybase
  • PDO_FIREBIRD: Firebird
  • PDO_IBM: IBM DB2
  • PDO_INFORMIX: IBM Informix Dynamic Server
  • PDO_MYSQL: MySQL 3.x/4.x/5.x
  • PDO_OCI: Oracle Call Interface
  • PDO_ODBC: ODBC v3 (IBM DB2, unixODBC và win32 ODBC)
  • PDO_PGSQL: PostgreSQL
  • PDO_SQLITE: SQLite 3 và SQLite 2
  • PDO_SQLSRV: Microsoft SQL Server / SQL Azure
  • PDO_4D: 4D

2. Kết nối cơ sở dữ liệu

Mỗi hệ quản trị cơ sở dữ liệu sẽ có các phương thức kết nối khác nhau (có loại cần Tên người dùng, Mật khẩu, Đường dẫn đến CSDL, Cổng, có loại không). Chuỗi kết nối cho các hệ quản trị cơ sở dữ liệu phổ biến thường có dạng như sau:

$conn = new PDO(‘mysql:host=localhost;dbname=izlearn’, $username, $password);

Với mysql là tên của hệ quản trị cơ sở dữ liệu, localhost có nghĩa là cơ sở dữ liệu được đặt trên cùng một máy chủ, izlearn là tên của cơ sở dữ liệu. $username và $password là hai biến lưu trữ thông tin xác thực.

Đối với SQLite, hệ quản trị cơ sở dữ liệu này không có cơ chế xác thực bằng người dùng và mật khẩu, mà chỉ đơn giản là đường dẫn đến tệp dữ liệu:

$conn = new PDO(“sqlite:your/database/path/izlearn.db”);

Đây là lúc bạn nên quên đi chuỗi kết nối lỗi thời mysql_connect(‘localhost’, ‘username’, ‘password’) or die(‘Could not connect: ‘ . mysql_error()); Hiện vẫn còn rất nhiều bài viết ở Việt Nam hướng dẫn người mới sử dụng cách kết nối CSDL dạng này vì cho rằng nó đơn giản. Thực tế, họ chỉ đang đưa bạn trở lại quá khứ thôi. Để ngắt kết nối khi không cần thao tác với cơ sở dữ liệu nữa, bạn chỉ cần gán biến $conn về null;

$conn = null;

3. Insert và Update

Thêm mới (insert) và cập nhật (update) dữ liệu là những hoạt động cơ bản khi thao tác với cơ sở dữ liệu. Với PDO, mỗi hoạt động insert hoặc update được thực hiện thông qua 3 quá trình sử dụng cơ chế Prepared Statement

  • Chuẩn bị câu lệnh: Chuẩn bị một câu lệnh SQL làm khung/mẫu gọi là Prepared Statement với các Placeholder (có thể hiểu placeholder như là tham số của các phương thức khi bạn khai báo hàm)
  • Bind params: Gắn giá trị thực vào các placeholder (tương tự như khi bạn truyền giá trị vào các tham số của phương thức)
  • Execute: Thực thi câu lệnh.

Prepared Statement

Có 2 loại Placeholder trong Prepared Statement: Placeholder không định danh (Unnamed Placeholder) và Placeholder định danh (Named Placeholder) như ví dụ sau:

$stmt = $conn->prepare(‘INSERT INTO users (name, email, age) values (?, ?, ?)’);

$stmt = $conn->prepare(‘INSERT INTO users (name, email, age) values (:name, :mail, :age)’);

Dòng lệnh thứ nhất sử dụng Placeholder không định danh là các dấu hỏi – ?. Dòng lệnh thứ hai sử dụng Placeholder định danh: :name, :mail, :age (lưu ý dấu hai chấm và placeholder không cần phải giống tên cột). Dưới đây là quá trình Insert và Update hoàn chỉnh sử dụng 2 loại Placeholder đã nêu:

Unnamed Placeholder

//Khởi tạo Prepared Statement từ biến $conn ở phần trước

$stmt = $conn->prepare(‘INSERT INTO users (name, email, age) values (?, ?, ?)’);

//Gán các biến (lúc này chưa mang giá trị) vào các placeholder theo thứ tự tương ứng

$stmt->bindParam(1, $name); $stmt->bindParam(2, $mail); $stmt->bindParam(3, $age);

//Gán giá trị và thực thi

$name = “Vu Hoang Lam”; $mail = “[email protected]”; $age = 22; $stmt->execute();

//Gán những giá trị khác và tiếp tục thực thi

$name = “Nguyen Van A”; $mail = “[email protected]”; $age = 23; $stmt->execute();

Như bạn thấy, ta chỉ cần khởi tạo Prepared Statement một lần và có thể sử dụng nhiều lần. Đối với mỗi cột – placeholder, ta chỉ cần gán tham số một lần, điều này không gây khó khăn với những bảng có ít cột như ví dụ trên, nhưng sẽ rất bất tiện nếu bảng có nhiều cột. May mắn là ta có một cách khác để làm việc này, đó là lưu toàn bộ giá trị vào một mảng và truyền mảng này vào phương thức execute(), cụ thể như sau:

$stmt = $conn->prepare(‘INSERT INTO users (name, email, age) values (?, ?, ?)’);

$data = array(‘Vu Hoang Lam’, ‘[email protected]’, 22);

$stmt->execute($data);

Named Placeholder

Đối với Named Placeholder, cách thực hiện tương tự như Unnamed Placeholder, chỉ khác là ta không dùng thứ tự placeholder để gán giá trị (bind) mà dùng chính tên của placeholder:

//Khởi tạo Prepared Statement từ biến $conn ở phần trước

$stmt = $conn->prepare(‘INSERT INTO users (name, email, age) values (:name, :mail, :age)’);

//Gán các biến (lúc này chưa có giá trị) vào các placeholder theo tên của chúng

$stmt->bindParam(‘:name’, $name); $stmt->bindParam(‘:mail’, $mail); $stmt->bindParam(‘:age’, $age);

//Gán giá trị và thực thi

$name = “Vu Hoang Lam”; $mail = “[email protected]”; $age = 22; $stmt->execute();

Bạn cũng có thể sử dụng mảng để rút gọn:

//Lưu ý: Không cần thiết phải sử dụng dấu hai chấm cho các key

$data = array(‘name’ => ‘Vu Hoang Lam’, ‘mail’ => ‘[email protected]’, ‘age’ => 22);

Một mẹo hữu ích khác khi sử dụng Named Placeholder là insert Object class:

class User { public $name; public $mail; public $age; }

$person = new $user(); $person->name = ‘Vu Hoang Lam’; $person->mail = ‘[email protected]’; $person->age = 22;

$stmt = $conn->prepare(‘INSERT INTO users (name, email, age) values (:name, :mail, :age)’);

$stmt->execute((array)$person);

Ở dòng cuối cùng, tôi đã thực hiện “ép kiểu” (cast) Object $person thành một mảng để truyền vào phương thức execute();

Việc sử dụng Prepared Statement sẽ giúp bạn tránh được SQL Injection, tôi sẽ đi sâu giải thích vấn đề này trong một bài viết khác.

4. Select Data – “Đọc” dữ liệu từ cơ sở dữ liệu

Khi đọc dữ liệu từ cơ sở dữ liệu, PDO sẽ trả về dữ liệu dưới dạng mảng (array) hoặc đối tượng (object) thông qua phương thức fetch(). Bạn nên thiết lập cấu trúc dữ liệu trước khi gọi phương thức này. PDO hỗ trợ các tuỳ chọn sau:

  • PDO::FETCH_ASSOC: Trả về dữ liệu dạng mảng với tên của cột là key
  • PDO::FETCH_BOTH (mặc định): Trả về dữ liệu dạng mảng với tên của cột và số thứ tự của cột là key
  • PDO::FETCH_BOUND: Gán giá trị của từng cột cho từng biến đã được khởi tạo trước đó thông qua phương thức bindColumn()
  • PDO::FETCH_CLASS: Gán giá trị của từng cột cho từng thuộc tính của một lớp Class theo tên cột và tên thuộc tính
  • PDO::FETCH_INTO: Gán giá trị của từng cột cho từng thuộc tính của một Instance Class
  • PDO::FETCH_LAZY: Gộp PDO::FETCH_BOTH/PDO::FETCH_OBJ
  • PDO::FETCH_NUM: Trả về dữ liệu dạng mảng với số thứ tự của cột là key
  • PDO::FETCH_OBJ: Trả về một Object của stdClass với tên thuộc tính của Object là tên của cột

Thực tế, chúng ta thường chỉ sử dụng 3 kiểu fetch: FETCH_ASSOC, FETCH_CLASS và FETCH_OBJ. Để thiết lập cấu trúc dữ liệu (Fetch Style hay Fetch Mode) trước khi fetch, sử dụng câu lệnh sau:

$stmt->setFetchMode(PDO::FETCH_ASSOC);

Hoặc nếu muốn, bạn có thể thiết lập kiểu fetch khi gọi hàm fetch():

$stmt->fetch(PDO::FETCH_ASSOC);

FETCH_ASSOC

Kiểu fetch này sẽ tạo ra một mảng kết hợp có khóa là tên của cột, tương tự như khi sử dụng MySQL/MySQLi Extension.

//Tạo Prepared Statement

$stmt = $conn->prepare(‘SELECT email, age from users WHERE name = :name’);

//Thiết lập kiểu dữ liệu trả về

$stmt->setFetchMode(PDO::FETCH_ASSOC);

// Gán giá trị và thực thi

$stmt->execute(array(‘name’ => ‘a’));

//Hiển thị kết quả, vòng lặp sau đây sẽ dừng lại khi đã duyệt qua toàn bộ kết quả

while($row = $stmt->fetch()) { echo $row[‘name’], ‘n’; echo $row[’email’], ‘n’; echo $row[‘age’], ‘n’; }

FETCH_OBJ

Kiểu fetch này trả về một đối tượng stdClass cho mỗi hàng của kết quả.

//Tạo Prepared Statement

$stmt = $conn->prepare(‘SELECT email, age from users WHERE name = :name’);

//Thiết lập kiểu dữ liệu trả về

$stmt->setFetchMode(PDO::FETCH_OBJ);

//Gán giá trị và thực thi

$stmt->execute(array(‘name’ => ‘a’));

//Hiển thị kết quả, vòng lặp sau đây sẽ dừng lại khi đã duyệt qua toàn bộ kết quả trả về

while($row = $stmt->fetch()) { echo $row->name, ‘n’; echo $row->email, ‘n’; echo $row->age, ‘n’; }

FETCH_CLASS

Kiểu fetch này cho phép bạn gán kết quả vào một đối tượng của một lớp được chỉ định trước. Khi sử dụng FETCH_CLASS, thuộc tính của lớp sẽ được gán giá trị trước khi constructor của lớp được gọi (hãy chú ý điều này vì rất quan trọng). Nếu không có thuộc tính nào khớp với tên của cột, thuộc tính đó sẽ được tạo tự động (public).

Giả sử table users có một lớp User đã được định nghĩa như sau:

class User { public $name; public $email; public $isAdmin = ‘No’; function __construct() { if ($this->name == ‘Vu Hoang Lam’) $this->isAdmin = ‘Yes’; } }

Khi truy vấn dữ liệu từ cơ sở dữ liệu sử dụng đoạn mã sau:

//Tạo Prepared Statement

$stmt = $conn->prepare(‘SELECT email, age from users WHERE name = :name’);

//Thiết lập kiểu dữ liệu trả về, chỉ định dữ liệu được đưa vào object của lớp User

$stmt->setFetchMode(PDO::FETCH_CLASS, ‘User’);

//Gán giá trị và thực thi

$stmt->execute(array(‘name’ => ‘a’));

//Hiển thị kết quả, vòng lặp sau đây sẽ dừng lại khi đã duyệt qua toàn bộ kết quả trả về

while($obj = $stmt->fetch()) { echo $obj->email; echo $obj->isAdmin; }

Vì constructor được gọi sau khi thuộc tính $name được gán bằng ‘Vu Hoang Lam’, nên $isAdmin sẽ mang giá trị ‘Yes’. Nếu muốn constructor của lớp được gọi trước khi các thuộc tính được gán giá trị, bạn phải sử dụng thêm PDO::FETCH_PROPS_LATE. Cách sử dụng như sau:

//Hãy thử sử dụng kiểu fetch này và so sánh kết quả hiển thị

$stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, ‘User’);

Nếu bạn cần truyền các tham số cho constructor của lớp thông qua phương thức fetch(), bạn có thể đặt chúng trong một mảng theo thứ tự tương ứng như sau:

$stmt->setFetchMode(PDO::FETCH_CLASS, ‘User’, array(‘param1’, ‘param2’, ‘param3’));

Exceptions – Xử lý ngoại lệ

PDO sử dụng Exceptions để xử lý lỗi phát sinh khi làm việc với cơ sở dữ liệu, vì vậy, tất cả những gì bạn làm với PDO nên được đặt trong một khối try/catch. PDO cung cấp 3 chế độ xử lý lỗi (Error Mode) có thể được thiết lập thông qua phương thức setAttribute():

$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);

$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);

$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

PDO::ERRMODE_SILENT

Đây là chế độ xử lý lỗi mặc định của PDO, khi gặp lỗi, PDO sẽ không thông báo và chương trình sẽ tiếp tục chạy. Bạn có thể lấy mã lỗi và thông tin về các lỗi đã xảy ra thông qua PDO::errorCode() và PDO::errorInfo()

PDO::ERRMODE_WARNING

Ở chế độ này, khi gặp lỗi PDO sẽ thông báo một cảnh báo PHP Warning, chương trình vẫn tiếp tục chạy.

PDO::ERRMODE_EXCEPTION

Đây là chế độ mà bạn nên sử dụng, khi đặt trong một khối try/catch block, nó sẽ giúp bạn kiểm soát các lỗi phát sinh một cách mượt mà và ẩn các thông báo lỗi có thể làm cho kẻ tấn công khai thác hệ thống của bạn.

try { $stmt = new PDO(‘mysql:host=localhost;dbname=izlearn’, ‘lamvh’, ‘talapassday’); $stmt->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //Sai cú pháp, FORM thay vì FROM $stmt->prepare(‘SELECT name FORM people’); } catch(PDOException $e) { echo “ERROR! Có lỗi xảy ra với PDO”; file_put_contents(‘PDOErrors.txt’, $e->getMessage(), FILE_APPEND); }

Đoạn mã trên sẽ ghi thông báo lỗi vào một file văn bản có tên PDOErrors.txt

Một số phương thức hữu ích khác

$conn->lastInsertId();

Phương thức trên trả về Auto Incremented ID của hàng vừa thêm gần nhất.

$conn->exec(‘DELETE FROM users WHERE uid = 1’);

Đối với các câu lệnh SQL không trả về dữ liệu và không cần truyền tham số, bạn có thể sử dụng phương thức exec(). Phương thức này sẽ trả về số lượng hàng bị ảnh hưởng sau khi thực hiện câu lệnh. Ví dụ trên sẽ trả về số lượng hàng bị xóa.

$conn = $DBH->quote($foo);

Phương thức quote() sẽ giúp bạn thêm dấu nháy cho một chuỗi để đảm bảo an toàn khi sử dụng trong truy vấn. Đây là lựa chọn nếu bạn không muốn sử dụng Prepared Statement.

$stmt->rowCount();

Phương thức rowCount() trả về số lượng hàng bị ảnh hưởng sau khi thực hiện các thao tác DELETE, INSERT và UPDATE. Sử dụng rowCount() cho các thao tác SELECT có thể trả về kết quả không chính xác trong một số loại cơ sở dữ liệu.

Tham khảo: http://www.izlearn.com/

Related Posts