431 Chapter 16 Managing Multiple Database Tables The previous chapter showed you how to use INNER JOIN and LEFT JOIN to retrieve information stored in multiple tables. You also learned how to link existing tables by adding an extra column to the child table and updating each record individually to insert a foreign key. However, most of the time, youll want to insert data simultaneously in both tables. That presents a problem, because INSERT commands can operate on only one table at a time. You need to get around this restriction by constructing scripts that handle the INSERT operations in the correct sequence, starting with the parent table, so that you can get the new records primary key and insert it in the child table at the same time as other details. Similar considerations also need to be taken into account when updating and deleting records. The code involved isnt difficult, but you need to keep the sequence of events clearly in mind as you build the scripts. This chapter guides you through the process of inserting new articles in the blog table, optionally selecting a related image or uploading a new one, and assigning the article to one or more categories, all in a single operation. Then, youll build the scripts to update and delete articles without destroying the referential integrity of related tables. Youll also learn about foreign key constraints, which control what happens if you try to delete records that still have a foreign key relationship in another table. The widely used MyISAM storage engine doesn t currently support foreign key constraints, but they are supported by InnoDB, the default storage engine in MySQL 5.5 and later. This chapter describes how to work with both storage engines. In particular, youll learn about the following: • Inserting, updating, and deleting records in related tables • Finding the primary key of a record immediately after it has been created • Converting a tables storage engine • Establishing foreign key constraints between InnoDB tables Maintaining referential integrity With single tables, it doesnt matter how often you update a record or how many records you delete, the impact on other records is zero. Once you store the primary key of a record as a foreign key in a different CHAPTER 16 432 table, you create a dependency that needs to be managed. For example, if you delete the second article from the blog table (“Trainee Geishas Go Shopping”), Figure 16-1 shows it linked to the Kyoto and People categories through the article2cat cross-reference table. Figure 16-1. You need to manage foreign key relations to avoid orphaned records. If you fail to delete the entries for article_id 2 in the cross-reference table, a query that looks for all articles in the Kyoto or People categories tries to match a nonexistent record in the blog table. Similarly, if you decide to delete one of the categories without also deleting matching records in the cross-reference table, a query that looks for the categories associated with an article tries to match a nonexistent category. Before long, your database is littered with orphaned records. Fortunately, maintaining referential integrity is not difficult. SQL does it through the establishment of rules known as foreign key constraints that tell the database what to do when you update or delete a record that has dependent records in another table. The bad news is that the default storage engine prior to MySQL 5.5, MyISAM, doesnt support foreign key constraints. You need to use InnoDB instead. Choosing between MyISAM and InnoDB isnt simply a matter of one being “better” than the other. MyISAMs strengths lie in smaller file sizes and speed. It also supports full text indexing and searching (see http://dev.mysql.com/doc/refman/5.1/en/fulltext-search.html ), which InnoDB does not. Support for foreign key constraints in MyISAM tables is planned for a later version of MySQL. InnoDB has been an integral part of MySQL since version 4.0 was released in 2003. Unfortunately, many hosting companies disable InnoDB or offer it only on premium hosting plans. If your hosting company supports InnoDB, you can easily convert MyISAM tables and use foreign key constraints. If you dont have access to InnoDB, you need to maintain referential integrity by building the necessary rules into your PHP scripts. This chapter shows both approaches. PHP Solution 16-1: Checking whether InnoDB is supported This PHP solution explains how to check whether your remote server supports the InnoDB storage engine. 1. If your hosting company provides phpMyAdmin to administer your database(s), launch phpMyAdmin on your remote server, and click the Engines tab at the top of the screen, if its available. This displays a list of storage engines similar to Figure 16-2. Download from Wow! eBook <www.wowebook.com> MANAGING MULTIPLE DATABASE TABLES 433 Figure 16-2. Checking storage engine support through phpMyAdmin 2. The list displays all storage engines, including those that are not supported. Unsupported or disabled storage engines are grayed out. If youre not sure of the status of InnoDB, click its name in the list. If InnoDB is not supported, youll see a message telling you so. If, on the other hand, you see a list of variables similar to Figure 16-3, youre in luck—InnoDB is supported. Figure 16-3. Confirmation that InnoDB is supported 3. If theres no Engines tab in phpMyAdmin, select any table in your database, and click the Operations tab at the top right of the screen. In the Table options section, click the down arrow to the right of the Storage Engine field to display the available options (see Figure 16-4). If InnoDB is listed, its supported. CHAPTER 16 434 Figure 16-4. The available storage engines are listed in the Table options. 4. If neither of the preceding methods gives you the answer, open storage_engines.php in the ch16 folder. Edit the first three lines to insert the hostname, username, and password for the database on your remote server. 5. Upload storage_engines.php to your website, and load the page into a browser. You should see a list of storage engines and level of support, as shown in Figure 16-5. In some cases, NO will be replaced by DISABLED. Figure 16-5. The SQL query in storage_engines.php reports which ones are supported. As Figure 16-5 shows, a typical installation of MySQL supports several storage engines. What may come as a surprise is that you can use different storage engines within the same database. In fact, its recommended that you do. Even if your remote server supports InnoDB, its usually more efficient to use MyISAM for tables that dont have a foreign key relationship. Use InnoDB for tables that have foreign key relationships. You should also use InnoDB if you need support for transactions. MANAGING MULTIPLE DATABASE TABLES 435 A transaction is a series of related SQL queries. If one part of the series fails, the transaction is terminated, and the database rolls back to its original state before the transaction. Financial databases make extensive use of transactions, which are beyond the scope of this book. Ill explain how to convert tables to InnoDB and set up foreign key constraints later in this chapter. Before that, lets take a look at how to establish and use foreign key relationships regardless of the storage engine being used. Inserting records into multiple tables An INSERT query can insert data into only one table. Consequently, when working with multiple tables, you need to plan your insert scripts carefully to ensure that all the information is stored and that the correct foreign key relationships are established. PHP Solution 15-2 in the previous chapter showed how to add the correct foreign key for an image that is already registered in the database. However, when inserting a new blog entry, you need to be able to select an existing image, upload a new image, or choose to have no image at all. This means that your processing script needs to check whether an image has been selected or uploaded and execute the relevant commands accordingly. In addition, tagging a blog entry with zero or more categories increases the number of decisions the script needs to make. Figure 16-6 shows the decision chain. Figure 16-6. The decision chain for inserting a new blog article with an image and categories CHAPTER 16 436 When the page first loads, the form hasnt been submitted, so the page simply displays the insert form. Both the existing images and categories are listed in the insert form by querying the database in the same way as for the images in the update form in PHP Solution 15-2. After the form has been submitted, the processing script goes through the following steps: 1. If an image has been uploaded, the upload is processed, the details of the image are stored in the images table, and the script gets the primary key of the new record. 2. If no image has been uploaded, but an existing image has been selected, the script gets its foreign key from the value submitted through the $_POST array. 3. In either case, the new blog article is inserted in the blog table along with the images primary key as a foreign key. However, if an image has neither been uploaded nor selected from the existing ones, the article is inserted in the blog table without a foreign key. 4. Finally, the script checks if any categories have been selected. If they have, the script gets the new articles primary key and combines it with the primary keys of the selected categories in the article2cat table. If theres a problem at any stage, the script needs to abandon the rest of the process and redisplay the users input. The script is quite long, so Ill break it up into several sections. The first stage is to create the article2cat cross-reference table. Creating a cross-reference table When dealing with many-to-many relationships in a database, you need to build a cross-reference table like the one in Figure 16-1. Whats unusual about a cross-reference table is that it consists of just two columns, which are jointly declared as the tables primary key (known as a composite primary key). If you look at Figure 16-7, youll see that the article_id and cat_id columns both contain the same number several times—something thats unacceptable in a primary key, which must be unique. However, in a composite primary key, its the combination of both values that is unique. The first two combinations, 1,3 and 2,1, are not repeated anywhere else in the table, nor are any of the others. Figure 16-7. In a cross-reference table, both columns together form a composite primary key. MANAGING MULTIPLE DATABASE TABLES 437 Setting up the categories and cross-reference tables In the ch16 folder, youll find categories.sql, which contains the SQL to create the categories table and the cross-reference table, article2cat, together with some sample data. The settings used to create the tables are listed in Tables 16-1 and 16-2. Both database tables have just two columns (fields). Table 16-1. Settings for the categories table Field Type Length/Values Attributes Null Index AUTO_INCREMENT cat_id INT UNSIGNED Deselected PRIMARY Selected category VARCHAR 20 Deselected Table 16-2. Settings for the article2cat cross-reference table Field Type Length/Values Attributes Null Index AUTO_INCREMENT article_id INT UNSIGNED Deselected PRIMARY cat_id INT UNSIGNED Deselected PRIMARY The important thing about the definition for a cross-reference table is that both columns are set as primary key, and that auto_increment is not selected for either column. To ensure that the table recognizes them as a composite primary key, you must declare both columns as primary key at the same time. If, by mistake, you declare only one as the primary key, MySQL prevents you from adding the second one later. You must delete the primary key index from the single column and then reapply it to both. Its the combination of the two columns that are treated as the primary key. Getting the filename of an uploaded image The script makes use of the Ps2_Upload class from Chapter 6, but the class needs tweaking slightly because the filenames of uploaded files are incorporated into the $_messages property. PHP Solution 16-2: Improving the Ps2_Upload class This PHP solution adapts the Ps2_Upload class from Chapter 6 by creating a new protected property to store the names of successfully uploaded files, together with a public method to retrieve the array. 1. Open Upload.php in the classes/Ps2 folder. Alternatively, copy Upload_05.php from the ch06 folder, and save it in classes/Ps2 as Upload.php. 2. Add the following line to the list of properties at the top of the file: protected $_filenames = array(); This initializes a protected property called $_filenames as an empty array. CHAPTER 16 438 3. Amend the processFile() method to add the amended filename to the $_filenames property if the file is successfully uploaded. The new code is highlighted in bold. protected function processFile($filename, $error, $size, $type, $tmp_name, $overwrite) { $OK = $this->checkError($filename, $error); if ($OK) { $sizeOK = $this->checkSize($filename, $size); $typeOK = $this->checkType($filename, $type); if ($sizeOK && $typeOK) { $name = $this->checkName($filename, $overwrite); $success = move_uploaded_file($tmp_name, $this->_destination . $name); if ($success) { // add the amended filename to the array of filenames $this->_filenames[] = $name; $message = "$filename uploaded successfully"; if ($this->_renamed) { $message .= " and renamed $name"; } $this->_messages[] = $message; } else { $this->_messages[] = "Could not upload $filename"; } } } } The name gets its value from the checkName() method, which replaces spaces with underscores and renames files that are the same as an existing file. Its added to the $_filenames array only if the file is successfully moved to the destination folder. 4. Add a public method to return the values stored in the $_filenames property. The code looks like this: public function getFilenames() { return $this->_filenames; } It doesnt matter where you put this code in the class definition, but its common practice to keep all public methods together. 5. Save Upload.php. If you need to check your code, compare it with Upload_06.php in the ch16 folder. Adapting the insert form to deal with multiple tables The insert form for blog articles that you created in Chapter 13 already contains the code needed to insert most of the details in the blog table. Rather than start again from scratch, it makes sense to adapt the existing page. As it stands, the page contains only a text input field for the title and a text area for the article. MANAGING MULTIPLE DATABASE TABLES 439 You need to add a multiple-choice <select> list for categories, and a drop-down <select> menu for existing images. To prevent a user from selecting an existing image at the same time as uploading a new one, a check box and JavaScript control the display of the relevant input fields. Selecting the check box disables the drop- down menu for existing images and displays the input fields for a new image and caption. Deselecting the check box hides and disables the file and caption fields, and reenables the drop-down menu. If JavaScript is disabled, the options for uploading a new image and captions are hidden. PHP Solution 16-3: Adding the category and image input fields This PHP solution begins the process of adapting the blog entry insert form from Chapter 13 by adding the input fields for categories and images. 1. In the admin folder, open the version of blog_insert_mysqli.php that you created in Chapter 13. Alternatively, copy blog_insert_mysqli.php from the ch13 folder to the admin folder. 2. The <select> elements for the categories and existing images need to query the database when the page first loads, so you need to move the connection script and database connection outside the conditional statement that checks if the form has been submitted. Locate the lines highlighted in bold: if (isset($_POST['insert'])) { require_once(' /includes/connection.inc.php'); // initialize flag $OK = false; // create database connection $conn = dbConnect('write'); Move them outside the conditional statement like this: require_once(' /includes/connection.inc.php'); // create database connection $conn = dbConnect('write'); if (isset($_POST['insert'])) { // initialize flag $OK = false; 3. The form in the body of the page needs to be capable of uploading a file, so you need to add the enctype attribute to the opening <form> tag like this: <form id="form1" method="post" action="" enctype="multipart/form-data"> 4. If an error occurs when trying to upload a file—for example, if its too big or not an image—the insert operation will be halted. Amend the existing text input field and text area to redisplay the values using the same technique as in Chapter 5. The text input field looks like this: <input name="title" type="text" class="widebox" id="title" value="<?php if (isset($error)) { echo htmlentities($_POST['title'], ENT_COMPAT, 'utf-8'); } ?>"> The text area looks like this: CHAPTER 16 440 <textarea name="article" cols="60" rows="8" class="widebox" id="article"><?php if (isset($error)) { echo htmlentities($_POST['article'], ENT_COMPAT, 'utf-8'); } ?></textarea> Make sure theres no gap between the opening and closing PHP tags and the HTML. Otherwise, youll add unwanted whitespace inside the text input field and text area. 5. The new form elements go between the text area and the submit button. First, add the code for the multiple-choice <select> list for categories. The code looks like this: <p> <label for="category">Categories:</label> <select name="category[]" size="5" multiple id="category"> <?php // get categories $getCats = 'SELECT cat_id, category FROM categories ORDER BY category'; $categories = $conn->query($getCats); while ($row = $categories->fetch_assoc()) { ?> <option value="<?php echo $row['cat_id']; ?>" <?php if (isset($_POST['category']) && in_array($row['cat_id'], $_POST['category'])) { echo 'selected'; } ?>><?php echo $row['category']; ?></option> <?php } ?> </select> </p> To allow the selection of multiple values, the multiple attribute has been added to <select> tag, and the size attribute set to 5. The values need to be submitted as an array, so a pair of square brackets has been appended to the name attribute. The SQL queries the categories table, and a while loop populates the <option> tags with the primary keys and category names. The conditional statement in the while loop adds selected to the <option> tag to redisplay selected values if the insert operation fails. 6. Save blog_insert_mysqli.php, and load the page into a browser. The form should now look like Figure 16-8. . into your PHP scripts. This chapter shows both approaches. PHP Solution 1 6-1 : Checking whether InnoDB is supported This PHP solution explains how to check whether your remote server supports the. with a public method to retrieve the array. 1. Open Upload .php in the classes/Ps2 folder. Alternatively, copy Upload_05 .php from the ch06 folder, and save it in classes/Ps2 as Upload .php. 2 $categories->fetch_assoc()) { ?> <option value="< ?php echo $row['cat_id']; ?>" < ?php if (isset($_POST['category']) && in_array($row['cat_id'],