As we have seen in the previous section, the PDOStatement class allows us to retrieve some information about the data contained in the result set. This information is called metadata, and you probably have already used some of it one way or another.
The most important metadata about a result set is, of course, the number of rows it contains. We can use the row count to enhance user experience by, for example, paginating long result sets. Our example library application is still quite small, with only three books so far, but as our database grows, we surely will need some tools to get the total row count for every table displayed and paginate it for easy browsing.
Traditionally, you would use the mysql_num_rows(), sqlite_num_rows() function or the pg_num_rows() function (depending on your database) to get the total number of rows returned by the query. In PDO, the method responsible for retrieving the number of rows is called PDOStatement::rowCount(). However, if you want to test it with the following code:
$q = $conn->query("SELECT * FROM books ORDER BY title");
$q->setFetchMode(PDO::FETCH_ASSOC);
var_dump($q->rowCount());
you will see that PDO returns 0 both for MySQL and SQLite. This is because PDO operates differently from the traditional database extensions. The documentation says, "If the last SQL statement executed by the associated PDOStatement class was a SELECT statement, some databases may return the number of rows returned by that statement. However, this behavior is not guaranteed for all databases and should not
be relied on for portable applications." Neither MySQL nor SQLite drivers support this functionality, and that's why the return value of this method is 0. We will see how to count the number of rows returned with PDO (so that this is a really portable method) in Chapter 5.
A RDBMS does not know how many rows a query will return till the last row has been retrieved. This is done because of performance considerations. In most cases, queries with a WHERE clause, return only part of the rows stored in a table, and database servers do their best to ensure that such queries execute as fast as possible. This means that they start returning rows as soon as they discover those that match the WHERE clause—this happens much earlier than when the last row is reached.
That is why they really don't know how many rows will be returned beforehand. The mysql_num_rows(), sqlite_num_rows() function or the pg_num_rows() function operates on result sets that have been prefetched into memory (buffered queries). PDO's default behavior is to use unbuffered queries. We will speak about MySQL buffered queries later in Chapter 6.
Another method that can be of interest is the PDOStatement::columnCount() method, which returns the number of columns in the result set. It is handy when we execute arbitrary queries. (For example, a database management application like phpMyAdmin could make great use of this method, as it allows a user to type arbitrary SQL queries.) We can use it in the following way:
$q = $conn->query("SELECT authors.id AS authorId, firstName, lastName, books.* FROM authors, books WHERE author=authors.id ORDER BY title");
var_dump($q->columnCount());
This will reveal that our query returns a result set containing 10 columns (seven columns from the books table and three columns from authors table).
Unfortunately, PDO currently does not allow you to retrieve the name of the table or of a particular column from a result set to which it belongs. This functionality is useful if your application utilizes queries that join two or more tables. In such case, it is possible to fetch the table name for every column given its numeric index, starting with 0. However, proper use of column aliases eliminates the need to use such functionality. For example, when we modified the books listing page to display the author's name, we aliased the author's ID column to avoid name conflict. That alias clearly identifies the column as belonging to the authors table.
Summary
In this chapter, we took our first steps with PDO and even created a small working database-driven, dynamic application that runs on two different databases. Now you should be able to connect to any supporting database, using the rules for constructing a connection string. You should then be able to run queries against it, and to traverse and display the result set.
In the next chapter, we will deal with a very important aspect of any
database-driven application—error handling. We will also extend our example application by giving it the ability to add and edit books and authors, thus making it more realistic and useful.
Error Handling
Now that we have built our first application that uses PDO, we will take a closer look at an important aspect of user-friendly web applications—error handling. Not onlyerror handling. Not only. Not only does it inform the user about an error condition, it also limits the damage if an error is not detected when it occurred.
Most web applications have rather simple error handling strategy. When an error occurs, the script terminates and an error page is presented. The error should be logged in the error log, and the developers or maintainers should check the logs periodically. The most common sources of errors in database-driven web applications are the following:
Server software failure or overload such as the famous "too many connections" error
Inappropriate configuration of the application, which may happen when we use an incorrect connection string, a rather common mistake when an application is moved from one host to another
Improper validation of user input, which may lead to malformed SQL and subsequent failure of the query
Inserting a record with a duplicate primary key or unique index value, which either results from an error in the business logic of the application or may occur in a controlled situation
Syntax errors in SQL statements
In this chapter, we will extend our application so that we can edit existing records as well as add new records. As we will deal with user input supplied via web forms, we have to take care of its validation. Also, we may add error handling so that we can react to non-standard situations and present the user with a friendly message.
•
•
•
•
•
Before we proceed, let's briefly examine the sources of errors mentioned above and see what error handling strategy should be applied in each case. Our error handling strategy will use exceptions, so you should be familiar with them. If you are not, you can refer to Appendix A, which will introduce you to the new object-oriented features of PHP5.
We have consciously chosen to use exceptions, even though PDO can be instructed not to use them, because there is one situation where they cannot be avoided. The PDO constructors always throw an exception when the database object cannot be created, so we may as well use exceptions as our main error-trapping method throughout the code.