Better solution for sequential statement executions with PDO and PHP - php

I have 3 tables which I have to add a record to them after registration of a new user:
List of Tables:
I. users
... ... ... id (auto_increment, primary)
... ... ... email (email address of new user)
II. blogs
... ... ... id (auto_increment, primary)
... ... ... owner_id (= 'id' in 'users')
III. events
... ... ... id (auto_increment, primary)
... ... ... owner_id (= 'id' in 'users')
... ... ... blog_id (= 'id' in 'blogs')
In this situation I found 2 solutions for adding sequential records:
Solution 1: Using lastInsertId
<?php
try {
// Step 1: add a record to 'users' table and get lastInsertId
$query = $conn->prepare("INSERT INTO users (email) VALUES (:email)");
$query->bindParam(':email', $email);
$query->execute();
$user_id = $conn->lastInsertId();
// Step 2: add a record to 'blogs' table and get lastInsertId
$query = $conn->prepare("INSERT INTO blogs (owner_id) VALUES (:owner)");
$query->bindParam(':owner', $user_id);
$query->execute();
$blog_id = $conn->lastInsertId();
// Step 3: add a record to 'events' table
$query = $conn->prepare("INSERT INTO events (owner_id, blog_id) VALUES (:owner, :blog)");
$query->bindParam(':owner', $user_id);
$query->bindParam(':blog', $blog_id);
$query->execute();
} catch (PDOException $e) {
echo $e->getMessage();
}
?>
Solution 2: Using single execute()
<?php
try {
// Step 1
$query = $conn->prepare("INSERT INTO users (email) VALUES (:email);" .
"INSERT INTO blogs (owner_id) VALUES ((SELECT id FROM users WHERE email = :email));" .
"INSERT INTO events (owner_id, blog_id) VALUES ((SELECT id FROM users WHERE email = :email), (SELECT id FROM blogs WHERE owner_id = (SELECT id FROM users WHERE email = :email)));");
$query->bindParam(':email', $email);
$query->execute();
} catch (PDOException $e) {
echo $e->getMessage();
}
?>
Which solution should I choose for a better performance and security? Is there a better solution for my purpose?
Note: the connection created using PDO:
<?php
$options = array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
);
try {
$conn = new PDO("mysql:host=" . App::DB_HOST . ";dbname=" . App::DB_NAME . ";charset=utf8", App::DB_USERNAME, App::DB_PASSWORD, $options);
} catch (PDOException $e) {
echo $e->getMessage();
}
?>

I would use transactions as a modification of 1st option.
$conn->beginTransaction();
try {
// Step 1: add a record to 'users' table and get lastInsertId
$query = $conn->prepare("INSERT INTO users (email) VALUES (:email)");
$query->bindParam(':email', $email);
$query->execute();
$user_id = $conn->lastInsertId();
// Step 2: add a record to 'blogs' table and get lastInsertId
$query = $conn->prepare("INSERT INTO blogs (owner_id) VALUES (:owner)");
$query->bindParam(':owner', $user_id);
$query->execute();
$blog_id = $conn->lastInsertId();
// Step 3: add a record to 'events' table
$query = $conn->prepare("INSERT INTO events (owner_id, blog_id) VALUES (:owner, :blog)");
$query->bindParam(':owner', $user_id);
$query->bindParam(':blog', $blog_id);
$query->execute();
$conn->commit();
}
catch (PDOException $e) {
// roll back transaction
$conn->rollback();
echo $e->getMessage();
die();
}

If you do some benchmarks you will see most time will be lost making the request.
From personal benchmarks on simple queries like this the execution time is very low.
The only thing that realy took time is the initialisation/prepare function.
There for making 3 requests will be slower then creating one large one.
EDIT:
Option 1 is the correct one because you do need to use id's, never link using a string or somethign else allways use id's.
Appart from that 1 (prepared) big query is better then 3x a prepare.

Edit. I misread the question at first, thought you are using exec(), not execute().
So, in fact you can combine both, as lastInsertId is just a PHP wrapper for Mysql's LAST_INSERT_ID()
But, as you need two ids, it will require additional mess with setting a variable. So, I doubt second option would worth, although feasible.
Just note that second would work only if PDO emulation mode is turned off
And surely there is no such question like "performance". Both will go perfectly.

Related

How to handle multiple insert requests if one fails?

My registration form inserts a row into two tables. How can I roll back all transactions if either doesn't complete?
Here is my snippet so far:
try {
// insert row for account
$stmt = $dbh->prepare("INSERT INTO accounts (account_num) VALUES (:account)");
$params = [
":account_num" => $account_num
]
$stmt=>execute($params);
// insert row for user
$stmt = $dbh->prepare("INSERT INTO users (email, account_num) VALUES (:email, :account_num)");
$params = [
":email" => $email,
":account_num" -> $account_num;
]
$stmt->execute($params);
} catch (PDOExeception $e) {
echo "error: could not create your account and profile";
}
You can do something inside a try catch like this-
$this->pdo->beginTransaction(); //prepare database for rollback changes if needed
try{
$stmt1 = $this->pdo->prepare(...); //prepare your first statement for execution
$stmt1->execute(...); //execute first statement
$stmt2 = $this->pdo->prepare(...); //prepare your second statement for execution
$stmt2->execute(...); //execute second statement
$this->pdo->commit(); //confirms that all statements are executed and no errors occured
} catch (\PDOException $e) {
$this->pdo->rollBack(); //if there is any error, the exception handler will rollback the operation
}
Please be noted that if you have an auto incremented primary key, then you may miss two auto-incremented values here because this rollback operation first creates/ inserts the data. If any error occurred then simply deletes them. So the auto incremented primary keys might be missing.

Insert rows for an m:m relationship (pupil-evening_activity) consistently

I have three tables in my database (MySQL/InnoDB): pupil, evening_activity and pupil_evening_activity. As you can guess there is an m:m relationship between pupil and evening_activity.
I have a HTML form to insert new pupils and associate to them evening activities. After submitting the form, I would like to insert pupils and their associated activities in the database. So I have written this code:
$db = new PDO('mysql:host=localhost;dbname=school;charset=utf8','root', '****');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
$sql = "INSERT INTO pupil
(name, surname)
VALUES
('test_name', 'test_surname')";
$st = $db->prepare($sql);
$st->execute();
foreach ($eveningActivityIds as $eveningActivityId) {
$sql = "INSERT INTO pupil_evening_activity
(pupil_id, evening_activity_id)
VALUES
(?, ?)
";
$st = $db->prepare($sql);
$st->execute(array($db->lastInsertId(), $eveningActivityId));
}
} catch (PDOException $e) {
echo $e->getMessage();
return false;
}
I have written that code expecting PDO::lastInsertId() always returns the last inserted pupil's id, but I've found out that what it really returns is the last inserted id on the whole database. So if another row is inserted in a hypothetical teacher table just before calling to $db->lastInsertId() on the code above, $db->lastInsertId() would return the id of the inserted teacher instead of the id of the last pupil.
So how I can get 100% safety the last inserted id of pupil table after submitting the form?
I have thought about using a transaction and MAX() function to get last id inserted in the pupil table, but I'm not sure about it.
It's a mistake to have $db->lastInsertId() within the foreach loop, as it may return unpredictable values. Please consider the code below:
try {
$sql = "INSERT INTO pupil
(name, surname)
VALUES
('test_name', 'test_surname')";
$st = $db->prepare($sql);
$st->execute();
// save pupil id into variable here:
$pupil_id = $db->lastInsertId();
// also there's no need to prepare the statement on each iteration:
$sql = "INSERT INTO pupil_evening_activity
(pupil_id, evening_activity_id)
VALUES
(?, ?)";
$st = $db->prepare($sql);
foreach ($eveningActivityIds as $eveningActivityId) {
$st->execute(array($pupil_id, $eveningActivityId));
}
} catch (PDOException $e) {
echo $e->getMessage();
return false;
}
Optimization: you can insert all acivities in one statement. It saves you few requests
try {
$sql = "INSERT INTO pupil
(name, surname)
VALUES
('test_name', 'test_surname')";
$st = $db->prepare($sql);
$st->execute();
// save pupil id into variable here:
$pupil_id = $db->lastInsertId();
$sql = 'INSERT INTO pupil_evening_activity
(pupil_id, evening_activity_id)
VALUES ';
$values = array();
foreach ($eveningActivityIds as $eveningActivityId) {
$sql .= '(?, ?),';
array_push($values, $pupil_id, $eveningActivityId);
}
$st = $db->prepare(chop($sql, ','));
$st->execute($values);
} catch (PDOException $e) {
echo $e->getMessage();
return false;
}

PHP MYSQL SELECT Returns NULL

I executing the following code which creates a company_id as a UUID_SHORT in a temporary table.
This company_id will then be used to insert records in multiple tables with the UUID as the primary key. My issue is when I try retrieve the company_id that is $company_id in my code it is null. However if I json_encode ($tempResult) the company_id value is there. What am I doing wrong?
Any help is much appreciated, thank you!
try {
$conn = new PDO("mysql:host=localhost;dbname=$dbname", $db->id, $db->pass); //connect to db
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //error modes
$temp = $conn->prepare('CREATE TEMPORARY TABLE tempId (user_id VARCHAR(17) PRIMARY KEY, company_id VARCHAR(17))');
$temp->execute();
$temp = $conn->prepare('INSERT INTO tempId(user_id, company_id) VALUES(:user_id, UUID_SHORT())');
$temp->bindParam(':user_id', $_SESSION['username'], PDO::PARAM_INT);
$temp->execute();
$temp = $conn->prepare('SELECT company_id FROM tempId WHERE user_id = :user_id ');
$temp->bindParam(':user_id', $_SESSION['username'], PDO::PARAM_INT);
$temp->execute();
$tempResult= $temp->fetchAll(PDO::FETCH_ASSOC);
$company_id = $tempResult->company_id;
// $result[1] =$_SESSION('username');
} catch(PDOException $e) {
$result = $e->getMessage();
}
print json_encode($company_id);
Here:
$tempResult= $temp->fetchAll(PDO::FETCH_ASSOC);
If the fetchAll is successful, then $tempResult will be an array. For debugging, we can verify this using the convenient var_dump, e.g.
var_dump($tempResult);
If $tempResult is an array, I'm wondering about this expression:
$tempResult->company_id
What does that return? What do you expect that to return? Why?
EDIT: I know better than to answer a question with a question, or three questions.
However, I can't (in good conscience) bring myself to giving an "answer" to the problem with OP code...
at least not without (figuratively) scratching my head wondering about the actual SQL being used in the code.
What is the purpose of the TEMPORARY TABLE? Why is there an INSERT to it? Why is the UNSIGNED BIGINT datatype (returned by UUID_SHORT() function) being cast to a VARCHAR(17)? Is there some reason we want to lop off 1 or 2 digits when the function returns 18 or 19 decimal digits?
If the intent of this block of code is to return a value from MySQL UUID_SHORT() function, I'm not understanding why we need more than one statement. Obviously, there's something I'm missing, why this wouldn't suffice:
try {
$conn = new PDO("mysql:host=localhost;dbname=$dbname", $db->id, $db->pass);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sth = $conn->prepare('SELECT UUID_SHORT() AS company_id');
$sth->execute();
$company_id = $sth->fetchColumn();
} catch(PDOException $e) {
//var_dump($e->getMessage);
} finally {
if(isset($sth)){ $sth->close(); }
if(isset($conn)){ $conn->close(); }
}
(An application wouldn't churn database connections like this; there would either be a connection pool, or the connection would be passed in to this routine.)
Not sure, but as soon as fetchAll returns array, your code:
$company_id = $tempResult->company_id;
is invalid, you should:
$company_id = $tempResult[0]['company_id'];
or
$tempResult= $temp->fetch(PDO::FETCH_ASSOC);
$company_id = $tempResult['company_id'];

looping a php script to insert data for multiple users

I have two tables: phpbb_sn_fms_groups and phpbb_fms_user_groups, which I would like to INSERT into using the two queries below, however the first problem is I can't possibly manually run the two queries ~1000 times for each user_id (56 through 1060). The second problem is I need to INSERT the auto incremented fms_gid from the first query (table: phpbb_sn_fms_groups) into fms_id from the second query (table: phpbb_sn_fms_users_group) when the php script INSERTs each user_id.
// user_id: 56 through 1060
// fms_gid in the table phpbb_sn_fms_users_group needs the unique fms_gid for each row from the table phpbb_sn_fms_groups because it doesn't autoincrement.
$result = mysql_query("INSERT INTO phpbb_sn_fms_groups (fms_gid, user_id, fms_name, fms_clean, fms_collapse) VALUES ('autoincrementednumberthatdoesntneedtobeinserted', '56', 'Staff', 'staff', '0')")
or die(mysql_error());
$result = mysql_query("INSERT INTO phpbb_sn_fms_users_group (fms_gid, user_id, owner_id) VALUES ('inserttheautoincrementednumberfromfms_gidinphpbb_sn_fms_groups', '2', '56')")
or die(mysql_error());
Any help would be appreciated. Thanks!
This is a great time for PDO.
See http://php.net/manual/en/book.pdo.php for connection info but something like
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
$pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
// First select the users you need query.
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE id BETWEEN 56 AND 1060");
$stmt->execute();
$result = $stmt->fetchAll();
OR if literally 56 through 1060 you can use a for loop.
Then loop through those results to execute
foreach($result as $row) {
$stmt1 = $pdo->prepare("INSERT INTO phpbb_sn_fms_groups (fms_gid, user_id, fms_name, fms_clean, fms_collapse) VALUES ('autoincrementednumberthatdoesntneedtobeinserted', :user_id , 'Staff', 'staff', '0')");
$stmt1->bindParam(":user_id", $row['id']);
$stmt1->execute();
//get last inserted id.
$inserted_id = $pdo->lastInsertId();
$stmt2 = $pdo->prepare("INSERT INTO phpbb_sn_fms_users_group (fms_gid, user_id, owner_id) VALUES (:last_id, '2', :user_id)");
$stmt2->bindParam(":last_id", $inserted_id);
$stmt2->bindParam(":user_id", $row['id']);
$stmt2->execute();
}
Hope this gets you started. Can also be done in mysqli. Don't want to jump into that debate. I just like naming the parameters for binding.

retrieve the id PDO if a value already exists

I have two tables which are linked with many many relationship,Normally I work with Mysqli for querying,but now for some reasons I have to use PDO,So my problem is 1st to check if a value doesn't exists, to add it in a table tag,and if not to take the id of the value and add the related data in the join data.
tag-tagmap-post a tag can belong to many posts,and a post can have many tags I try this but when a tag name already exists I can't retrieve the id of the tag.
$sql = "
INSERT INTO tag (name)
SELECT * FROM (SELECT :name) AS tmp
WHERE NOT EXISTS (
SELECT name FROM tag WHERE name =:name
) LIMIT 1";
try {
$db = getConnection();
$stmt = $db->prepare($sql);
$stmt->bindParam(":name", $post->name);
$stmt->execute();
$test=$post->id = $db->lastInsertId();
$db = null;
//echo json_encode($post);
} catch(PDOException $e) {
//error_log($e->getMessage(), 3, '/tmp/php.log');
echo '{"error":{"text":'. $e->getMessage() .'}}';
}
$sql2="INSERT INTO tagmap (tag_id,post_id,user_id) VALUES(:id2,:post_id,:id)";//
try {
$db = getConnection();
$stmt = $db->prepare($sql2);
$stmt->bindParam("id2", $test);//tag id
$stmt->bindParam("post_id", $post->post_id);//post_id
$stmt->bindParam("id", $id);
$stmt->execute();
//$post2->id = $db->lastInsertId();
$db = null;
echo json_encode(array("result"=>$test));
} catch(PDOException $e) {
//error_log($e->getMessage(), 3, '/tmp/php.log');
echo '{"error":{"text":'. $e->getMessage() .'}}';
}
Thank you for your help!!!
As documented under PDO::prepare():
You cannot use a named parameter marker of the same name more than once in a prepared statement, unless emulation mode is on.
That said, the insertion query itself is over-complicated (and contains syntax errors: there is no LIMIT clause in the INSERT syntax). You could instead:
define a UNIQUE key over (name) in the tag table:
ALTER TABLE tag ADD UNIQUE (name);
use INSERT ... ON DUPLICATE KEY UPDATE:
INSERT INTO tag (name) VALUES (?)
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id);
This will ensure that $db->lastInsertId() returns the ID of the relevant record whether it has been newly inserted or it already existed.

Categories