Все примеры будут на PHP, а используемая база данных mysql. Хотя, приведенный код не привязан к какой-либо базе данных, он всего лишь определяет, какие строки должны быть удалены, какие добавлены, а какие модифицированы. Далее эти «указания» можно легко реализовать для любой БД.
Например, мы имеем простой документ с одиночными свойствами типа «идентификатор», «название», «дата создания», а так же к этому документу имеется множественное поле — таблица управления доступом в виде: код пользователя, время и дата начала разрешения доступа, и время и дата с которой доступ запрещается.
Структуру данных конкретного документа в PHP можно изобразить примерно так:
$document["id"] = "1"; // идентификатор документа $document["name"] = "Название документа"; // название документа $document["create_date"] = "25-10-2012"; // дата создания документа $document["permissions_table"] = array( array( "user_id" => 1, // Код пользователя "grant_from" => "2012-10-25 00:00:00", // Время и дата с которого доступ разрешается "grant_to" => "2012-10-27 23:59:59" // Время и дата с которого доступ запрещается ) );
Примем, что в БД такой документ будет храниться в двух таблицах:
/* - document_header (тут храним одиночные свойства, одна строка - один документ) id INT NOT NULL AUTOINCREMENT name TEXT NOT NULL create_date DATETIME NOT NULL - document_permissions (тут храним множественные свойства, один документ - много строк) id INT NOT NULL AUTOINCREMENT document_id INT NOT NULL user_id INT NOT NULL grant_from DATETIME grant_to DATETIME */
Далее представим, что возникает задача изменения таблицы доступа нашего документа. При этом после сабмита пользователем, мы получаем в обработку два массива нашего документа: массив со старыми данными и массив с новыми данными.
Теперь рассмотрим, как нам эти массивы преобразовать в последовательность SQL запросов для изменения документа в БД.
С плоскими данными все достаточно просто, их можно обработать следующим кодом
$changes = array(); foreach($old_document as $k => $v) { if($k == "permissions_table") continue; if($old_document[$k] != $new_document[$k]) $changes[$k] = $new_document[$k]; } $changes["id"] = $old_document["id"] $changes["document_id"] = $old_document["document_id"]
В итоге мы получаем массив $changes с измененными полями и их новыми значениями, который легко преобразовать в запрос базы данных UPDATE. Не хочу перегружать статью этой операцией, поэтому опустим ее.
Далее нам необходимо обработать изменения в таблице доступа к документу и произвести необходимые изменения в БД.
Тут могут случиться разные ситуации, например:
— старая строка была изменена
— была добавлена новая строка
— старая строка была удалена
— изменился порядок строк
И любые комбинации вышеуказанных операций.
При этом нам необходимо сгенерировать соответствующие запросы в БД и выполнить их обязательно в следующей последовательности:
1. Удаление лишних строк
2. Изменение существующих строк
3. Добавление новых строк
Удаление производится в первую очередь для того, чтобы не было конфликта уникальных ключей в БД при операциях добавления или модификации.
В итоге нам необходима функция, которая получает на вход массивы старых и новых значений, а на выходе дает нам три массива с линиями таблицы, которые необходимо удалить, изменить и добавить в нашу БД.
Для этой задачи я написал универсальную функцию, которая работает с любой конфигурацией табличных данных. Лишь с некоторыми ограничениями, которые указаны в конце статьи.
Итак, волшебная функция имеет следующий интерфейс:
/* @$data - массив с новыми табличными данными @$old_data - массив со старыми табличными данными @$keys - ключевые поля в строках табличных данных (необязательный, см. в конце статьи описание по настройке) @$hidden_keys - ключевые поля в таблице БД, отвечающей за наши данные (см. в конце статьи описание по настройке) @$options - опции выполнения */
На выходе мы получаем заветные массивы delete, update, insert, которые легко преобразовать в запросы БД для последующего их выполнения.
Исходный код функции:
static function generateOperationsFromMultiData($data, $old_data, $keys, $hidden_keys, $options) { $out = array("insert" => array(), "update" => array(), "delete" => array()); $unique_elements = array(); $unique_keys = array(); // Обходим все старые данные и считаем для каждой строки хеш $old_elements_hashes = array(); $old_elements_keys = array(); foreach($old_data as $k => $fields) { $res = self::__getKeyAndHashFromLine($fields, $keys, $hidden_keys); $old_data[$k]["___key"] = $res["key"]; $old_data[$k]["___hash"] = $res["hash"]; if($res["key"]) { $old_elements_hashes[$res["key"]] = $res["hash"]; $old_elements_keys[$res["key"]] = $k; } else { $old_elements_hashes[$k] = $res["hash"]; } } // Обходим все новые данные $data = array_merge($data); foreach($data as $k => $fields) { $res = self::__getKeyAndHashFromLine($fields, $keys); $data[$k]["___key"] = $res["key"]; $data[$k]["___hash"] = $res["hash"]; // Если включен флаг уникальности выкидываем неуникальные элементы if($options["unique"]) { if(in_array($res["hash"], $unique_elements)) continue; else $unique_elements[] = $res["hash"]; } if($res["key"]) { // Проверяем чтобы данные были уникальные в пределах ключа if(in_array($res["key"], $unique_keys)) continue; else $unique_keys[] = $res["key"]; // Добавляем строку если в старых данных нет такого ключа if(!isset($old_elements_hashes[$res["key"]])) $out["insert"][$k] = $fields; else { // Такой ключ существует в старых данных, сравниваем хеши данных if($res["hash"] != $old_elements_hashes[$res["key"]]) { // Добавляем к строке скрытые ключи из старых данных foreach($hidden_keys as $v) { $fields[$v] = $old_data[$old_elements_keys[$res["key"]]][$v]; } // Добавляем новые данные в массив обновления данных $out["update"][$k] = $fields; } $old_data[$old_elements_keys[$res["key"]]]["___new_key"] = $k; unset($old_elements_hashes[$res["key"]]); unset($old_elements_keys[$res["key"]]); } } else { // Если ключ не задан просто проверяем наличие хеша в старых данных if($key = array_keys($old_elements_hashes, $res["hash"])) { $key = current($key); unset($old_elements_hashes[$key]); $old_data[$key]["___new_key"] = $k; } else { // Если хеш не найден, добавляем новые данные $out["insert"][$k] = $fields; } } } // Остатки из old_data помещаем в массив для удаления if($keys) foreach($old_elements_keys as $k => $v) { unset($old_data[$v]["___key"]); unset($old_data[$v]["___hash"]); unset($old_data[$v]["___new_key"]); $out["delete"][] = $old_data[$v]; unset($old_data[$v]); } else foreach($old_elements_hashes as $k => $v) { unset($old_data[$k]["___key"]); unset($old_data[$k]["___hash"]); unset($old_data[$k]["___new_key"]); $out["delete"][] = $old_data[$k]; unset($old_data[$k]); } // Выполняем проверку порядка следования данных // Выравниваем индексы данных $old_data = array_merge($old_data); $data = array_merge($data); if($options["save_order"]) { $delete = false; // Обходим старые данные (остались только те, которые должны остаться и некоторые или все из них в update массиве) if($old_data[0]["___new_key"] != "0") $delete = true; foreach($old_data as $k => $v) { if($keys) { // Если не совпали ключи, значит с этого момента необходимо перезаписать данные if($data[$v["___new_key"]]["___key"] != $old_data[$k]["___key"]) $delete = true; if($delete) { unset($old_data[$k]["___key"]); unset($old_data[$k]["___hash"]); unset($old_data[$k]["___new_key"]); unset($data[$v["___new_key"]]["___key"]); unset($data[$v["___new_key"]]["___hash"]); $out["delete"][] = $old_data[$k]; foreach($hidden_keys as $hk) { $data[$v["___new_key"]][$hk] = $old_data[$k][$hk]; } $out["insert"][$v["___new_key"]] = $data[$v["___new_key"]]; unset($out["update"][$v["___new_key"]]); } } else { // Ключи не заданы ориентируемся по хешам // Если не совпали хеши, значит с этого момента необходимо перезаписать данные if($data[$v["___new_key"]]["___hash"] != $old_data[$k]["___hash"]) $delete = true; if($delete) { unset($old_data[$k]["___key"]); unset($old_data[$k]["___hash"]); unset($old_data[$k]["___new_key"]); unset($data[$v["___new_key"]]["___key"]); unset($data[$v["___new_key"]]["___hash"]); $out["delete"][] = $old_data[$k]; foreach($hidden_keys as $hk) { $data[$v["___new_key"]][$hk] = $old_data[$k][$hk]; } $out["insert"][$v["___new_key"]] = $data[$v["___new_key"]]; } } } } $out["update"] = array_merge($out["update"]); ksort($out["insert"]); $out["insert"] = array_merge($out["insert"]); return $out; } // Получение ключа из строки function __getKeyAndHashFromLine($line, $keys, $hide_keys = array()) { $hash = $line; // Удаляем ключи foreach($keys as $v) unset($hash[$v]); foreach($hide_keys as $v) unset($hash[$v]); // Считаем хеш строки $hash = serialize($hash); // ключ строки $key = ""; foreach($keys as $v) $key .= "__" . $line[$v]; return array("hash" => $hash, "key" => $key); }
В нашем случае вызов функции будет выглядеть следующим образом:
$result = generateOperationsFromMultiData($new_document["permissions_table"], $old_document["permissions_table"], false, array("id"), array("unique" => false));
В итоге $result будет содержать три массива: delete, update, insert.
Приведу пару примеров для наглядности.
В первом примере мы имеем две строки для двух пользователей. Эмулируя изменения, вводим третьего пользователя и удаляем второго. При этом специально дублируем строку с первым пользователем и делаем изменения в датах обоих строк для него.
$old_document["permissions_table"] = array( array( "id" => 1, "document_id" => 1, "user_id" => 1, "grant_from" => "2012-10-25 00:00:00", "grant_to" => "2012-10-27 00:00:00" ), array( "id" => 2, "document_id" => 1, "user_id" => 2, "grant_from" => "2012-10-25 00:00:00", "grant_to" => "2012-10-27 00:00:00" ) ); $new_document["permissions_table"] = array( array( "document_id" => 1, "user_id" => 3, "grant_from" => "2012-10-25 00:00:00", "grant_to" => "2012-10-27 00:00:00" ), array( "document_id" => 1, "user_id" => 1, "grant_from" => "2012-10-25 00:00:00", "grant_to" => "2012-10-03 00:00:00" ), array( "document_id" => 1, "user_id" => 1, "grant_from" => "2012-10-25 00:00:00", "grant_to" => "2012-10-31 00:00:00" ) ); generateOperationsFromMultiData( $new_document["permissions_table"], $old_document["permissions_table"], array("user_id"), array("id"), array( "unique" => false, "save_order" => false ) )
На выходе мы получим:
Array ( [insert] => Array ( [0] => Array ( [document_id] => 1 [user_id] => 3 [grant_from] => 2012-10-25 00:00:00 [grant_to] => 2012-10-27 00:00:00 ) ) [update] => Array ( [0] => Array ( [document_id] => 1 [user_id] => 1 [grant_from] => 2012-10-25 00:00:00 [grant_to] => 2012-10-03 00:00:00 [id] => 1 ) ) [delete] => Array ( [0] => Array ( [id] => 2 [document_id] => 1 [user_id] => 2 [grant_from] => 2012-10-25 00:00:00 [grant_to] => 2012-10-27 00:00:00 ) ) )
В итоге, получается, что нам надо вставить строку с третьим пользователем, удалить строку со вторым пользователем и сделать изменения в первом пользователе. При этом обратите внимание, что изменения для первого пользователя берутся из первой строки для данного пользователя в массиве $new_document[permissions_table] . Этим я хотел показать корректную обработку «задублированных данных».
Теперь рассмотрим параметры, которые управляют поведением функции.
$keys — массив ключей табличных данных. Если указаны, то функция автоматически начнет выкидывать строки с повторяющимися ключами, в вышеуказанном примере указан ключ «user_id», таким образом, мы запретили повторение строк для одного и того же пользователя. Ключи могут быть составные, для этого достаточно просто их указать в массиве данного параметра. Если ключи не задать, то функция будет по прежнему работоспособна, однако придется ввести в БД какой-нибудь id, по которому надо будет отличать одну строку от другой. Его надо будет прописать в $hidden_keys функции.
$hidden_keys — скрытые ключи, это ключи, которые могут присутствовать в массиве старых данных и отсутствовать в массиве измененных данных. При этом функция автоматически переносит эти ключи в выходные массивы удаления и изменения строк. В частности это нужно для управления множественными данными, когда строка состоит из одного или нескольких не ключевых полей. Тогда для идентификации таких строк вводится уникальный id в БД, но его не обязательно «тягать» в форму и следить за его «сохранностью» при редактировании. Функция сама определит какие строки с определенным id удалить, какие изменить, и какие строки добавить новыми.
$options[unique] — если установить этот флаг в true, то помимо уникальности ключей функция начнет проверять уникальность остальных данных строки, то есть в нашем случае при установленном флаге $options[unique] мы не смогли бы указать одни и те же периоды для разных пользователей, при чем функция оставила бы первый попавшийся период и удалила последующие повторяющиеся.
$options[save_order] — флаг, который указывает на необходимость сохранения порядка следования данных в БД, такого же как в массиве $data (в нашем примере $new_document[permissions_table]). Чтобы понять, как действует этот флаг рассмотрим результат нашего примера, но только с установленным флагом $options[save_order]:
Array ( [insert] => Array ( [0] => Array ( [document_id] => 1 [user_id] => 3 [grant_from] => 2012-10-25 00:00:00 [grant_to] => 2012-10-03 00:00:00 ) [1] => Array ( [document_id] => 1 [user_id] => 1 [grant_from] => 2012-10-25 00:00:00 [grant_to] => 2012-10-03 00:00:00 [id] => 1 ) ) [update] => Array ( ) [delete] => Array ( [0] => Array ( [id] => 2 [document_id] => 1 [user_id] => 2 [grant_from] => 2012-10-25 00:00:00 [grant_to] => 2012-10-27 00:00:00 ) [1] => Array ( [id] => 1 [document_id] => 1 [user_id] => 1 [grant_from] => 2012-10-25 00:00:00 [grant_to] => 2012-10-27 00:00:00 ) ) )
Мы видим, что сначала функция предлагает удалить все существующие записи, чтобы вставить новую запись с третьим пользователем и после нее запись с первым пользователем. Таким образом, в БД мы получим такую же последовательность при выборке без сортировки, как в нашем новом массиве. С помощью такой функциональности можно сэкономить на дополнительном поле, которое понадобилось бы нам для сортировки строк. Однако если строка вставляется в середину или в начало таблицы, придется сначала удалить все данные следующие за этой строкой, а затем их заново добавить (эти действия мы и увидели в последнем примере)
Примечание: функция всегда предлагает минимальное количество запросов к БД для достижения желаемого результата.
Ограничения: данные строк должны быть только плоскими на момент выполнения фукнции. Поля не должны называться: "___key", "___hash", "___new_key".
ссылка на оригинал статьи http://habrahabr.ru/post/156273/
Добавить комментарий