1

I would like to know how can I insert multiple values in an array via prepared statements. I've looked at these two (this question and this other one ) questions but they don't seem to do what I'm trying. This is what I have:

$stmt = $this->dbh->prepare("INSERT INTO
t_virtuemart_categories_en_gb 
(category_name, virtuemart_category_id)
VALUES
(:categoryName, :categoryId)
;");

foreach($this->values as $insertData){
    $categoryName = $insertData['categoryName'];
    $categoryId = $insertData['categoryId'];
    $stmt->bindParam(':categoryName', $categoryName);
    $stmt->bindParam(':categoryId', $categoryId);
    $stmt->execute();
}

I tried placing the prepare line inside the foreach loop and outside, but it only adds the first key in the array, and I don't understand why.

This is my Connection.php file:

<?php
$hostname = 'localhost';
$username = 'root';
$password = '';

function connectDB ($hostname, $username, $password){
$dbh = new PDO("mysql:host=$hostname;dbname=test", $username, $password);
return $dbh;
}

try {
$dbh = connectDB ($hostname, $username, $password);
} catch(PDOException $e) {
echo $e->getMessage();
}

And my Import.php file:

<?php
class Import{
public function __construct($dbh, $values) {
    $this->dbh = $dbh;
    $this->values = $values;
}

public function importData() {
    $stmt = $this->dbh->prepare("INSERT INTO
    t_virtuemart_categories_en_gb 
    (category_name, virtuemart_category_id)
    VALUES
    (:categoryName, :categoryId)
    ;");

    foreach($this->values as $insertData){
        $categoryName = $insertData['categoryName'];
        $categoryId = $insertData['categoryId'];
        $stmt->bindParam(':categoryName', $categoryName);
        $stmt->bindParam(':categoryId', $categoryId);
        $stmt->execute();
    }
}

}
Newwt
  • 491
  • 1
  • 5
  • 22
  • have you debugged the array (`$this->values`) using a print statement to see if each set of values is printing OK? Typically I would say put the `prepare` outside the loop and bind the params outside the loop too – Professor Abronsius Sep 13 '17 at 13:21
  • @RamRaider Yep. I tried doing a `var_dump`, `echo` and everything, but they return all the proper values for each iteration. – Newwt Sep 13 '17 at 13:26
  • Is it only inserting once, or `count($this->values)` times with the same data? – ishegg Sep 13 '17 at 14:17
  • @ishegg Only once. – Newwt Sep 13 '17 at 14:18
  • And what does `var_dump($this->values);` print? – ishegg Sep 13 '17 at 14:18
  • @ishegg https://pastebin.com/Htp2xirq There are a ton of results, so I cut it. – Newwt Sep 13 '17 at 14:57
  • Hm I'm thinking of several possibilites, doesn't it throw an error? Put `$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);` after you create instantiate `PDO` (change `$dbh` for your variable). – ishegg Sep 13 '17 at 15:03
  • @ishegg Where should I put it? I edited the question with my php files. – Newwt Sep 13 '17 at 15:14
  • Right after `$dbh = new PDO("mysql:host=$hostname;dbname=test", $username, $password);` – ishegg Sep 13 '17 at 15:19
  • @ishegg AH-HA!! `Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '' for key 'slug' ` What a stupid mistake from my side. Just fixed it by using auto-increment and everything went just fine. Thank you very much! – Newwt Sep 14 '17 at 07:36

3 Answers3

2

Working principle:

Use only one INSERT sql statement to add multiple records, defined by your values pairs. In order to achieve this you have to build the corresponding sql statement in the form

INSERT INTO [table-name] ([col1],[col2],[col3],...) VALUES (:[col1],:[col2],:[col3],...), (:[col1],:[col2],:[col3],...), ...

by iterating through your values array.

Notes:

  • I hope you'll understand all. I commented as much as I could. I didn't test it, but it should work. Maybe an answer I wrote a short time ago will give you further ideas regarding structuring of data access classes/functions as well.
  • Never use ";" at the end of the sql statements when you define them in PHP.
  • Never use one input marker to bind multiple values. For each value to bind use a unique named input marker.

Good luck.

Connection.php

<?php

$hostname = 'localhost';
$username = 'root';
$password = '';
$port = 3306;

try {
    // Create a PDO instance as db connection to a MySQL db.
    $connection = new PDO(
            'mysql:host='. $hostname .';port='.$port.';dbname=test'
            , $username
            , $password
    );

    // Assign the driver options to the db connection.
    $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
    $connection->setAttribute(PDO::ATTR_PERSISTENT, TRUE);
} catch (PDOException $exc) {
    echo $exc->getMessage();
    exit();
} catch (Exception $exc) {
    echo $exc->getMessage();
    exit();
}

Import.php:

<?php

class Import {

    /**
     * PDO instance as db connection.
     * 
     * @var PDO
     */
    private $connection;

    /**
     * 
     * @param PDO $connection PDO instance as db connection.
     * @param array $values [optional] Values list.
     */
    public function __construct(PDO $connection, array $values = array()) {
        $this->connection = $connection;
        $this->values = $values;
    }

    /**
     * Import data.
     * 
     * @return int Last insert id.
     * @throws PDOException
     * @throws UnexpectedValueException
     * @throws Exception
     */
    public function importData() {
        /*
         * Values clauses list. Each item will be
         * later added to the sql statement.
         * 
         *  array(
         *      0 => '(:categoryName0, :categoryId0)',
         *      1 => '(:categoryName1, :categoryId1)',
         *      2 => '(:categoryName2, :categoryId2)',
         *  )
         */
        $valuesClauses = array();

        /*
         * The list of the input parameters to be
         * bound to the prepared statement.
         * 
         *  array(
         *      :categoryName0 => value-of-it,
         *      :categoryId0 => value-of-it,
         *      :categoryName1 => value-of-it,
         *      :categoryId1 => value-of-it,
         *      :categoryName2 => value-of-it,
         *      :categoryId2 => value-of-it,
         *  )
         */
        $bindings = array();

        /*
         * 1) Build a values clause part for each array item,
         *    like '(:categoryName0, :categoryId0)', and 
         *    append it to the values clauses list.
         * 
         * 2) Append each value of each item to the input
         *    parameter list.
         */
        foreach ($this->values as $key => $item) {
            $categoryName = $item['categoryName'];
            $categoryId = $item['categoryId'];

            // Append to values clauses list.
            $valuesClauses[] = sprintf(
                    '(:categoryName%s, :categoryId%s)'
                    , $key
                    , $key
            );

            // Append to input parameters list.
            $bindings[':categoryName' . $key] = $categoryName;
            $bindings[':categoryId' . $key] = $categoryId;
        }

        /*
         * Build the sql statement in the form:
         *  INSERT INTO [table-name] ([col1],[col2],[col3]) VALUES 
         *  (:[col1],:[col2],:[col3]), (:[col1],:[col2],:[col3]), ...
         */
        $sql = sprintf('INSERT INTO t_virtuemart_categories_en_gb (
                    category_name,
                    virtuemart_category_id
                ) VALUES %s'
                , implode(',', $valuesClauses)
        );

        try {
            // Prepare the sql statement.
            $statement = $this->connection->prepare($sql);

            // Validate the preparing of the sql statement.
            if (!$statement) {
                throw new UnexpectedValueException('The sql statement could not be prepared!');
            }

            /*
             * Bind the input parameters to the prepared statement 
             * and validate the binding of the input parameters.
             * 
             * -----------------------------------------------------------------------------------
             * Unlike PDOStatement::bindValue(), when using PDOStatement::bindParam() the variable 
             * is bound as a reference and will only be evaluated at the time that 
             * PDOStatement::execute() is called.
             * -----------------------------------------------------------------------------------
             */
            foreach ($bindings as $key => $value) {
                // Read the name of the input parameter.
                $inputParameterName = is_int($key) ? ($key + 1) : (':' . ltrim($key, ':'));

                // Read the data type of the input parameter.
                if (is_int($value)) {
                    $inputParameterDataType = PDO::PARAM_INT;
                } elseif (is_bool($value)) {
                    $inputParameterDataType = PDO::PARAM_BOOL;
                } else {
                    $inputParameterDataType = PDO::PARAM_STR;
                }

                // Bind the input parameter to the prepared statement.
                $bound = $statement->bindValue($inputParameterName, $value, $inputParameterDataType);

                // Validate the binding.
                if (!$bound) {
                    throw new UnexpectedValueException('An input parameter could not be bound!');
                }
            }

            // Execute the prepared statement.
            $executed = $statement->execute();

            // Validate the prepared statement execution.
            if (!$executed) {
                throw new UnexpectedValueException('The prepared statement could not be executed!');
            }

            /*
             * Get the id of the last inserted row.
             */
            $lastInsertId = $this->connection->lastInsertId();
        } catch (PDOException $exc) {
            echo $exc->getMessage();
            // Only in development phase !!!
            // echo '<pre>' . print_r($exc, TRUE) . '</pre>';
            exit();
        } catch (Exception $exc) {
            echo $exc->getMessage();
            // Only in development phase !!!
            // echo '<pre>' . print_r($exc, TRUE) . '</pre>';
            exit();
        }

        return $lastInsertId;
    }

}
  • It gives me this: `SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'interno0, ?),(? Portátil1, ?),(? Proyectores2, ?),(? y fuentes3, ?),(? en red' at line 4`. By the way, why shouldn't I use ";" at the end of SQL statements? – Newwt Sep 14 '17 at 08:09
  • 1
    Those question marks are very strange: `interno0, ?),(? Portátil1, ?),(? Proyectores2, ?),(? y fuentes3, ?),(? en red`!! Show your sql statement with only one values row, please. –  Sep 14 '17 at 10:04
  • 1
    @Newwt No problem, each of us had to be a newbie in the beginning ;-) Just read the code and the comments with patience, line by line. This code I provided to you (from the sql statement forward) is a very important code, to use in the future on all data access operations. I help you with pleasure, as well. Now, about the specific problem, give me 5 mins to study it. –  Sep 14 '17 at 15:08
  • 1
    @Newwt I found the problem. I made a mistake in the code, inside the loop `foreach ($this->values as $key => $item) {...}`. Change the equality `$valuesClauses[$key] = sprintf(...);` with what i changed in the edited answer. So, just that equality. Then try again and give feedback. A note: Don't post such sensible information (like the one you posted in the pastebin) on the web! It's better to post in the comments hier a little part of it and describe the problem in words, because the comments hier can be deleted. –  Sep 14 '17 at 15:25
  • I just tried it, and it works! I'm now figuring how to adapt it to all my tables (so I can use this same function for any table, depending on which one I select). It's a bit tricky and I feel like a monkey playing with a shotgun, but it works wonderfully, thank you very much!! – Newwt Sep 15 '17 at 08:08
  • By the way, I would also like to thank you for the comments explaining step by step what they do. They're immensely helpful. – Newwt Sep 15 '17 at 08:13
  • 1
    @Newwt You are welcome. I'm glad that it works. Now, you could realise a "separation of concerns": 1) You can construct the INSERT statement with the help of a corresponding method of a so called "query builder" class. 2) You can run all the data access operations using a so called "database adapter" class. You pass the sql stament and the bindings array to the corresponding class method and she makes all the work of preparing, validating and inserting the data for you. –  Sep 15 '17 at 12:44
  • 1
    @Newwt 3) In the Import class you can then pass the QueryBuilder class and the DbAdapter class as constructor arguments and use them inside importData() method. On its side, the importData() method would have the table name and the array of the values to insert, as parameters. Not so long time ago I posted a db adapter class in [an answer of mine](https://stackoverflow.com/questions/46014772/return-multiple-response-data-in-one-response/46018999#46018999). Maybe it can be of help for you. –  Sep 15 '17 at 12:49
  • 1
    @Newwt Some advices: In the db adapter class, the connection should be created outside of the class and just be passed as constructor argument (instead of having a connect() and a disconnect() method). Also, try to become an expert in "exception handling" in general. This should take you about 3 hours, top, to comprehence. Read tutorials, watch youtubes, etc. Then you should play with it as much as possible. In the end, you'll have a totally new way of validating and handling throughout ALL of your projects. –  Sep 15 '17 at 13:06
  • 1
    @Newwt And you'll then know how to easily apply it properly on the data access side ;-) The last one: for each function/method you are using in PHP, especially for the ones of the PDO layer, read the php.net documentation. Especially the "Return Values" part. Their examples are also very important. Well, that's it :-) Good luck! –  Sep 15 '17 at 13:06
  • 1
    @Newwt I gave a quick thought about it: don't use any query builder class. For maximal flexibility, just build your sql statements on-place. But you should definitely create and use a db adapter class. –  Sep 15 '17 at 13:23
  • That's a lot, I really appreciate the suggestions. I will give a good look to everything, thank you very much, I really mean it! – Newwt Sep 15 '17 at 14:15
  • 1
    @Newwt Well, have fun with all of it. –  Sep 15 '17 at 16:40
0

I think the statements have to be prepared and bound separately for each iteration:

if($stmt = $this->dbh->prepare("INSERT INTO t_virtuemart_categories_en_gb (category_name, virtuemart_category_id) VALUES (:categoryName, :categoryId);")){
    foreach($this->values as &$insertData){
        $stmt->bindParam(':categoryName', $insertData['categoryName']);
        $stmt->bindParam(':categoryId', $insertData['categoryId']);
        $stmt->execute();
        $stmt->close();
    }
}
Jarek.D
  • 1,274
  • 1
  • 8
  • 18
0

I would suggest this, using a $dbh = mysqli_connect():

<?php
class Import{
public function __construct($dbh, $values) {
    $this->dbh = $dbh;
    $this->values = $values;
}

public function importData() {
    $stmt = $this->dbh->prepare("INSERT INTO t_virtuemart_categories_en_gb 
    (category_name, virtuemart_category_id)
    VALUES
    (?, ?)");

    $catetoryName = ''; $categoryId = '';
    $stmt->bind_param('ss', $categoryName, $categoryId);
    foreach($this->values as $insertData){
        $categoryName = $insertData['categoryName'];
        $categoryId = $insertData['categoryId'];
        $stmt->execute();
    }
}

}

In this way you create a reference and bind that variable to the execution of the prepared statement.

You should be careful about it when passing an array, the trick is commented in the php.net page ( http://php.net/manual/it/mysqli.prepare.php )

A code that work is:

$typestring = 'sss'; //as many as required: calc those
$stmt = $dbconni->prepare($ansqlstring);
$refs = [$typestring];
// if $source is an array of array  [ [f1,f2,...], [f1,f2,...], ...]
foreach($source as $data) {
  if (count($refs)==1) {
    foreach ($data as $k => $v) {
      $refs[] = &$data[$k];
    }
    $ref = new \ReflectionClass('mysqli_stmt');
    $method = $ref->getMethod("bind_param");
    $method->invokeArgs($stmt, $refs);
    $r = $stmt->execute();
  } else {
    // references are maintained: no needs to bind_param again
    foreach ($data as $k => $v) {
      $refs[$k+1] = $v;
    }
    $r = $stmt->execute();
  }
}

this spare resources and is more performant, but you have to make benchmark to be sure about my words.

this is one of the case where prepared statement make sense, see

https://joshduff.com/2011-05-10-why-you-should-not-be-using-mysqli-prepare.md

Normally PDO emulate prepared statement, see PDO::ATTR_EMULATE_PREPARES

EDIT: specify it is using mysqli, and correct the code.

Daniele Cruciani
  • 623
  • 1
  • 8
  • 15