Нарушение целостности Mysql / PDO Duplicate Primary – хотя ничего уже в таблице

Я уже рассмотрел ограничение первичного ключа SQL, хотя записи не существует, какой из всех вопросов на SO кажется наиболее близким к моей проблеме, но это не то же самое.

Я не сомневаюсь, что я, наверное, делаю что-то глупое, но здесь идет:

Я пытаюсь написать сценарий (в php), который будет переносить данные (без структуры, он предполагает, что структура уже выполнена) из любой базы данных PDO в любую другую данную базу данных PDO – в моем случае я тестирую ее на sqlite3 – > mysql.

Когда я запускаю скрипт в своих тестовых базах, я получаю «Нарушение ограничения целостности: 1062 Дублировать запись« 1 »для ключевого« PRIMARY », который я не совсем понимаю, потому что в таблице нет данных (даже до того, как скрипт запускается DELETE).

Я предполагаю, что это связано с тем, что основным ключом является auto_increment, но я попытался установить следующее значение приращения для чего-то другого, кроме любого вставленного значения (думаю, я попытался установить его на 80) – не имело никакого значения.

Я искал метод отключения auto_increment на время транзакции, но не изменяя таблицу перед рукой, а затем изменяя ее потом, я не могу придумать способ – и изменение всей таблицы просто кажется неправильным, и я на самом деле не хотел иметь никакого DDL.

1 <?php 2 3 $abspath = dirname(__FILE__)."/"; 4 5 $source_dsn = 'sqlite:'.$abspath.'db.sqlitedb'; 6 $source_username = null; 7 $source_password = null; 8 $target_dsn = "mysql:dbname=name;host=127.0.0.1"; 9 $target_username = "name"; 10 $target_password = "pass"; 11 12 $transfer_data = array(); 13 $table_data = array(); 14 15 try { 16 17 // connect to source 18 $source = new PDO($source_dsn, $source_username, $source_password); 19 $source->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 20 21 // connect to target 22 $target = new PDO($target_dsn, $target_username, $target_password); 23 $target->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 24 25 //TODO Generalise this statement to all database types. 26 $stmt = $source->prepare("SELECT * FROM sqlite_master WHERE type='table';"); 27 $stmt->execute(); 28 29 // get all tables 30 while($tablerow = $stmt->fetch(PDO::FETCH_ASSOC)) 31 { 32 // TODO Generalise these statements to all database types. 33 $transfer_data[$tablerow['tbl_name']] = array(); 34 $table_data[$tablerow['tbl_name']] = array(); 35 } 36 $stmt->closeCursor(); 37 38 // for each table, load data 39 foreach($transfer_data as $tablename => $void) 40 { 41 $stmt = $source->prepare("SELECT * FROM $tablename;"); 42 $stmt->execute(); 43 // load data row at a time 44 while($datarow = $stmt->fetch(PDO::FETCH_ASSOC)) 45 { 46 // store data for later 47 $transfer_data[$tablename][] = $datarow; 48 // if we haven't gained column data yet, do so now 49 if(!array_key_exists($tablename,$table_data)) 50 { 51 $t_data = array(); 52 foreach($datarow as $colname => $void) 53 { 54 $t_data[] = $colname; 55 } 56 $table_data[$tablename] = $t_data; 57 } 58 } 59 $stmt->closeCursor(); 60 echo "Read $tablename\n"; 61 } 62 63 //start a transaction (if driver supports transactions / if not then this is noop) 64 $target->beginTransaction(); 65 // for each table clear existing data and insert copied data 66 foreach($table_data as $tablename => $columns) 67 { 68 // not using an empty/truncate because mysql and possibly others autocommit 69 $stmt = $target->prepare("DELETE FROM $tablename;"); 70 $stmt->execute(); 71 $stmt->closeCursor(); 72 73 // prepare the insert statement - we don't know how many columns so is dynamic 74 $querystr = "INSERT INTO $tablename (".join(", ",$columns).") VALUES ("; 75 foreach($columns as $k => $column) 76 { 77 $columns[$k] = ':'.$column; 78 } 79 // using named placeholders so order doesn't matter 80 $querystr = $querystr.join(", ",$columns).");"; 81 $stmt = $target->prepare($querystr); 82 //echo "Using: $querystr\n"; 83 $rowcount = 0; 84 // for each row of data, bind data and execute insert statement 85 foreach($transfer_data[$tablename] as $rowdata) 86 { 87 foreach($rowdata as $rowname => $rowvalue) 88 { 89 $stmt->bindParam(':'.$rowname, $rowvalue); 90 } 91 $stmt->execute(); 92 $stmt->closeCursor(); 93 $rowcount++; 94 } 95 echo "Written $rowcount rows to $tablename\n"; 96 } 97 $target->commit(); 98 99 } 100 catch (PDOException $e) 101 { 102 echo 'PDO Error: '.get_class($e).' - '.$e->getMessage()."\n"; 103 echo 'Query String was: '.$querystr."\nData:\n"; 104 var_export($transfer_data[$tablename]); 105 if($target->inTransaction()){ 106 $target->rollBack(); 107 } 108 } 109 

Теперь у меня есть таблица в моей целевой базе данных, которая:

 +-------------------+------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------------+------------------+------+-----+---------+----------------+ | channel_id | int(11) | NO | PRI | NULL | auto_increment | | channel_parent_id | int(10) unsigned | YES | | NULL | | | server_id | int(10) unsigned | NO | MUL | NULL | | +-------------------+------------------+------+-----+---------+----------------+ 

Выход:

 PDO Error: PDOException - SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1' for key 'PRIMARY' Query String was: INSERT INTO channels (channel_id, channel_parent_id, server_id) VALUES (:channel_id, :channel_parent_id, :server_id); Data: array ( 0 => array ( 'channel_id' => '1', 'channel_parent_id' => '0', 'server_id' => '1', ), 1 => array ( 'channel_id' => '24', 'channel_parent_id' => '0', 'server_id' => '1', ), 2 => array ( 'channel_id' => '34', 'channel_parent_id' => '0', 'server_id' => '1', ), 

Рад, что вы это решили. Однако это предназначено для устранения причины, по которой bindParam() не работает для вас. Это не ошибка, она работает таким образом по дизайну.

Согласно документам :

Привязывает переменную PHP к соответствующему заполнителю имени или вопроса в выражении SQL, который использовался для подготовки оператора. В отличие от PDOStatement :: bindValue () переменная привязана как ссылка и будет оцениваться только в момент вызова PDOStatement :: execute () .

(акцент мой)

Учитывая вышеизложенное, это:

  87 foreach($rowdata as $rowname => $rowvalue) 88 { 89 $stmt->bindParam(':'.$rowname, $rowvalue); 90 } 

… будет привязывать каждый параметр к $rowvalue по ссылке, которая во время выполнения запроса всегда будет 1 (последний элемент $rowdata )

Способ заставить его работать с помощью bindParam() будет выглядеть примерно так:

  87 foreach($rowdata as $rowname => $rowvalue) 88 { 89 $stmt->bindParam(':'.$rowname, $rowdata[$rowname]); 90 } 

… или, может быть, даже:

  87 foreach($rowdata as $rowname => &$rowvalue) 88 { 89 $stmt->bindParam(':'.$rowname, $rowvalue); 90 } 

… так что каждый параметр будет ссылаться на соответствующий элемент массива.

Альтернативой, как указано выше, будет bindValue() которая связывает параметры по значению, а не по ссылке. Это означает, что параметр будет оцениваться в момент, когда bindValue() вызывается, а не в то время, когда это действительно необходимо (т.е. выполнение запроса):

  87 foreach($rowdata as $rowname => $rowvalue) 88 { 89 $stmt->bindValue(':'.$rowname, $rowvalue); 90 } 

Конечно, другой альтернативой является подача execute() с массивом параметров, который позволяет execute() разрешать привязывающую часть (таким образом, мой личный фаворит!) .

Распечатайте данные, когда вы пытаетесь вставить их в цикл. Если вы находитесь в транзакции и попытаетесь вставить 2 записи с тем же самым первичным ключом в той же транзакции, вы получите ошибку на втором, и таблица останется пустой, если вы откажетесь.

В MySQL нет необходимости отключать auto_increment. Явные значения pk для вставок должны просто увеличить счетчик id для следующей вставки, которая не является явной.

Я уверен, что установка значения вставки для поля auto_increment завершится неудачно, если значение, которое вы пытаетесь вручную вставить, совпадает с номером, в котором активируется автоматическое увеличение. Вы можете игнорировать поле auto_increment при вставке, кроме как получить last_insert_id ()

Это auto_increment по какой-то причине ;-). Пусть MySQL установит его.