Search code examples
phppdophp-7.4bindvalue

Insert multiple data into different tables in a single transaction


This has nothing to do with Frameworks because it is an external project, I am currently developing to learn, some things about database management or how databases behave...

I need help with some php and PDO... Previously I dev this script:

<?php

class DataBaseManager
{
    public function GetData($query, $user, $pass)
    {
        try {
            $db_result = [];
            $conn = new PDO("mysql:host=localhost;dbname=test", $user, $pass);
            $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $conn->exec("set names utf8");
            reset($query);
            $db_name = key($query);
            $conn->exec('USE ' . $db_name);
            $db_result['r'] = $conn->query($query[$db_name], PDO::FETCH_ASSOC);
            $count = $db_result['r']->rowCount();
            $db_result['c'] = $count;
            if (0 == $count) {
                $db_result['r'] = null;
            } elseif (1 == $count) {
                $db_result['r'] = $db_result['r']->fetch();
            }

            return $db_result;
        } catch (PDOException $e) {
            echo $e->getMessage();
            return false;
        }
    }

    public function DeleteData($query, $user, $pass)
    {
        try {
            $conn = new PDO("mysql:host=localhost;dbname=test", $user, $pass);
            $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $conn->beginTransaction();
            $conn->exec("set names utf8");
            foreach ($query as $db_name => $query_arr) {
                $conn->exec('USE ' . $db_name);
                foreach ($query_arr as $key => $query_string) {
                    $conn->exec($query_string);
                    ++$ct;
                }
            }
            $conn->commit();
            $conn = null;

            return '<b>' . $ct . ' Records Deleted Successfully.</b>';
        } catch (PDOException $e) {
            $conn->rollback();
            echo $e->getMessage();
            return false;
        }
    }

    public function SetData($query, $user, $pass)
    {
        try {
            $db_result = [];
            $conn = new PDO("mysql:host=localhost;dbname=test", $user, $pass);
            $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $conn->beginTransaction();
            $conn->exec("set names utf8");
            $count = ['u' => 0, 'i' => 0];
            foreach ($query as $db_name => $query_arr) {
                $conn->exec('USE ' . $db_name);
                foreach ($query_arr as $key => $query_string) {
                    $cq = $conn->exec($query_string);
                    if (strpos($query_string, 'UPDATE') !== false) {
                        $count['u'] += $cq;
                    }
                    if (strpos($query_string, 'INSERT') !== false) {
                        $count['i'] += $cq;
                    }
                }
            }
            $conn->commit();
            $db_result['r'] = true;
            $db_result['t'] = 'Updates: ' . $count['u'] . ', Inserts: ' . $count['i'];

            return $db_result;
        } catch (PDOException $e) {
            $conn->rollback();
            echo $e->getMessage();
            return false;
        }
    }
}

I would like to be able to insert data by volumes in multiple tables in a single commit...

The idea of doing it that way is because if something fails I want it to be automatically rolled back in all table instances...

So I have a data structure in an array with the following content in my new class:

$dbquery =[
        'test'=>[
            '0' =>[
                0 => 'INSERT INTO table_name_1(column1,column2) VALUES (?,?)', //mean it is the query
                1 =>[value11,value12],  // mean it is a row
                2 =>[value21,value22],  // mean it is a row
            ],
            '1' =>[
                0 => 'INSERT INTO table_name_2(column1,column2,column3,column4) VALUES (?,?,?,?)', //mean it is the query
                1 =>[value11,value12,value13,value14], // mean it is a row
                2 =>[value21,value22,value23,value24], // mean it is a row
            ],
        ],
    ];

but i have my first try with bindValue, this is my new class mentioned:

<?php

class DataBase
{
    private static ?DataBase $instance = null;

    public static function getInstance(): DataBase
    {
        if (!self::$instance instanceof self) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    private array $config;

    public function __construct()
    {

        $this->config = ['DB_HOST'=>'test','DB_NAME'=>'test','DB_USER'=>'test','DB_PASSWORD'=>''];
        $this->setConnection(new PDO("mysql:host=" . $this->config['DB_HOST'] . ";dbname=" . $this->config['DB_NAME'], $this->config['DB_USER'], $this->config['DB_PASSWORD']));
    }

    private PDO $connection;

    /**
     * @return PDO
     */
    private function getConnection(): PDO
    {
        return $this->connection;
    }

    /**
     * @param PDO $connection
     */
    private function setConnection(PDO $connection): void
    {
        $this->connection = $connection;
    }

    public function changeConnectionServer(string $host, string $db_name, string $user, string $password): void
    {
        $this->setConnection(new PDO("mysql:host=" . $host . ";dbname=" . $db_name, $user, $password));
    }

    private array $query;

    public function setDataBaseTarget(string $db_name)
    {
        if (empty($this->query)) {
            $this->query = [];
        }
        $this->query[$db_name] = [];
    }

    public function buildQuery(string $query)
    {
        if (empty($this->query)) {
            $this->query = [];
            $this->query[$this->config['DB_NAME']] = [];
        }
        $target = array_key_last($this->query);


        $this->query[$target][] = [$query];
    }

    public function addQueryData($data)
    {
        $target = array_key_last($this->query);
        $key = array_key_last($this->query[$target]);
        $this->query[$target][$key][] = $data;
    }

    private function getQuery(): array
    {
        return $this->query;
    }

    /**
     * @throws Exception
     */
    public function setData(): array
    {
        try {
            $time = -microtime(true);
            $con = $this->getConnection();
            $con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $con->beginTransaction();
            $con->exec("set names utf8;");

            foreach ($this->getQuery() as $db_name => $query_arr) {
                $con->exec('USE `' . $db_name . '`;');
                $ct = 0;
                
                // on this section have proble with code and logic .... i dont know what i need to dev to insert the data
                
                foreach ($query_arr as $query_structure) {
                    foreach ($query_structure as $key => $raw) {
                        if ($key === 0) {
                            $ct++;
                            $stmt[$ct] = $con->prepare($raw);
                        } else {
                            if (is_array($raw)) {
                                $c = 0;
                                foreach ($raw as $value) {
                                    $c++;
                                    $stmt[$ct]->bindValue($c, $value, $this->getParamType($value));
                                }
                            }
                        }
                    }
                    $stmt[$ct]->execute();
                }
                
                //end section
                
            }



            //$con->commit();


            return true;
        } catch (PDOException $e) {
            
            $con->rollback();
            echo $e->getMessage();

            return false;
        }
    }

    private function getParamType($value)
    {
        if (is_int($value)) {
            return PDO::PARAM_INT;
        } elseif (is_bool($value)) {
            return PDO::PARAM_BOOL;
        } elseif (is_null($value)) {
            return PDO::PARAM_NULL;
        } elseif (is_string($value)) {
            return PDO::PARAM_STR;
        } else {
            return false;
        }
    }
}

$db_handler = DataBase::getInstance();

$db_handler->buildQuery("INSERT INTO `client_list`(`email`,`mobile`) VALUES ('?','?');");
$db_handler->addQueryData(['[email protected]', '35634634636546']);
$db_handler->addQueryData(['[email protected]', '35634634636546']);
$db_handler->addQueryData(['[email protected]', '35634634636546']);
$db_handler->setData();

I can't figure out, develop the part that allows me to package everything in a single transaction... what I have is a stm[] ...

Can someone help me with this development?


Solution

  • I can't make heads or tails of all the things your class is doing, but here's a generalized version according to the bits that seem obvious:

    public function runMultipleQueries() {
        $dbh = $this->getConnection();
        // ... setup stuff
        $dbh->beginTransaction();
        try {
            foreach($this->queries as $query) {
                $stmt->prepare($query->queryString);
                $param_id = 1;
                foreach($query->params as $param) {
                    $stmt->bindValue($param_id++, $param);
                }
                $stmt->execute();
            }
        } catch( \Exception $e ) {
            $dbh->rollback();
            throw $e;
            // IMO if you cannot fully/meaningfully recover, just re-throw and let it kill execution or be caught elsewhere where it can be
            // otherwise you're likely having to maintain a stack of if(foo() == false) { ... } wrappers
        }
        $dbh->commit();
    }
    

    Additionally, singleton DB classes have the drawbacks of both being limiting if you ever need a second DB handle, as well as boiling down to being global state, and subject to the same "spooky action at a distance" problems.

    Consider using dependency inversion and feeding class dependencies in via constructor arguments, method dependencies via method arguments, etc. Eg:

    interface MyWrapperInterface {
        function setQueries(array $queries);
        function runQueries();
    }
    
    class MyDbWrapper implements MyWrapperInterface {
        protected $dbh;
        
        public function __construct(\PDO $dbh) {
            $this->dbh = $dbh;
        }
        
        public function setQueries(array $queries) { /* ... */ }
        public function runQueries() { /* ... */ }
    }
    
    class MyOtherThing {
        protected $db;
        
        public function __construct( MyWrapperInterface $db ) {
            $this->db = $db;
        }
        
        // ...
    }
    
    $wrapper1 = new MyDbWrapper(new PDO($connstr1, $options));
    $wrapper2 = new MyDbWrapper(new PDO($connstr2, $options));
    
    $thing1   = new MyOtherThing($wrapper1);
    $thing2   = new MyOtherThing($wrapper2);
    $thing1_2 = new MyOtherThing($wrapper1);
    
    // etc