Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Advanced PHP Programming

.pdf
Скачиваний:
71
Добавлен:
14.04.2015
Размер:
7.82 Mб
Скачать

308 Chapter 12 Interacting with Databases

public $firstname; public $lastname; public $salutation; public $countrycode;

public static function findByUsername($username)

{

$dbh = new DB_Mysql_Test;

$query = SELECT * from users WHERE username = :1;

list($userid) = $dbh->prepare($query)->execute($username)->fetch_row(); if(!$userid) {

throw new Exception(no such user);

}

return new User($userid);

}

public function __construct($userid = false)

{

if(!$userid) { return;

}

$dbh = new DB_Mysql_Test;

$query = SELECT * from users WHERE userid = :1;

$data = $dbh->prepare($query)->execute($userid)->fetch_assoc(); foreach( $data as $attr => $value ) {

$this->$attr = $value;

}

}

public function update()

{

if(!$this->userid) {

throw new Exception(User needs userid to call update());

}

$query = UPDATE users

SET username = :1, firstname = :2, lastname = :3, salutation = :4, countrycode = :5

WHERE userid = :6; $dbh = new DB_Mysql_Test;

$dbh->prepare($query)->execute($this->username, $this->firstname, $this->lastname, $this->salutation, $this->countrycode, $this->userid) ;

}

$userid

TEAM

FLY

Database Access Patterns

309

 

public function insert()

{

if($this->userid) {

throw new Exception(User object has a userid, cant insert);

}

$query = INSERT INTO users

(username, firstname, lastname, salutation, countrycode) VALUES(:1, :2, :3, :4, :5);

$dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($this->username, $this->firstname,

$this->lastname, $this->salutation, $this->countrycode);

list($this->userid) =

$dbh->prepare(select last_insert_id())->execute()->fetch_row();

}

public function delete()

{

if(!$this->userid) {

throw new Exception(User object has no userid);

}

$query = DELETE FROM users WHERE userid = :1; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($this->userid);

}

}

Using this User class is easy.To instantiate a user by user ID, you pass it into the constructor:

$user = new User(1);

If you want to find a user by username, you can use the static findByUsername method to create the object:

$user = User::findByUsername(george);

Whenever you need to save the object’s state permanently, you call the update() method to save its definitions.The following example changes my country of residence to Germany:

$user = User::findByUsername(george); $user->countrycode = de;

$user->update();

When you need to create a completely new User object, you instantiate one, fill out its details (except for $userid, which is set by the database), and then call insert on it. This performs the insert and sets the value in the object.The following code creates a user object for Zak Greant:

310 Chapter 12 Interacting with Databases

$user = new User; $user->firstname = Zak; $user->lastname = Greant; $user->username = zak; $user->countrycode = ca; $user->salutation = M.; $user->insert();

The Active Record pattern is extremely useful for classes that have a simple correspondence with individual database rows. Its simplicity and elegance make it one of my favorite patterns for simple data models, and it is present in many of my personal projects.

The Mapper Pattern

The Active Record pattern assumes that you are dealing with a single table at a time. In the real world, however, database schemas and application class hierarchies often evolve independently. Not only is this largely unavoidable, it is also not entirely a bad thing:The ability to refactor a database and application code independently of each other is a positive trait. The Mapper pattern uses a class that knows how to save an object in a distinct database schema.

The real benefit of the Mapper pattern is that with it you completely decouple your object from your database schema.The class itself needs to know nothing about how it is saved and can evolve completely separately.

The Mapper pattern is not restricted to completely decoupled data models.The simplest example of the Mapper pattern is to split out all the database access routines from an Active Record adherent. Here is a reimplementation of the Active Record pattern User class into two classes—User, which handles all the application logic, and UserMapper, which handles moving a User object to and from the database:

require_once DB.inc; class User {

public $userid; public $username; public $firstname; public $lastname; public $salutation; public $countrycode;

public function __construct($userid = false, $username = false, $firstname = false, $lastname = false, $salutation = false, $countrycode = false)

{

$this->userid = $userid; $this->username = $username; $this->firstname = $firstname;

Database Access Patterns

311

$this->lastname = $lastname; $this->salutation = $salutation; $this->countrycode = $countrycode;

}

}

class UserMapper {

public static function findByUserid($userid)

{

$dbh = new DB_Mysql_Test;

$query = SELECT * FROM users WHERE userid = :1;

$data = $dbh->prepare($query)->execute($userid)->fetch_assoc(); if(!$data) {

return false;

}

return new User($userid, $data[username], $data[firstname], $data[lastname], $data[salutation], $data[countrycode]);

}

public static function findByUsername($username)

{

$dbh = new DB_Mysql_Test;

$query = SELECT * FROM users WHERE username = :1;

$data = $dbh->prepare($query)->execute($username)->fetch_assoc(); if(!$data) {

return false;

}

return new User($data[userid], $data[username], $data[firstname], $data[lastname], $data[salutation], $data[countrycode]);

}

public static function insert(User $user)

{

if($user->userid) {

throw new Exception(User object has a userid, cant insert);

}

$query = INSERT INTO users

(username, firstname, lastname, salutation, countrycode) VALUES(:1, :2, :3, :4, :5);

$dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($user->username, $user->firstname,

$user->lastname, $user->salutation, $user->countrycode);

list($user->userid) =

312 Chapter 12 Interacting with Databases

$dbh->prepare(select last_insert_id())->execute()->fetch_row();

}

public static function update(User $user)

{

if(!$user->userid) {

throw new Exception(User needs userid to call update());

}

$query = UPDATE users

SET username = :1, firstname = :2, lastname = :3, salutation = :4, countrycode = :5

WHERE userid = :6; $dbh = new DB_Mysql_Test;

$dbh->prepare($query)->execute($user->username, $user->firstname, $user->lastname, $user->salutation, $user->countrycode, $user->userid);

}

public static function delete(User $user)

{

if(!$user->userid) {

throw new Exception(User object has no userid);

}

$query = DELETE FROM users WHERE userid = :1; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($userid);

}

}

User knows absolutely nothing about its corresponding database entries. If you need to refactor the database schema for some reason, User would not have to be changed; only UserMapper would. Similarly, if you refactor User, the database schema does not need to change.The Mapper pattern is thus similar in concept to the Adaptor pattern that you learned about in Chapter 2,“Object-Oriented Programming Through Design Patterns”: It glues together two entities that need not know anything about each other.

In this new setup, changing my country back to the United States would be done as follows:

$user = UserMapper::findByUsername(george); $user->countrycode = us;

UserMapper::update($user);

Refactoring with the Mapper pattern is easy. For example, consider your options if you want to use the name of the user’s country as opposed to its ISO code in User. If you are using the Active Record pattern, you have to either change your underlying users table or break the pattern by adding an ad hoc query or accessor method.The Mapper pattern instead instructs you only to change the storage routines in UserMapper. Here is the example refactored in this way:

Database Access Patterns

313

class User { public $userid; public $username;

public $firstname; public $lastname; public $salutation; public $countryname;

public function __construct($userid = false, $username = false, $firstname = false, $lastname = false, $salutation = false, $countryname = false)

{

$this->userid = $userid; $this->username = $username; $this->firstname = $firstname; $this->lastname = $lastname; $this->salutation = $salutation; $this->countryname = $countryname;

}

}

class UserMapper {

public static function findByUserid($userid)

{

$dbh = new DB_Mysql_Test;

$query = SELECT * FROM users u, countries c WHERE userid = :1

AND u.countrycode = c.countrycode;

$data = $dbh->prepare($query)->execute($userid)->fetch_assoc(); if(!$data) {

return false;

}

return new User($userid, $data[username], $data[firstname], $data[lastname], $data[salutation], $data[name]);

}

public static function findByUsername($username)

{

$dbh = new DB_Mysql_Test;

$query = SELECT * FROM users u, countries c WHERE username = :1

AND u.countrycode = c.countrycode;

$data = $dbh->prepare($query)->execute($username)->fetch_assoc(); if(!$data) {

return false;

}

314 Chapter 12 Interacting with Databases

return new User($data[userid], $data[username], $data[firstname], $data[lastname], $data[salutation], $data[name]);

}

public static function insert(User $user)

{

if($user->userid) {

throw new Exception(User object has a userid, cant insert);

}

$dbh = new DB_Mysql_Test;

$cc_query = SELECT countrycode FROM countries WHERE name = :1; list($countrycode) =

$dbh->prepare($cc_query)->execute($user->countryname)->fetch_row(); if(!$countrycode) {

throw new Exception(Invalid country specified);

}

$query = INSERT INTO users

(username, firstname, lastname, salutation, countrycode) VALUES(:1, :2, :3, :4, :5);

$dbh->prepare($query)->execute($user->username, $user->firstname, $user->lastname, $user->salutation, $countrycode) ;

list($user->userid) =

$dbh->prepare(select last_insert_id())->execute()->fetch_row();

}

public static function update(User $user)

{

if(!$user->userid) {

throw new Exception(User needs userid to call update());

}

$dbh = new DB_Mysql_Test;

$cc_query = SELECT countrycode FROM countries WHERE name = :1; list($countrycode) =

$dbh->prepare($cc_query)->execute($user->countryname)->fetch_row(); if(!$countrycode) {

throw new Exception(Invalid country specified);

}

$query = UPDATE users

SET username = :1, firstname = :2, lastname = :3, salutation = :4, countrycode = :5

WHERE userid = :6; $dbh->prepare($query)->execute($user->username, $user->firstname,

$user->lastname, $user->salutation, $countrycode, $user->userid);

}

Database Access Patterns

315

public static function delete(User $user)

{

if(!$user->userid) {

throw new Exception(User object has no userid);

}

$query = DELETE FROM users WHERE userid = :1; $dbh = new DB_Mysql_Test; $dbh->prepare($query)->execute($userid);

}

}

Notice that User is changed in the most naive of ways:The now deprecated $countrycode attribute is removed, and the new $countryname attribute is added. All the work is done in the storage methods. findByUsername() is changed so that it pulls not only the user record but also the country name for the user’s record from the countries lookup table. Similarly insert() and update() are changed to perform the necessary work to find the country code for the user’s country and update accordingly.

The following are the benefits of the Mapper pattern:

nIn our example, User is not concerned at all with the database storage of users. No SQL and no database-aware code needs to be present in User.This makes tuning the SQL and interchanging database back ends much simpler.

nIn our example, the database schema for the table users does not need to accommodate the changes to the User class.This decoupling allows application development and database management to proceed completely independently. Certain changes to the class structures might make the resulting SQL in the Mapper class inefficient, but the subsequent refactoring of the database tables will be independent of User.

The drawback of the Mapper pattern is the amount of infrastructure it requires.To adhere to the pattern, you need to manage an extra class for mapping each complex data type to its database representation.This might seem like overkill in a Web environment. Whether that complaint is valid really depends on the size and complexity of the application.The more complex the objects and data mappings are and the more often the code will be reused, the greater the benefit you will derive from having a flexible albeit large infrastructure in place.

The Integrated Mapper Pattern

In the Active Record pattern, the object is database aware—that is, it contains all the methods necessary to modify and access itself. In the Mapper pattern, all this responsibility is delegated to an external class, and this is a valid problem with this pattern in many PHP applications. In a simple application, the additional layer required for splitting out the database logic into a separate class from the application logic may be overkill. It incurs overhead and makes your code base perhaps needlessly complex.The Integrated

316Chapter 12 Interacting with Databases

Mapper pattern is a compromise between the Mapper and Active Record patterns that provides a loose coupling of the class and its database schema by pulling the necessary database logic into the class.

Here is User with an Integrated Mapper pattern: class User {

public $userid; public $username; public $firstname; public $lastname; public $salutation; public $countryname;

public function __construct($userid = false)

{

$dbh = new DB_Mysql_Test;

$query = SELECT * FROM users u, countries c WHERE userid = :1

AND u.countrycode = c.countrycode;

$data = $dbh->prepare($query)->execute($userid)->fetch_assoc(); if(!$data) {

throw new Exception(userid does not exist);

}

$this->userid = $userid; $this->username = $data[username]; $this->firstname = $data[firstname]; $this->lastname = $data[lastname]; $this->salutation = $data[salutation]; $this->countryname = $data[name];

}

public static function findByUsername($username)

{

$dbh = new DB_Mysql_Test;

$query = SELECT userid FROM users u WHERE username = :1; list($userid) = $dbh->prepare($query)->execute($username)->fetch_row(); if(!$userid) {

throw new Exception(username does not exist);

}

return new User($userid);

}

public function update()

{

if(!$this->userid) {

throw new Exception(User needs userid to call update());

}

Tuning Database Access

317

$dbh = new DB_Mysql_Test;

$cc_query = SELECT countrycode FROM countries WHERE name = :1; list($countrycode) =

$dbh->prepare($cc_query)->execute($this->countryname)->fetch_row(); if(!$countrycode) {

throw new Exception(Invalid country specified);

}

$query = UPDATE users

SET username = :1, firstname = :2, lastname = :3, salutation = :4, countrycode = :5

WHERE userid = :6; $dbh->prepare($query)->execute($this->username, $this->firstname,

$this->lastname, $this->salutation, $countrycode, $this->userid);

}

/* update and delete */

// ...

}

This code should look very familiar, as it is almost entirely a merge between the Active Record pattern User class and the database logic of UserMapper. In my mind, the decision between making a Mapper pattern part of a class or an external entity is largely a matter of style. In my experience, I have found that while the elegance of the pure Mapper pattern is very appealing, the ease of refactoring brought about by the identical interface of the Active Record and Integrated Mapper patterns make them my most common choices.

Tuning Database Access

In almost all the applications I have worked with, database access has consistently been the number-one bottleneck in application performance.The reason for this is pretty simple: In many Web applications, a large portion of content is dynamic and is contained in a database. No matter how fast your database access is, reaching across a network socket to pull data from your database is slower than pulling it from local process memory. Chapters 9,“External Performance Tunings,” 10,“Data Component Caching,” and 11, “Computational Reuse,” you show various ways to improve application performance by caching data. Caching techniques aside, you should ensure that your database interactions are as fast as possible.The following sections discuss techniques for improving query performance and responsiveness.

Limiting the Result Set

One of the simplest techniques for improving query performance is to limit the size of your result sets. A common mistake is to have a forum application from which you need to extract posts N through N+M.The forum table looks like this:

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]