20

I'd like to switch PDO INSERT and UPDATE prepared statements to INSERT and ON DUPLICATE KEY UPDATE since I think it'll be a lot more efficient than what I'm currently doing, but I'm having trouble figuring out the correct syntax to use with named placeholders and bindParam.

I found several similar question on SO, but I'm new to PDO and couldn't successfully adapt the code for my criteria. This is what I've tried, but it doesn't work (it doesn't insert or update):

try { 
  $stmt = $conn->prepare('INSERT INTO customer_info (user_id, fname, lname) VALUES(:user_id, :fname, :lname)'          
 'ON DUPLICATE KEY UPDATE customer_info SET fname= :fname, 
                                            lname= :lname   
                                            WHERE user_id = :user_id'); 
  $stmt->bindParam(':user_id', $user_id);  
  $stmt->bindParam(':fname', $_POST['fname'], PDO::PARAM_STR);
  $stmt->bindParam(':lname', $_POST['lname'], PDO::PARAM_STR);      
  $stmt->execute();
}

This is a simplified version of my code (I have several queries, and each query has between 20 - 50 fields). I'm currently updating first and checking if the number of rows updated is greater than 0 and if not then running the Insert, and each of those queries has it's own set of bindParam statements.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Chaya Cooper
  • 2,566
  • 2
  • 38
  • 67
  • Don't reuse the same placeholder in multiple places in the same query. Is your PDO connection set to throw exceptions? Unless you have a real need for `bindParam`, `bindValue` or passing parameters through `execute` is a better choice. – DCoder Dec 30 '12 at 07:42
  • When you say that I shouldn't reuse the same placeholder in multiple places, do you mean that I need to have 2 sets of bindParam statements? I have it set currently with - catch(PDOException $e) {echo 'Error: ' . $e->getMessage();} - and I'm not getting any error messages for this code. – Chaya Cooper Dec 30 '12 at 07:47
  • 1
    Just putting try/catch around the query is not enough. See [Error Handling](http://us.php.net/manual/en/pdo.error-handling.php) and configure it to throw exceptions on errors. – DCoder Dec 30 '12 at 07:57
  • @DCoder: Would you mind elaborating on what you mean? I thought the way I'm doing it is in line with ERRMODE_EXCEPTION in the manual, which should throw a PDOException. – Chaya Cooper Dec 30 '12 at 15:44
  • Your example doesn't show ERRMODE_EXCEPTION being set. That's why I asked if your connection is configured to do that. If you do have it set, nothing more is needed, problems should throw exceptions. – DCoder Dec 30 '12 at 15:53
  • @DCoder My bad for not including it ;-) It's taking me a little while to get a handle on PDO, and I'd forgotten that I'd put some of the error handling in the config file. – Chaya Cooper Dec 30 '12 at 16:05
  • @DCoder: I took your suggestions and switched it to bindValue, but I'd love to clarify what you were saying about the difference bindParam vs bindValue, and passing parameters through execute. (I'd originally been unable to get the code to work with bindValue so I was using bindParam) – Chaya Cooper Dec 30 '12 at 16:26
  • 1
    See [this question](http://stackoverflow.com/questions/10633229/all-columns-being-set-to-the-same-value) for differences between `bindParam` and `bindValue`. `$stmt->execute(array(':fname' => $_POST['fname']))` can also be used to pass bound variables. In my opinion, `execute` is the simplest option and should be preferred unless you need the specific behaviours of `bindParam` or `bindValue`. Your mileage may vary, of course. – DCoder Dec 30 '12 at 16:37
  • Thanks for that link - it explained it so well :-) I was under the impression that binding values or parameters is more secure then just executing an array. Am I mistaken? – Chaya Cooper Dec 30 '12 at 17:16

2 Answers2

26

Your ON DUPLICATE KEY syntax is not correct.

$stmt = $conn->prepare('INSERT INTO customer_info (user_id, fname, lname) VALUES(:user_id, :fname, :lname)
    ON DUPLICATE KEY UPDATE fname= :fname2, lname= :lname2');

$stmt->bindParam(':user_id', $user_id);  
$stmt->bindParam(':fname', $_POST['fname'], PDO::PARAM_STR);
$stmt->bindParam(':lname', $_POST['lname'], PDO::PARAM_STR);      
$stmt->bindParam(':fname2', $_POST['fname'], PDO::PARAM_STR);
$stmt->bindParam(':lname2', $_POST['lname'], PDO::PARAM_STR);      

You don't need to put the table name or SET in the ON DUPLICATE KEY clause, and you don't need a WHERE clause (it always updates the record with the duplicate key).

See http://dev.mysql.com/doc/refman/5.5/en/insert-on-duplicate.html

You also had a PHP syntax error: you split the query up into two strings.

UPDATE:

To bind multiple parameters:

function bindMultiple($stmt, $params, &$variable, $type) {
  foreach ($params as $param) {
    $stmt->bindParam($param, $variable, $type);
  }
}

Then call it:

bindMultiple($stmt, array(':fname', ':fname2'), $_POST['fname'], PDO::PARAM_STR);
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • Thank you for explaining it so clearly :-) Is there a way around having to create two bindParam statements for each parameter? – Chaya Cooper Dec 30 '12 at 08:10
  • 1
    No, PDO requires each placeholder to be unique. You could write a function that takes an array of placeholders and a variable, and calls `bindParam()` in a loop to bind them all. – Barmar Dec 30 '12 at 08:34
  • You're awesome :-D I'll try it out first thing in the morning :-) – Chaya Cooper Dec 30 '12 at 09:15
  • The first part of the code worked like a charm, but I'm having trouble getting it to work with binding multiple parameters. Just to clarify, do I create separate bindMultiple statements for each element? Also, I wasn't sure if I was correct in thinking that I should leave the original bindParam statement for user_id? – Chaya Cooper Dec 30 '12 at 17:37
  • You need to use a separate `bindMultiple` for each variable that you need to repeat in the SQL. You need to repeat `$_POST['fname']` and `$_POST['lname']`, but there's only one `$user_id`. – Barmar Dec 30 '12 at 17:41
  • 2
    I just noticed an issue. `bindParam` requires the variable to be a reference. I just changed `bindMultiple` to reflect that. – Barmar Dec 30 '12 at 17:43
  • Glad you caught that :-D It's working perfectly now :-D Is there anything I need to do differently for bindValue? Also, I'm assuming that I can add additional placeholders for other queries (i.e. fetch), but I was wondering if there are performance issues, etc. that I should keep in mind? – Chaya Cooper Dec 30 '12 at 19:28
  • For `bindValue()` it doesn't make sense to refer to the same column or variable multiple times, so it's not an issue. – Barmar Dec 31 '12 at 03:46
  • I know this is probably a very basic question, but can you explain why it wouldn't make sense to do it with bindValue? I don't quite have a handle on all this yet. @DCoder commented that bindValue would be a better choice then bindValue (and I was happy that your awesome script worked with both ;-)) I have several queries for each of the fields (insert & update of course, fetch, etc.) and since there are a few hundred fields I was hoping to simplify defining and binding variables – Chaya Cooper Dec 31 '12 at 04:06
  • Sorry, my mistake, I was thinking of bindResults. The only difference with bindValue is that it doesn't need to use a reference, but I think it still requires unique placeholders. – Barmar Dec 31 '12 at 04:29
  • Thanks for clarifying that :-D So just to verify, can I use bindMultiple as is for bindValue? Also, I'm assuming that I can add additional placeholders for other queries (i.e. fetch), but I was wondering if there are performance issues, etc. that I should keep in mind? – Chaya Cooper Dec 31 '12 at 04:33
  • 1
    Yes, just change it to use a normal parameter instead of reference. I don't think there are any performance issues, there's nothing special about doing it this way. – Barmar Dec 31 '12 at 05:15
26

IMHO below is the right answer for anyone coming across this again.
Note: this statement assumes user_id is a KEY in the table.

The STATEMENT indeed was wrong, but the accepted answer was not completely correct.

If you're inserting and updating using the same values (and not updating with different values), this is the query pseudo-code corrected:

try { 
    //optional if your DB driver supports transactions
    $conn->beginTransaction();

    $stmt = $conn->prepare('INSERT INTO customer_info (user_id, fname, lname) ' . 
                'VALUES(:user_id, :fname, :lname)' .
                'ON DUPLICATE KEY UPDATE fname=VALUES(fname), lname=VALUES(lname)');
    $stmt->bindParam(':user_id', $user_id);  
    $stmt->bindParam(':fname', $_POST['fname'], PDO::PARAM_STR);
    $stmt->bindParam(':lname', $_POST['lname'], PDO::PARAM_STR);      
    $stmt->execute();

    //again optional if on MyIASM or DB that doesn't support transactions
    $conn->commit();
} catch (PDOException $e) {
    //optional as above:
    $conn->rollback();

    //handle your exception here $e->getMessage() or something
}
akahunahi
  • 1,782
  • 23
  • 21
  • 7
    +1 I agree that using `VALUES()` is easier when you need to use parameters. But as a side issue, fwiw you don't need to break your string in PHP like you do in Java. You can put a multi-line string into a single set of quotes. – Bill Karwin Feb 13 '14 at 01:49
  • @BillKarwin Is correct, but we like to wrap our lines to 80 or 100 characters in our company :-) – akahunahi Feb 13 '14 at 18:49
  • 1
    Very elegant solution using `VALUES()` again, instead of duplicating field names without an extra suffix. It will be deprecated soon and can be simplified by just using the field name. [mysql docs](https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html). – Markus Zeller Jan 27 '21 at 11:49