Доктрина: ОБ ДУБЛИКАТЕ КЛЮЧЕВОЕ ОБНОВЛЕНИЕ

Как я могу написать INSERT запрос доктрины с опцией ON DUPLICATE KEY UPDATE?

9 ответов

Решение

Проблема в том, что это специфическая проблема MySQL, поэтому она не будет напрямую рассмотрена Doctrine.

Как уже упоминалось, вам нужно написать запрос RawSQL для этого. Это был бы самый простой способ.

Если вы хотите, чтобы он был более сложным и действительно независимым от БД, посмотрите на События и его возможности. Перед выполнением фактического запроса вы можете проверить существование и, если он существует, действовать соответствующим образом.

Независимый от ORM/PHP способ - написать хранимую процедуру / триггер, который обрабатывает эту проблемную базу данных.

Для Symfony 2 используйте 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();

Ты не можешь Это не поддерживается Доктриной прямо сейчас.

Что вы можете сделать, это подражать тому, что делает MySQL, проверив, существует ли сущность, и соответственно обновить / создать ее:

$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);
  }
});

Если запись существует PESSIMISTIC_WRITE гарантирует, что он не обновляется никем (например, другими потоками), пока мы его обновляем.

Хотя вам нужно проверять существование сущности при каждом обновлении, это просто воспроизведение "обновите, если существует, и создайте, если нет".

Как указано в комментариях, это не предотвращает состояние гонки, если запись не существует: если строка с тем же ключом (ключами) вставляется между выбором и вставкой, в которую вы запускаете, в исключение дублирующего ключа.

Но учитывая ограничения, что это должно быть независимым от БД и, следовательно, написанным с использованием Doctrine и не использующим собственный SQL, это может помочь в некоторых случаях.

Рекомендации:

Вы можете использовать функцию, подобную этой, чтобы создать и выполнить 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();
}

У вас есть три варианта.

Первый вариант - перейти на SQL. Затем вы можете использовать все функции, которые предоставляет ваша СУБД. Но многие программисты не хотят переходить на SQL без крайней необходимости.

Второй вариант заключается в блокировке связанной строки в другой таблице. Например, если сущность, которую вы вставляете, имеет уникальный ключ для каждого пользователя, вы можете заблокировать пользователя, для которого вы вставляете / обновляете сущность. Проблема с этим решением в том, что оно не работает для корневых объектов, таких как User сам, потому что вы не можете заблокировать строку, которая еще не существует.

Третий вариант - просто поймать ошибку / исключение дубликата ключа. То есть вы не проверяете, существует ли уже строка с определенным ключом; вместо этого вы просто пытаетесь вставить его. Если это удастся, все хорошо. Если произойдет сбой с ошибкой / исключением повторяющегося ключа, вы перехватываете его и обновляете существующую строку. Это решение является лучшим, потому что оно избегает лишних SELECT Запрос перед каждой вставкой - это постоянные издержки для низкой вероятности попадания в состояние гонки. И это лучше, потому что он работает как для корневых, так и для некорневых объектов.

У меня была та же проблема, и после небольшого расследования похоже, что Доктрина не делает этого. Моим решением было сделать findBy перед моей вставкой, чтобы увидеть, существуют ли какие-либо записи с уникальными полями. Если это возвращает сущность, то я обновляю эту сущность и сохраняю ее вместо создания новой сущности для сохранения.

Если вас беспокоит производительность, то это не идеально, так как мы делаем выбор перед каждой вставкой. Однако, поскольку Doctrine не зависит от базы данных, это единственная альтернатива блокированию себя для MySQL. Это один из тех компромиссов: хотите ли вы производительность или мобильность.

Я создал доктрину dbal wrapper для этого. Может использоваться с DoctrineBundle с опцией dbal wrapper_class.

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

В случае, если это поможет, вы можете расширить конструктор запросов для добавления произвольного SQL (очевидно, это может не работать на ядрах PDO):

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;
    }
}

Я написал простое решение для меня. Просто создал класс AbstractRepository, который является родительским классом для всех репозиториев (например, UserRepository), и создал следующий метод:

 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();
    }

Вы можете использовать этот код следующим образом:

$this->getEntityManager()
           ->getRepository('User')
           ->onDuplicateUpdate(['column1' => 'user_reminder_1', 'column2' => 235], ['column2' => 255]);
Другие вопросы по тегам