Preserving referential integrity on deletion In PHP Solution 16-5, there was no need to worry about referential integrity when you deleted records in the cross-reference table because t
Trang 1} else {
$sql = 'UPDATE blog SET image_id = NULL, title = ?, article = ?
WHERE article_id = ?';
$stmt->prepare($sql);
$stmt->bind_param('ssi', $_POST['title'], $_POST['article'],
$_POST['article_id']);
}
$stmt->execute();
$done = $stmt->affected_rows;
}
The last two lines of this code block execute the prepared statement that updates the record in the blog table, and then assign the number of affected rows to $done If you update a record, the affected_rows property is 1, which is treated as true However, if you dont make any changes to the record, affected_rows is 0, which is treated as false If you update only the categories associated with a record, without changing the record itself, $done is interpreted as false, and you wont be returned to blog_list_mysqli.php
Delete the following line:
$done = $stmt->affected_rows;
6 Assign the return value of $stmt->execute() to $done like this:
$done = $stmt->execute();
The execute() method returns true if the prepared statement is executed successfully, even if
it doesnt result in any changes to the record
7 Immediately after the line you have just edited, insert the code to delete existing values in the
cross reference table and to insert the newly selected values like this:
$done = $stmt->execute();
// delete existing values in the cross-reference table
$sql = 'DELETE FROM article2cat WHERE article_id = ?';
$stmt->prepare($sql);
$stmt->bind_param('i', $_POST['article_id']);
$stmt->execute();
// insert the new values in articles2cat
if (isset($_POST['category']) && is_numeric($_POST['article_id'])) {
$article_id = (int) $_POST['article_id'];
foreach ($_POST['category'] as $cat_id) {
$values[] = "($article_id, " (int) $cat_id ')';
}
if ($values) {
$sql = 'INSERT INTO article2cat (article_id, cat_id)
Trang 2452
}
}
}
}
This code needs little explanation The DELETE query removes all entries in the cross-reference table that match article_id The remaining code inserts the values selected in the update form Its identical to the code in step 4 of PHP Solution 16-4 The key thing to note is that it uses
an INSERT query, not UPDATE The original values have been deleted, so youre adding them anew
8. Save blog_update_mysqli.php, and test it by updating existing records in the blog table You can check your code, if necessary, against blog_update_mysqli_06.php in the ch16 folder The PDO version is in blog_update_pdo_06.php
Preserving referential integrity on deletion
In PHP Solution 16-5, there was no need to worry about referential integrity when you deleted records in the cross-reference table because the values stored in each record are foreign keys Each record simply refers
to the primary keys stored in the blog and categories tables Referring to Figure 16-1 at the beginning of this chapter, deleting from the cross-reference table the record that combines article_id 2 with cat_id 1 simply breaks the link between the article titled “Trainee Geishas Go Shopping” and the Kyoto category Neither the article nor the category is affected They both remain in their respective tables
The situation is very different if you decide to delete either the article or the category If you delete the
“Trainee Geishas Go Shopping” article from the blog table, all references to article_id 2 must also be deleted from the cross-reference table Similarly, if you delete the Kyoto category, all references to cat_id
1 must be removed from the cross-reference table Alternatively, you must halt the deletion if an items primary key is stored elsewhere as a foreign key
The best way to do this is through the establishment of foreign key restraints To do so, you need to convert the storage engine of related tables to InnoDB
PHP Solution 16-6: Converting tables to the InnoDB storage engine
This PHP solution shows how to use phpMyAdmin to convert the storage engine of database tables from MyISAM to InnoDB If you plan to upload the tables to your remote server, it must also support InnoDB (see PHP Solution 16-1)
1. Select the phpsols database in phpMyAdmin, and then select the article2cat table
2 Click the Operations tab at the top right of the screen
3 In the Table options section, select InnoDB from the Storage Engine drop-down menu, as
shown in Figure 16-10
Trang 3Figure 16-10 Changing a tables storage engine is very easy in phpMyAdmin
4 Click Go Changing the storage engine is as simple as that!
5 All tables related to each other through foreign key relationships need to use InnoDB Repeat
steps 1–4 with the blog, categories, and images tables
PHP Solution 16-7: Setting up foreign key constraints
This PHP solution describes how to set up foreign key constraints between the article2cat, blog, and category tables in phpMyAdmin The foreign key constraints must always be defined in the child table In this this case, the child table is article2cat, because it stores the article_id and cat_id primary keys from the other tables as foreign keys
1 Select the article2cat table in phpMyAdmin, and click the Structure tab
2 Click Relation view (circled in Figure 16-11) at the bottom of the structure table
Figure 16-11 Foreign key constraints are defined in phpMyAdmins Relation view
3 Foreign key constraints can be set up only on columns that are indexed The article_id and
cat_id columns in article2cat are the tables composite primary key, so theyre both listed in
the screen that opens If your version of phpMyAdmin has an option labeled Internal relations, you can ignore it The section youre interested in is labeled FOREIGN KEY (INNODB)
Trang 4454
Figure 16-12 This will be used to establish a formal foreign key relationship between article_id
in the article2cat table and article_id in the blog table
Figure 16-12 Selecting the primary key in the parent table
The ON DELETE drop-down menus have the following options:
CASCADE: When you delete a record in the parent table, all dependent records are
deleted in the child table For example, if you delete the record with the primary key article_id 2 in the blog table, all records with article_id 2 in the article2cat table are automatically deleted
SET NULL: When you delete a record in the parent table, all dependent records in
the child table have the foreign key set to NULL The foreign key column must accept NULL values
NO ACTION: On some database systems, this allows foreign constraint checks to
be delayed MySQL performs checks immediately, so this has the same effect as
RESTRICT
RESTRICT: This prevents the deletion of a record in the parent table if dependent
records still exist in the child table
The same options are available for ON UPDATE With the exception of RESTRICT, they are of limited interest, because you should change the primary key of a record only in exceptional circumstances ON
UPDATE RESTRICT not only stops changes being made to the primary key in the parent table; it also
rejects any inserts or updates in the child table that would result in foreign key values that dont have a match in the parent table
In the case of a cross-reference table, CASCADE is the logical choice If you decide to delete a
record in the parent table, you want all cross-references to that record to be removed at the same time However, to demonstrate the default behavior of foreign key constraints, select
RESTRICT Leave ON UPDATE blank
4 In the cat_id row, select `phpsols`.`categories`.`cat_id` from the drop-down menu
immediate to the left of ON DELETE, and set ON DELETE to RESTRICT Click Save
If RESTRICT isnt available in the drop-down menu, leave the option blank
5 If you have not already done so, update at least one blog entry to associate it with a category
Trang 56 In phpMyAdmin, select the categories table, and click the Dele te icon next to a category that
you know to be associated with a blog entry, as shown in Figure 16-13
Figure 16-13 Click the large red X to delete a record in phpMyAdmin
7 Click OK when phpMyAdmin asks you to confirm the deletion If you have set up the foreign key
constraints correctly, youll see the error message shown in Figure 16-14
Figure 16-14 The foreign key constraint prevents the deletion if dependent records exist
8 Select the article2cat table, and click the Structure tab Then click Relation view In all
probability, the ON DELETE options will be blank This is not a cause for concern, RESTRICT is
the default for both ON DELETE and ON UPDATE Leaving these options blank has the same effect as selecting RESTRICT
9 Change both ON DELETE settings to CASCADE, and click Save
10 Select a record in the blog table that you know is associated with a category, and delete it
11 Check the article2cat table The records associated with the record you have just deleted
have also been deleted
To continue your exploration of foreign key constraints, select the blog table, and establish a foreign key relationship with image_id in the images table If you delete a record from the images table, the image_id
foreign key in the blog table needs to be set to NULL This is done automatically if you set the value of ON
DELETE to SET NULL Test it by deleting a record from the images table and checking the associated
record(s) in the blog table
If you need to convert an InnoDB table back to MyISAM, you must first remove any foreign key
constraints Select Relation view, set all fields to blank, and click Save After removing the foreign key
Trang 6456
Creating delete scripts with foreign key constraints
Choosing the values for ON DELETE in InnoDB tables depends on the nature of the relationship between tables In the case of the phpsols database, its not only safe but desirable to set the option to CASCADE
for both columns in the article2cat cross-reference table If a record is deleted in either the blog or categories parent table, the related values need to be deleted in the cross-reference table
The relationship between the images and blog tables is different If you delete a record from the images
table, you probably dont want to delete related articles in the blog table In that case, SET NULL is an
appropriate choice When a record is deleted from the images table, the foreign key in related articles is set
to NULL, but the articles remain intact
On the other hand, if images are vital to the understanding of articles, select RESTRICT Any attempt to
delete an image that still has related articles is automatically halted
These considerations affect how you handle deletion scripts When the foreign key constraint is set to
CASCADE or SET NULL, you dont need to do anything special You can use a simple DELETE query and
leave the rest to MySQL
However, if the foreign key constraint is set to RESTRICT, the DELETE query will fail To display an appropriate error message, use the errno property of a MySQLi statement object The MySQL error code for
a query that fails as a result of a foreign key constraint is 1451 After calling the execute() method, you can check for errors like this in MySQLi:
$stmt->execute();
if ($stmt->affected_rows > 0) {
$deleted = true;
} else {
$deleted = false;
if ($stmt->errno == 1451) {
$error = 'That record has dependent files in a child table, and cannot be
deleted.';
} else {
$error = 'There was a problem deleting the record.';
}
}
If you are using PDO, use the errorCode() method The code for a query that fails as a result of a foreign key constraint is HY000 After checking the number of affected rows with rowCount(), you can check the error code like this with PDO:
$deleted = $stmt->rowCount();
if (!$deleted) {
if ($stmt->errorCode() == 'HY000') {
$error = 'That record has dependent files in a child table, and cannot be
deleted.';
} else {
$error = 'There was a problem deleting the record.';
}
}
Trang 7The error codes in the PDO and MySQLi versions are different because PDO uses the codes defined by the ANSI SQL standard, whereas MySQLi uses MySQL-specific codes
Creating delete scripts without foreign key constraints
If you cant use InnoDB tables, you need to build the same logic into your own delete scripts To achieve the
same effect as ON DELETE CASCADE, run two consecutive DELETE queries like this:
$sql = 'DELETE FROM article2cat WHERE article_id = ?';
$stmt->prepare($sql);
$stmt->bind_param('i', $_POST['article_id']);
$stmt->execute();
$sql = 'DELETE FROM blog WHERE article_id = ?';
$stmt->prepare($sql);
$stmt->bind_param('i', $_POST['article_id']);
$stmt->execute();
To achieve the same effect as ON DELETE SET NULL, run an UPDATE query combined with a DELETE query
like this:
$sql = 'UPDATE blog SET image_id = NULL WHERE image_id = ?';
$stmt->prepare($sql);
$stmt->bind_param('i', $_POST['image_id']);
$stmt->execute();
$sql = 'DELETE FROM images WHERE image_id = ?';
$stmt->prepare($sql);
$stmt->bind_param('i', $_POST['image_id']);
$stmt->execute();
To achieve the same effect as ON DELETE RESTRICT, you need to run a SELECT query to find if there are
dependent records before continuing with the DELETE query like this:
$sql = 'SELECT image_id FROM blog WHERE image_id = ?';
$stmt->prepare($sql);
$stmt->bind_param('i', $_POST['image_id']);
$stmt->execute();
// if num_rows is not 0, there are dependent records
if ($stmt->num_rows) {
$error = 'That record has dependent files in a child table, and cannot be deleted.'; } else {
$sql = 'DELETE FROM images WHERE image_id = ?';
$stmt->prepare($sql);
$stmt->bind_param('i', $_POST['image_id']);
$stmt->execute();
}
Trang 8458
Chapter review
Once you have learned basic SQL and the PHP commands to communicate with a database, working with single tables is very easy Linking tables through foreign keys, however, can be quite challenging The power of a relational database comes from its sheer flexibility The problem is that this infinite flexibility means there is no single “right” way of doing things
Dont let this put you off, though Your instinct may be to stick with single tables, but down that route lies even greater complexity The key to making it easy to work with databases is to limit your ambitions in the early stages Build simple structures like the one in this chapter, experiment with them, and get to know how they work Add tables and foreign key links gradually People with a lot of experience working with databases say they frequently spend more than half the development time just thinking about the table structure After that, the coding is the easy bit!
In the final chapter, we move back to working with a single table—addressing the important subject of user authentication with a database and how to handle encrypted passwords
Trang 9Authenticating Users with a Database
Chapter 9 showed you the principles of user authentication and sessions to password protect parts of your website, but the login scripts all relied on usernames and passwords stored in text files Keeping user details in a database is both more secure and more efficient Instead of just storing a list of usernames and passwords, a database can store other details, such as first name, family name, email address, and
so on MySQL also gives you the option of using either one- or two-way encryption In the first section of this chapter, well examine the difference between the two Then youll create registration and login scripts for both types of encryption
What this chapter contains:
• Deciding how to encrypt passwords
• Using one-way encryption for user registration and login
• Using two-way encryption for user registration and login
• Decrypting passwords
Choosing an encryption method
The PHP solutions in Chapter 9 use the SHA-1 encryption algorithm It offers a high level of security,
particularly if used in conjunction with a salt (a random value thats added to make decryption harder)
SHA-1 is a one-way encryption method: once a password has been encrypted, theres no way of converting it back to plain text This is both an advantage and a disadvantage It offers the user greater security because passwords encrypted this way remain secret However, theres no way of reissuing a lost password, since not even the site administrator can decrypt it The only solution is to issue the user a temporary new password, and ask the user to reset it
The alternative is to use two-way encryption, which relies on a pair of functions: one to encrypt the
Trang 10460
the key in your registration and login scripts—either directly or through an include file—so if your scripts are ever exposed, your security is blown wide apart MySQL offers a number of two-way encryption functions, but AES_ENCRYPT() is considered the most secure It uses the Advanced Encryption Standard with a 128-bit key length (AES-128) approved by the U.S government for the protection of classified material up to the SECRET level (TOP SECRET material requires AES-192 or AES-256)
Both one-way and two-way encryption have advantages and disadvantages Many security experts recommend that passwords should be changed frequently So, forcing a user to change a forgotten password because it cant be decrypted could be regarded as a good security measure On the other hand, users are likely to be frustrated by the need to deal with a new password each time they forget the existing one Ill leave it to you to decide which approach is best suited to your circumstances, and Ill concentrate solely on the technical implementation
Using one-way encryption
In the interests of keeping things simple, Im going to use the same basic forms as in Chapter 9, so only the username, salt, and encrypted password are stored in the database
Creating a table to store users details
In phpMyAdmin, create a new table called users in the phpsols database The table needs four columns (fields) with the settings listed in Table 17-1
Table 17-1 Settings for the users table
Field Type Length/Values Attributes Null Index A_I
To ensure no one can register the same username as one thats already in use, the username column is given an UNIQUE index
In Chapter 9, the username doubled as the salt, but storing the details in a database means that you can choose something more unique and difficult to guess Although a Unix timestamp follows a predictable pattern, it changes every second So even if an attacker knows the day on which a user registered, there are 86,400 possible values for the salt, which would need to be combined with every attempt to guess the password So the salt column needs to store an integer (INT)
The pwd column, which is where the encrypted password is stored, needs to be 40 characters long because the SHA-1 algorithm always produces an alphanumeric string of that length Its a fixed length, so CHAR is used in preference to VARCHAR The CHAR data type is more efficient when dealing with fixed-length strings