21

How can I write an INSERT doctrine query with option ON DUPLICATE KEY UPDATE?

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
Leonardo
  • 2,273
  • 6
  • 29
  • 32
  • 1
    the only problem with replace() seems to be that it drops and then creates a new row (rather than performing an actual UPDATE), thus dropping the auto increment ids (in this case, my primary id). Am I missing something here? eg - my auto increment id is 9, but the count is as 3000. When I perform REPLACE INTO for row 9, the new row id is 3001. – Leonardo Dec 29 '10 at 15:27
  • how could your autoincrement id be 9 yet the count of that column is 3000, doesn't make logical sense. – JM4 Dec 29 '10 at 15:34
  • I believe you need to bypass Doctrine to do this and write raw SQL, at least in Doctrine 1.2. The alternative is breaking it into two queries: check first, then update/insert. – Tom Dec 30 '10 at 00:59
  • See http://stackoverflow.com/questions/1132571/implementing-update-if-exists-in-doctrine-orm which should somewhat help. – Frank N Dec 02 '11 at 12:39
  • Using replace() is your best bet. See http://stackoverflow.com/questions/1132571/implementing-update-if-exists-in-doctrine-orm – Frank N Dec 02 '11 at 12:39
  • I've started work on a plugin to implement this functionality. It's still in the early stages but tested and working for my use-case. It's available at: https://github.com/m14t/m14tDoctrineRecordPlugin Test cases, bug reports and pull requests welcome. – m14t Apr 19 '12 at 07:27

9 Answers9

20

for Symfony 2 use raw sql:

$em->getConnection()->prepare("INSERT INTO table SET 
    some_fields = "some data", created_at = NOW() 
    ON DUPLICATE KEY UPDATE
    some_fields = "some data", updated_at = NOW()
")->execute();
FlameStorm
  • 944
  • 15
  • 20
artemiuz
  • 444
  • 5
  • 7
  • 2
    I don't like this approach because it breaks the DB independence Doctrine gives me. But it's still better than the need to do a a DB fetch every time to check if the entity already exists, because most of the time it _will_ exist. The update will be way more common then the insert. – flu Jul 18 '14 at 09:50
  • 4
    It's the only solution I have seen so far that doesn't have a race condition – Ryall Jan 24 '15 at 20:28
8

The problem is that this is a MySQL specific problem so it will not be directly covered by Doctrine.

As a comment mentioned, you would need to write a RawSQL Query for this. This would be the easiest way.

If you want it more sophisticated and truely DB independent, look into Events and it's possibilities. Before the actual query is executed, you can check for an existence and if it exists, act accordingly.

An ORM/PHP independent way is to write a stored procedure/trigger that handles this problem database side.

DrColossos
  • 12,656
  • 3
  • 46
  • 67
  • MySQL's REPLACE INTO statement is covered by Doctrine, so your initial point is false. – stan Aug 27 '11 at 20:18
  • 4
    @StanislavPalatnik The only reference I found to REPLACE is in the reserved keyword lists for MySQL and SQLite. – vbence Jun 15 '12 at 16:13
6

You can't. It's not supported by Doctrine right now.

What you could do is to imitate what MySQL does by checking if the entity exists and update/create it accordingly:

$em = $this->getEntityManager();

// Prevent race conditions by putting this into a transaction.
$em->transactional(function($em) use ($content, $type) {
  // Use pessimistic write lock when selecting.
  $counter = $em->createQueryBuilder()
    ->select('MyBundle:MyCounter', 'c')
    ->where('c.content = :content', 'c.type = :type')
    ->setParameters(['content' => $content, 'type' => $type])
    ->setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE);
    ->getQuery()
    ->getResult()
  ;

  // Update if existing.
  if ($counter) {
    $counter->increase();
  } else {
    // Create otherwise.
    $newCounter = new Counter($content, $type, 1);
    $em->persist($newCounter);
  }
});

If the record exists PESSIMISTIC_WRITE makes sure that it's not updated by anyone (e.g., other threads) while we're updating it.

Although you need to check for the entity's existence on every update, it's a simple reproduction of "update if existing and create if not".

As pointed out in the comments this does not prevent a race condition if the record doesn't exist: If a row with the same key(s) gets inserted between the select and the insert you're running into a duplicate key exception.

But given the constraints that this needs to be DB independent and thus written using Doctrine and not using native SQL it may help in some cases.

References:

flu
  • 14,307
  • 8
  • 74
  • 71
  • 1
    This exposes a race case and does not replace the functionality of MySQL's `ON DUPLICATE KEY UPDATE` – Brian Dec 16 '15 at 19:31
  • @Brian You are right, thank you. I've wrapped the code into a transaction and added references for further reading. – flu Jan 04 '16 at 12:13
  • 2
    Since you can't lock a row that doesn't exist, your solution still suffers from a race condition. That means that if a row with the same key gets inserted after the `SELECT` but before the `INSERT`, you'll catch an error/exception. – Elnur Abdurrakhimov Mar 24 '18 at 01:34
  • @ElnurAbdurrakhimov Thanks, @Brian also pointed that out. I updated the answer accordingly as this was never intended to fully replicate `ON DUPLICATE KEY UPDATE` but give guidance on how someone could try to reproduce it if forced to do it in Doctrine/DQL without any native SQL queries. – flu Mar 27 '18 at 08:00
2

You can use function like this to build and execute raw sql:

 /**
 * 
 * insertWithDuplicate('table_name', array('unique_field_name' => 'field_value', 'field_name' => 'field_value'), array('field_name' => 'field_value'))
 * 
 * @param string $tableName
 * @param array $insertData 
 * @param array $updateData
 * 
 * @return bolean
 */
public function insertWithDuplicate($tableName, $insertData, $updateData) {
    $columnPart = '';
    $valuePart = '';
    $columnAndValue = '';
    foreach ($insertData as $key => $value) {
        $value = str_replace(array('"', "'"), array('\"', "\'"), $value);
        $columnPart .= "`" . $key . "`" . ',';
        is_numeric($value) ? $valuePart .= $value . ',' : $valuePart .= "'" . $value . "'" . ',';
    }
    foreach ($updateData as $key => $value) {
        $value = str_replace(array('"', "'"), array('\"', "\'"), $value);
        is_numeric($value) ? $columnAndValue .= $key . ' = ' . $value . ',' : $columnAndValue .= "`" . $key . "`" . ' = ' . "'" . $value . "'" . ',';
    }
    $_columnPart = substr($columnPart, 0, strlen($columnPart) - 1);
    $_valuePart = substr($valuePart, 0, strlen($valuePart) - 1);
    $_columnAndValue = substr($columnAndValue, 0, strlen($columnAndValue) - 1);
    $query = "INSERT INTO " . $tableName .
            " (" . $_columnPart . ") "
            . "VALUES" .
            " (" . $_valuePart . ") "
            . "ON DUPLICATE KEY UPDATE " .
            $_columnAndValue;
    return $this->entityManager->getConnection()
                    ->prepare($query)->execute();
}
Marcin Żurek
  • 145
  • 1
  • 3
1

I had the same problem and after investigating a bit it looks like Doctrine doesn't do it. My solution was to do a findBy before my insert to see if any records exist with the unique fields. If this returns an entity then I update that entity and persist it instead of creating a new entity to persist.

If you are concerned about performance then this is not ideal as we are doing a select before every insert. However since Doctrine is database agnostic it is the only alternative to locking yourself to MySQL. It's one of those tradeoffs: do you want performance or portability.

Emmet O'Grady
  • 269
  • 2
  • 8
1

I created a doctrine dbal wrapper to do that. It can be used with DoctrineBundle with the dbal wrapper_class option.

https://github.com/iJanki/doctrine-mysql-dbal-extensions

iJanki
  • 41
  • 4
  • Your code is in danger of SQL injection. I even wrote an article that explains it: [SQL injection in update helper](https://phpdelusions.net/pdo/sql_injection_example) – Your Common Sense Dec 17 '17 at 11:44
  • @yesterday I dind't mean it to be used with user data, anyway i fixed it, thanks. – iJanki Dec 18 '17 at 13:26
1

You have three options.

The first option is to drop down to SQL. Then you can use all the features your RDBMS of choice provides. But many programmers don't want to drop down to SQL unless absolutely necessary.

The second option is to lock on a related row in another table. For instance, if the entity you're inserting has a unique key per user, you could do a lock on the user you're inserting/updating the entity for. The problem with this solution is that it doesn't work for root entities like User itself because you can't lock a row that doesn't exist yet.

The third option is to just catch the duplicate key error/exception. That is, you don't check if a row with a particular key already exists; instead, you just attempt to insert it. If it succeeds, all is good. If it fails with the duplicate key error/exception, you catch it and update the existing row. This solution is the best because it avoids an extra SELECT query before each insertion that's a constant overhead for the low probability of hitting a race condition. And it's the best because it works for both root and non-root entities.

Elnur Abdurrakhimov
  • 44,533
  • 10
  • 148
  • 133
  • 1
    Doctrine closes the entity manager on an exception, so catching it then requires recreating the entity manager, and any (other) entities that were due to be written are now detached. – Tim Lytle Jun 24 '21 at 19:06
0

In case this helps, you can extend the query builder to append arbitrary SQL (obviously, this may not work across PDO engines):

class MyQB extends QueryBuilder {

    private $append = '';

    /**
     * {@inheritdoc}
     */
    public function getSQL() {
        return parent::getSQL() . $this->append;
    }

    /**
     * Append raw SQL to the output query
     *
     * @param string $sql SQL to append. E.g. "ON DUPLICATE ..."
     *
     * @return self
     */
    public function appendSql($sql) {
        $this->append = $sql;
        return $this;
    }
}
Steve Clay
  • 8,671
  • 2
  • 42
  • 48
-2

I wrote simple solution for me. Just created AbstractRepository class which is parent class of all Repositories(for example UserRepository) and created next method:

 public function onDuplicateUpdate($insertFields, $updateFields)
    {
        $table = $this->getEntityManager()->getClassMetadata($this->getEntityName())->getTableName();
        $sql = 'INSERT INTO '.$table;
        $sql .= '(`'.implode('`,`', array_flip($insertFields)).'`) ';
        $sql .= 'VALUES("'.implode('","', $insertFields).'") ';
        $sql .= 'ON DUPLICATE KEY UPDATE ';
        foreach($updateFields as $column => $value) {
            $sql .= '`'.$column . '` = "'. $value.'"';
        }
        $stmt = $this->getEntityManager()->getConnection()->prepare($sql);
        $stmt->execute();
    }

You can use this code like this:

$this->getEntityManager()
           ->getRepository('User')
           ->onDuplicateUpdate(['column1' => 'user_reminder_1', 'column2' => 235], ['column2' => 255]);
Anton_Sh
  • 225
  • 1
  • 7
  • 16