I'm trying to get my head around PDO transactions to commit a fairly complex set of MySQL queries at once. When I run the transaction however, it will commit one query and not the other - if there is a single error in either query I expect it to roll back both queries and not make changes to either table.
So far:
My connect.php file:
class DbConnect {
private $conn;
function __construct() {
}
/**
* Establishing database connection
* #return database connection handler
*/
function connect() {
//Where HOST, USER, PASS etc are set
include_once "./dbconfig.php";
// Establish the connection
try {
$this->conn = new PDO("mysql:host=".HOST.";dbname=".DBNAME, USER, PASS);
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
return $this->conn;
} catch (PDOException $e) {
print "Error!: " . $e->getMessage() . "<br/>";
die();
}
}
}
My file where I'm trying to pass the simultaneous SQL queries
public function transaction ($userId, $amount){
//Creates the PDO connection EDIT: added my DB connection
$db = new DbConnect();
$this->conn = $db->connect();
$con = $this->conn;
$con->beginTransaction();
try{
$sql = "INSERT INTO transactions (id_user, amount) VALUES (?, ?)";
$trans = $con->prepare($sql);
$trans->execute([$userId, $amount]);
//If I purposely create an error here the query above still runs in the database e.g. remove the $amount variable
$this->updateBalance($userId, $amount);
$con->commit();
return true;
}
catch (PDOException $e) {
$con->rollBack();
throw $e;
}
}
private function updateBalance ($userId, $amount){
$time = time();
$sql = "UPDATE balance SET balance=balance + ? WHERE user_id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->execute([$amount, $userId]);
$row_count = $stmt->rowCount();
return $row_count > 0;
}
The above is just a small sample of a bigger more complex procedure otherwise I'd just put the balance query in the same place as the transaction, however I need to keep it in a separate function. Any ideas how I can get this into an "All or nothing" commit state?
Well first of all you are not checking the return status of the call to your second function
$this->updateBalance($userId, $amount);
So how will you know there is an error even if there is one?
If you make that called function Throw an exception rather than returning a status, it should be caught by the calling blocks catch() block causing the rollback() and not the commit()
Something like this
public function transaction ($userId, $amount){
//Creates the PDO connection
$con = $this->conn;
$con->beginTransaction();
try{
$sql = "INSERT INTO transactions (id_user, amount) VALUES (?, ?)";
$trans = $con->prepare($sql);
$trans->execute([$userId, $amount]);
// If I purposely create an error here the
// query above still runs in the database
// e.g. remove the $amount variable
$this->updateBalance($userId, $amount);
$con->commit();
return true;
}
catch (PDOException $e) {
$con->rollBack();
throw $e;
return false;
}
}
/*
* If this function throws an exception
* rather than returning a status, then it will
* stop execution of the try block and
* be caught by the calling blocks catch() block
*/
private function updateBalance ($userId, $amount){
$sql = "UPDATE balance SET balance=balance + ? WHERE user_id = ?";
$stmt = $this->conn->prepare($sql);
$res = $stmt->execute([$amount, $userId]);
if ( ! $res ) {
throw new Exception('It errored');
}
}
Alternatively you could make all PDO calls throw exceptions by setting PDO::ERRMODE_EXCEPTION just after you connect to your database.
$dbh = new PDO($dsn, $user, $password);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
However, this may be to major a change to PDO's error processing depending on how much code you have already produced.
Related
I'm struggling to make the jump form Procedural to Object Orientated style so if my code is untidy or flawed please be nice - here I'm passing a couple of posts via jQuery to a class to update a record when the user checks a checkbox:
Here is the database connection
class db {
private $host ;
private $username;
private $password;
private $dbname;
protected function conn()
{
$this->host = "localhost";
$this->username = "root";
$this->password = "";
$this->dbname = "mytest";
$db = new mysqli($this->host, $this->username, $this->password, $this->dbname);
if($db->connect_errno > 0){
die('Unable to connect to database [' . $db->connect_error . ']');
}
return $db;
}
}
Here is the update class
class updOrders extends db {
public $pid;
public $proc;
public function __construct()
{
$this->pid = isset($_POST['pid']) ? $_POST['pid'] : 0;
$this->proc = isset($_POST['proc']) ? $_POST['proc'] : 1;
// $stmt = $this->conn()->query("UPDATE tblorderhdr SET completed = ".$this->proc." WHERE orderid = ".$this->pid);
$stmt = $this->conn()->prepare("UPDATE tblorderhdr SET completed = ? WHERE orderid = ?");
$stmt->bind_param('ii', $this->proc, $this->pid);
$stmt->execute();
if($stmt->error)
{
$err = $stmt->error ;
} else {
$err = 'ok';
}
/* close statement */
$stmt->close();
echo json_encode($err);
}
}
$test = new updOrders;
When I comment out the prepare statement and run the query directly (commented out) it updates, when I try and run it as a prepare statement it returns an error "MySQL server has gone away".
I have looked at your code and I found this.
$stmt = $this->conn()->prepare("UPDATE tblorderhdr SET completed = ? WHERE orderid = ?");
I wrote nearly the same. But I saw you separated the connection from the prepare function.
$db = $this->conn();
$stmt = $db->prepare("UPDATE tblorderhdr SET completed = ? WHERE orderid = ?");
I don't know either why, but it works now.
It would appear that the problem lies within the connection to the database. Here is the (relevant bit of the) updated code:
$db = $this->conn();
$stmt = $db->prepare("UPDATE tblorderhdr SET completed = ? WHERE orderid = ?");
$stmt->bind_param('ii', $this->proc, $this->pid);
$stmt->execute();
When you call $this->conn(), you are creating a new connection object of class mysqli. When no more variables point to the object, PHP will trigger its destructor. The destructor for mysqli will close the connection. This means that if you do not save the return value of this method into a variable, there will be no more references pointing to the object and the connection will be closed.
To fix this, simply save the object and reuse it. Don't connect each time.
$conn = $this->conn();
$stmt = $conn->prepare("UPDATE tblorderhdr SET completed = ? WHERE orderid = ?");
On a side note, creating a class like the one you have db is absolutely pointless. This class doesn't do anything useful. You haven't added any extra functionality to mysqli. You never need to save the credentials in properties. The whole class can be replaced with this code:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli('localhost', 'user', 'password', 'test');
$mysqli->set_charset('utf8mb4'); // always set the charset
Then your classes will expect the mysqli object as a dependency.
class updOrders {
public $pid;
public $proc;
public function __construct(mysqli $conn) {
}
In a particular PHP - MySQL application i have been following a practice of creating and destroying the PDO connections from with in the public methods.
For Example:
public function updateCustomerNumber($customer_id, $customer_number){
$conn = new database_class();
$sql = "UPDATE customers set number = :CUSTOMER_NUMBER where id = :CUSTOMER_ID";
$query = $db->prepare($sql);
$query->execute(array(':CUSTOMER_NUMBER' => $customer_number, ':CUSTOMER_ID'=>$customer_id));
$conn->disconnect();
if($query->rowCount()>0){
return true;
} else {
return false;
}
}
So i have connected and disconnected the PDO Connection within the method. Database connect method:
public function connect() {
if (!$this->con) {
try {
$this->db = new PDO("mysql:host=$this->db_host;dbname=$this->db_name;charset=utf8", $this->db_user,$this->db_pass);
$this->db->exec("SET CHARACTER SET utf8");
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->con = true;
return $this->db;
} catch (PDOException $e) {
$error = $e->getMessage();
echo $error;
}
} else {
return $this->db;
}
}
Database disconnect method:
public function disconnect() {
if ($this->con) {
unset($this->db);
$this->con = NULL;
return true;
} else {
return false;
}
}
I haven't come across any issue so far except that a in a couple of method i forgot to disconnect the PDO connection at the end of the script. However, I am not sure if there are any drawbacks of following this approach.
Reason i started questioning this method is when i check the MySQL global status values.
Aborted_clients: 32560
Aborted_connects: 128080
Going by the definition, Even If we don't close connection explicitly, PHP will automatically close the connection when your script ends.
What could be the cause behind these numbers? and is this approach of opening and closing a PHP PDO connections correct ?
Feeling a little stupid to ask such a question, but this code block is driving me crazy.
function __construct() {
$db = new db();
$this->db = $db->pdo;
}
function getEmployeeDetails() {
$eid = $this->db->quote($this->eid);
try {
$sql = $this->db->query("
SELECT email, cnumber
FROM employees
WHERE EID = $eid
");
$r = $sql->fetch();
$this->email = $r[0];
$this->cnumber = $r[1];
}
catch (PDOException $e) {
throw new Exception("failed");
}
}
It doesn't throw an exception but fails inside the try block - "Call to a member function fetch() on a non-object".
var_dump of the statement object returns 'false'. Why?
I've tried running the query independently, inside MySql. It returns 1 row.
It's hard to tell whether you have done this, but PDO doesn't throw exceptions by default, except on connection failures. You have to specifically add this:
$this->db = $db->pdo;
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Without this, errors that occur during the query will cause ->query() to return false and that's obviously not an object that will have the ->fetch() method. You can also specify this attribute as part of the constructor call.
Also, you could use prepared statements instead of using ->quote():
$stmt = $this->db->prepare("SELECT email, cnumber
FROM employees
WHERE EID = ?");
$stmt->execute(array($this->eid));
$r = $stmt->fetch();
Here is Database.php:
<?php
/*Data Base Class
* MySQL - InnoDB
* PHP - PDO (PHP Data Object -So we could change databases if needed)
*/
class Database extends PDO{
private $DBH;
function __construct($host, $dbname, $user, $pass){
try {
$this->DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
} catch (PDOException $e) {
return $e->getMessage();
}
}
public function alteration_query($sql){
/* Begin a transaction, turning off autocommit */
$this->DBH->beginTransaction();
try{
$count = $this->DBH->exec($sql);
$this->DBH->commit();
return $count;
}catch (PDOException $e) {
$this->DBH->rollback();
return $e;
}
}
}
?>
Here is test.php:
<?php
require('Database.php');
$dbo = new Database('***.***.com','***','***','***');
echo $dbo->alteration_query('DELETE * from T_Table');
?>
For some reason, it won't give me an error or delete the contents of the T_table.
EDIT: The problem in your case is that the argument is called $sql, but you're using $query to execute it (in the alteration_query method). Next time, please enable error reporting, and/or use a decent IDE, that can show you these errors. Like so:
EDIT2: Set PDO's error mode to exception, this way any error would throw an exception. See updated code.
Don't catch your exceptions inside of the functions, do it outside:
<?php
/*Data Base Class
* MySQL - InnoDB
* PHP - PDO (PHP Data Object -So we could change databases if needed)
*/
class Database
{
private $DBH;
function __construct($host, $dbname, $user, $pass)
{
$this->DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
//Set PDO to throw exceptions on errors!
$this->DBH->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function alteration_query($sql)
{
/* Begin a transaction, turning off autocommit */
$this->DBH->beginTransaction();
$count = $this->DBH->exec($query);
$this->DBH->commit();
return $count;
}
}
try {
$pdo = new Database("localhost", "dbname", "user", "pass");
$pdo->alteration_query("SELECT * FROM wrong_table");
}
catch (PDOException $e) {
die("An error has occured! " . $e->getMessage());
}
This way, you can catch the error exactly where you need it, and not force it inside of the function (which kinda beats the point of the exception).
Also, from the manual:
When the script ends or when a connection is about to be closed, if
you have an outstanding transaction, PDO will automatically roll it
back. This is a safety measure to help avoid inconsistency in the
cases where the script terminates unexpectedly--if you didn't
explicitly commit the transaction, then it is assumed that something
went awry, so the rollback is performed for the safety of your data.
Exceptions halt the execution of the function, meaning the commit() never happens, and it rolls back.
Try this line :
$this->DBH = new parent::__construct("mysql:host=$host;dbname=$dbname", $user, $pass);
Reason : __construct overwrite the main class function. I think that's why, really not sure.
If it's not working, try parrent::PDO. I am pretty sure it an overwrite problem.
I have a db.php file that has the following in it:
// db.php file
// creates connection to database
// DATABASE CONNECTION FUNCTION
function sql_con(){
try{
$dbh = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME, DB_USER, DB_PASS,
array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
// log error to file and display friendly error to user
ExceptionErrorHandler($e);
exit;
}
}
I have a signup page and i want to check if username already exists, so i call my sql_con(); function beforehand to connect to database then do the below query
// connect to database
sql_con();
$stmt = $dbh->prepare("SELECT `user_login` FROM `users` WHERE `user_login` = ? LIMIT 1");
$stmt->execute(array($username));
if ( $stmt->rowCount() > 0 ) {
$error[] = 'Username already taken';
}
I'm very new to PDO and with the above i get the following errors:
Notice: Undefined variable: dbh in C:\wamp\www\signup.php on line 64
Fatal error: Call to a member function prepare() on a non-object in
C:\wamp\www\signup.php on line 64
Probably something very silly and I seem to confuse myself with PDO as I'm at the beginner stages. Could anyone tell me what I am doing wrong? Also I am not sure if this is the correct way as I'm new to PDO so if there's a more efficient way to do the username query check then please let me know.
This is because the $dbh object is limited to inside the try catch block and your sql_con() function due to scope.
The correct solution would be to remove the try catch block, and return the $dbh variable at the end of the sql_con() function.
Then:
try {
$dbh = sql_con();
$stmt = $dbh->prepare("SELECT `user_login` FROM `users` WHERE `user_login` = ? LIMIT 1");
$stmt->execute(array($username));
if ( $stmt->rowCount() > 0 ) {
$error[] = 'Username already taken';
}
}
catch (PDOException $e) {
//Do stuff with $e
}
The $dbh variable is destroyed once the function is done executing.
You could try returning the handle:
function sql_con() {
// your connection code
return $dbh;
}
Then in your signup page:
$dbh = sql_con();
Depending on your needs, a better alternative would be to employ a DI container.
You are defining pdo in function an ist not visible out it.
function sql_con(){
try{
$dbh = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME, DB_USER, DB_PASS,
array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $dbh; //this
} catch (PDOException $e) {
// log error to file and display friendly error to user
ExceptionErrorHandler($e);
exit;
}
}
$dbh = sql_con();
It will be better if you create a class for pdo abstraction.
$dbh = DB::getInstance();