Слежение за изменениями данных в MySQL при помощи PHP

в 22:55, , рубрики: mysql, php, Веб-разработка

А что если к такой замечательной возможности MySQL как создание триггеров, способных записывать старые и новые значения данных при вставке, изменении и удалении записей добавить информацию, которой обладает php скрипт?

Триггеры MySQL знают:
* момент в который происходит изменение
* старое и новое значение

PHP знает:
* кто в данный момент залогинен
* какая страница открыта, с какой перешли
* бравзер
* IP адрес
* POST, GET
* Cookie

Каким же способом можно умудриться записать всю необходимую информацию?

Воспользуемся особенностями PHP и MySQL!

MySQL умеет создавать временные таблицы, которые живут только пока коннект к бд не закроется, а PHP как раз при открытии каждой страницы — создаёт новый коннект (в абсолютном большинстве конфигураций вебсервера и php).

Таким образом, если триггер будет писать ещё и во временную таблицу — мы сможем узнать кто или что в ответе за конкретные изменения.

Чуть подробнее — мы можем вначале страницы создавать временную таблицу, если триггер срабатывает, он записывает информацию в таблицу предназначенную для слежения за данными, а айдишники, которые возвращает last_insert_id() записывать во временную таблицу. При завершении работы обращаемся к временной таблице, и если она не пуста — записываем в таблицу изменений всё что хотим из того что знает загруженная PHP страница.

Далее — вариант реализации.

1) Начнём с временной таблицы — мы можем её создавать, когда триггер выполняется на странице в первый раз! Для этого нужно всего лишь написать CREATE TEMPORARY TABLE temp_watch_changes IF NOT EXISTS. Есть только небольшая проблема — в текущих версиях MySQL невозможно узнать существует ли временная таблица каким-либо запросом. Поэтому, она обязательно должна быть создана, чтобы не было ошибки, когда через php мы будем выбирать значения.

Мы может потрюкачить, а можем всё сделать прямолинейно.

В качестве небольшого трюка — в MySQL может одновременно существовать таблица обычная и временная с одинаковым именем. Если есть временная — то будет использоваться именно она. И каждый раз из пхп проверяя есть ли записи внутри таблицы temp_watch_changes там будет либо пусто, либо айдишники записанные триггером и никаких ошибок.

Более прямолинейный метод — просто создавать временную таблицу каждый раз при загрузке страницы. На нашем сервере это занимает 0.0008 секунд, что впринципе приемлемо :)

CREATE TEMPORARY TABLE temp_watch_changes ( id_change INTEGER NOT NULL )

2) Создаём таблицу, содержащую сами изменения

CREATE TABLE `watch_changes` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `table_name` varchar(255) DEFAULT NULL,
  `column_name` varchar(255) DEFAULT '',
  `key_name` varchar(255) DEFAULT NULL,
  `key_value` varchar(1000) DEFAULT NULL,
  `old_value` text,
  `new_value` text,
  `type` enum('insert','update','delete') DEFAULT 'update',
  `date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `ip` varchar(255) DEFAULT NULL,
  `id_user` int(11) DEFAULT '0',
  `user_email` varchar(255) DEFAULT '',
  `post` text,
  `get` text,
  `session` text,
  `server` text,
  `user_agent` varchar(1000) DEFAULT '',
  `url` text,
  `referer` text,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Выбираем MyISAM т.к. будет много инсертов.

3) Создаём триггер. Выяснить, можно ли динамически использовать названия колонок в триггерах — не удалось. Наверное, просто нельзя, но нам это не сильно и нужно. Ведь есть PHP.

function createWatchTrigger($table,$columns,$primaryKey){
		if(!is_array($primaryKey)){
			$primaryKey=array($primaryKey);
		}
		$types=array('update','insert','delete');
		foreach($types as $type){
			db::$used->internalQuery("drop trigger IF EXISTS {$table}_t_$type");

			$triggerContent="CREATE TRIGGER {$table}_t_$type
				AFTER $type ON {$table}
				FOR EACH ROW
				BEGIN
					CREATE TEMPORARY TABLE IF NOT EXISTS temp_watch_changes (
						id_change			INTEGER NOT  NULL
					);
				";
			foreach($columns as $columnTitle){
				if($type=='update'){
					$triggerContent.="
					IF NEW.{$columnTitle} != OLD.$columnTitle
					THEN ";
				}
				$triggerContent.="INSERT INTO watch_changes (table_name,column_name,old_value,new_value,type,key_name,key_value) ";
				if($type=='insert'){
					$triggerContent.="VALUES('{$table}','$columnTitle','', NEW.$columnTitle,'$type','".implode(',',$primaryKey)."',CONCAT('',NEW.".implode(",',',NEW.",$primaryKey)."));";
				}else if($type=='update'){
					$triggerContent.="VALUES('{$table}','$columnTitle',OLD.$columnTitle, NEW.$columnTitle,'$type','".implode(',',$primaryKey)."',CONCAT('',NEW.".implode(",',',NEW.",$primaryKey)."));";
				}else if($type=='delete'){
					$triggerContent.="VALUES('{$table}','$columnTitle',OLD.$columnTitle,'','$type','".implode(',',$primaryKey)."',CONCAT('',OLD.".implode(",',',OLD.",$primaryKey)."));";
				}
				$triggerContent.="
				set @last_id=last_insert_id();
				INSERT INTO temp_watch_changes (id_change) values (@last_id);";

				if($type=='update'){
					$triggerContent.="END IF;";
				}
			}
			$triggerContent.="nEND;";
			db::$used->internalQuery($triggerContent);
		}
	}

Сама функция создания могла быть по-лучше в плане читаемости. Она создаёт 3 триггера — на update, insert, delete. Принимает название таблицы, колонок за которыми следить и ключ по которому можно найти эту запись (может быть и несколько ключей).
Вызываться может так:

createWatchTrigger('employees',array('salary','job_title'),'id');

4) Теперь сделаем так, чтобы после того, как PHP страница отработает обработались данные из временной таблицы.

Воспользуемся register_shutdown_function, которая позволяет выполнить любую функцию по завершении скрипта. На любом проекте есть файл, который инклюдится всегда — туда её и разместим.

function shutdown(){
	$affectedRows=db::$used->fetchRows("select * from temp_watch_changes");
	if($affectedRows){
		if(User::isLogged()){
			$userId=User::getCurrent()->getId();
			$email=User::getCurrent()->getEmail();
		}else{
			$userId=0;
			$email='';
		}
		$updateData=array(
			'ip'=>$_SERVER['REMOTE_ADDR'],
			'id_user'=>$userId,
			'user_email'=>$email,
			'post'=>serialize($_POST),
			'get'=>serialize($_GET),
			'session'=>serialize($_SESSION),
			'server'=>serialize($_SERVER),
			'user_agent'=>$_SERVER['HTTP_USER_AGENT'],
			'url'=>serialize($_SERVER['REQUEST_URI']),
			'referer'=>$_SERVER['HTTP_REFERER']
		);
		foreach($affectedRows as $row){
			db::$used->update('watch_changes',$updateData,array('id'=>$row['id_change']));
		}
	}
}

register_shutdown_function('shutdown');

Вот и всё.

Автор: Aleks_ja

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js