LINUX.ORG.RU

История изменений сообщений как на ЛОРе

 ,


1

1

Вещь очень крутая, даже если не используется явно, кмк, в любой движок сайта должна закладываться такая фича. Чтобы любой элемент сайта имел историю изменений, как на ЛОРе: любой пост можно редактировать и всё сохраняется. Да чего там, взгляните на опеннет: даже там любой анонимус может исправить новость! Ящитаю это «маст хэв» фичей.

Мне захотелось поиграться и вместо банальных post_title, post_content реализовать более динамичное содержимое SQL-таблиц, в которых хранится вся история изменений, будь то комментариев, постов и т.д.

Хочу поделиться идеей реализации и спросить ваших советов, о мудрецы всея ЛОРа! Спасибо заранее. :)

Значит, ключевая табличка blog_posts — тут храним посты в ЖЖ, всё как обычно. Однако, как вы могли заметить, здесь отсутствуют привычные нам post_title, post_content и прочие «как у всех» поля, потому что в blog_posts будут хранится лишь мета-данные о постах, вроде, «доступен ли пост для чтения» и всё в таком роде. в post_object можно хранить serialized-массив с такими мета-данными. Лишние поля не нужны.

CREATE TABLE 'blog_posts'
(
	'post_id' INTEGER PRIMARY KEY,
	'post_object' TEXT
);

В табличке revision_posts хранится история изменений всех постов в ЖЖ. Привязка по post_id, затем id самой ревизии, далее отсылка на text_id — сам текст тоже храним в отдельной табличке, комментарий для описания коммита, и информация об авторе, конечно же, который создал этот «коммит», внеся изменения.

CREATE TABLE 'revision_posts'
(
	'post_id' INTEGER,
	'id' INTEGER PRIMARY KEY,
	'date' DATETIME DEFAULT CURRENT_TIMESTAMP,
	'text_id' INTEGER,
	'text_comment' NVARCHAR(255),
	'text_length' INTEGER DEFAULT 0,
	'author_id' INTEGER,
	'author_object' NVARCHAR(255),
	'author_ip' NVARCHAR(45),
	'author_agent' NVARCHAR(255)
);

Отдельно от всех мета-данных хранится уже сам текст поста в ЖЖ, каждая новая ревизия ссылается на новый изменённый текст, и старый текст никуда не девается. В поле text хранится сырой текст, который пользователь создал, text_filtered предназначен для хранения HTML-варианта, образованного из обработки сырого text, а text_flags это какие-нибудь опции, например, можно хранить сжатый gzip-текст, и указывать в text_flags что он был пожат.

CREATE TABLE 'text_posts'
(
	'text_id' INTEGER PRIMARY KEY,
	'text' TEXT,
	'text_filtered' TEXT,
	'text_flags' NVARCHAR(255)
);

Как вы знаете, пост в ЖЖ это не только сам текст, это ещё и заголовок, а ещё у поста есть тэги и прочая-прочая-прочая. Где это?

Решение простое — храним вместе с текстом, здесь же.

Как? В формате HTTP-заголовков, лол. :)

Date: дата создания
Tags: тэг.
	ещё один тэг, на новой строке.
	HTTP разрешает переносы строк,
	если ставить таб в начале.
Title: Заголовок поста

После пустой строки идёт содержание поста.

Храним целиком все дополнительные данные о посте вместе с его текстом, преобразуя эти данные в формат HTTP-заголовков!

Почему мне так захотелось? А прост. Для простоты измерения изменений.

1) HTTP-заголовки имеют простой формат, понятный человеку, любой дурак сможет отредактировать сырой текст поста, не говоря уже о том, что будет веб-интерфейс.

2) Когда мы захотим сравнить две ревизии, два коммита, — мы сравним не только текст поста, а ещё и все мета-данные! Вдруг, изменился заголовок, были добавлены/удалены тэги, и т.п.

Все данные в одном месте — при сравнении ревизий все изменения будут выявлены.

Опционально, разумеется, можно создать поле post_title в табличке blog_posts и дублировать всегда актуальный заголовок там, просто для удобства обращения к информации, потому что он нам будет часто нужен, например.

Тэги тоже дублировать в другое место, чтобы сделать более эффективную выборку по тэгам. Это понятно.

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

Ну так вот, значит, для записи у нас есть данные $_POST['title'], $_POST['content'], $_POST['tags'], угу?

Нужно часть данных сохранить в заголовках, а текст оставить «как есть».

$headers = array(
        'Date' => date(DATE_RFC2822),
        'Tags' => http_header_tabbed($_POST['tags']),
        'Title' => http_header_tabbed($_POST['title'])
);
$content = $_POST['content'];

$text = http_headers_from_array($headers) . $content;

Готовый $text сохраняем в БД, всё просто.

Я вам своего кода принёс, братишки!

Функция http_header_tabbed ($multiline_text) исправляет переносы строк для HTTP-заголовков, расставляя в начале каждой новой строки TAB.

Функция http_headers_to_array ($raw_headers) преобразует сырой текст HTTP-заголовков в массив данных.

Функция http_headers_from_array ($php_array) соответственно наоборот (не тестировал, юзайте на страх и риск).

<?php
function http_headers_to_array($raw_headers) {
	if ($pos = strpos($raw_headers, "\r\n\r\n")) {
		$raw_headers = substr($raw_headers, 0, $pos + 4);
	}
	$headers = array();
	$header = '';
	foreach(explode("\r\n", $raw_headers) as $i => $line) {
		$key = strstr($line, ':', true);
		$value = substr(strstr($line, ':'), 2);
		if (isset($header) && substr($line, 0, 1) == "\t") {
			$headers[$header] .= "\r\n\t" . trim($line);
		}
		elseif (isset($value)) {
			$header = $key;
			if (isset($headers[$key])) {
				if (is_array($headers[$key])) {
					$headers[$key] = array_merge($headers[$key], array($value));
				}
				else {
					$headers[$key] = array_merge(array($headers[$key]), array($value));
				}
			}
			else {
				$headers[$key] = $value;
			}
		}
		else {
			$headers[0] = $key;
		}
	}
	return $headers;
}
function http_headers_from_array($php_array, $force_header = null) {
	$headers = '';
	foreach ($php_array as $key => $value) {
		if (isset($force_header)) {
			$key = $force_header;
		}
		if (is_array($value)) {
			$headers .= substr(http_headers_from_array($value, $key), -2);
		}
		else {
			$headers .= ucwords(trim($key, ':'), '-') . ': ';
			$headers .= http_header_tabbed($value) . "\r\n";
		}
	}
	return $headers . "\r\n";
}
function http_header_tabbed($multiline_text) {
	return str_replace(array("\r\n", "\r", "\n"), "\r\n\t", $multiline_text);
}
?>

Таким образом, мы преобразовали текст поста и все мета-данные к нему, типа заголовка, тэгов, даты — в удобный читаемый формат, который очень наглядно сравнивать!

Теперь самое интересное.

Вот имеем мы все эти данные, разбросанные по трём разным таблицам, надо это дело как-то склеить, да?

В SQL я лох, чего скрывать, все это знают, поэтому написал такой стрёмный SQL-запрос, чтобы данные склеивались, и если вы поможете его оптимизировать — буду благодарен!

-- нам нужен пост
SELECT * FROM blog_posts

-- берём самую первую ревизию поста
INNER JOIN revision_posts AS r_init ON
	(
		r_init.post_id = blog_posts.post_id AND r_init.id =
		(
		SELECT id FROM revision_posts WHERE post_id = blog_posts.post_id ORDER BY id ASC LIMIT 1
		)
	)

-- и берём самую последнюю ревизию поста
INNER JOIN revision_posts AS r_last ON
	(
		r_last.post_id = blog_posts.post_id AND r_last.id =
		(
		SELECT id FROM revision_posts WHERE post_id = blog_posts.post_id ORDER BY id DESC LIMIT 1
		)
	)

-- берём самый последний текст поста, это последняя ревизия
INNER JOIN text_posts ON text_posts.text_id = r_last.text_id

-- автор поста тот, кто создал первую ревизию
LEFT JOIN user ON user.user_id = r_init.author_id

-- ищем такой-то пост
WHERE post_id = :post_id

Такие дела.

Дабы не быть многословным... Я уже.

Я уже переделал свой ЖЖ под новый формат данных, который описал.

blog_posts, revision_posts, text_posts, text_posts изнутри

Блог, все комментарии в нём уже хранятся, добавляются и выводятся с учётом истории изменений! Дело тривиальное теперь, прикрутить функционал, нарисовать дополнительный интерфейс для редактирования всего и вся любыми анонимусам. И будет совсем как на ЛОРе! Так вот.

Очень хочется услышать ваших рекомендаций. Вдоль не предлагать.

★★★★★

всю простыню не читал, не проще хранить диффы и последнюю полную версию текста?

Harald ★★★★★ ()

На середине поста мне стало страшно и я проскипал до «В формате HTTP-заголовков, лол. :)», после чего упал в обморок.

Deleted ()
Ответ на: комментарий от Harald

с диффами идея заманчивая, чтобы экономить место, да.. надо бы попробовать. а то сейчас хранится весь текст целиком.

спасибо за совет! а чем можно сделать дифф в пхп, не подскажете?

и ещё вопрос. это же, при сравнении двух старых ревизий — придётся накладывать диффы один за другим? чтобы сравнить два итоговых результата. а это очень ресурсоёмко получается! м?

Spoofing ★★★★★ ()
Последнее исправление: Spoofing (всего исправлений: 2)

зачем в sql кавычки?

if (isset($header) && substr($line, 0, 1) == «\t») {
$headers[$header] .= «\r\n\t» . trim($line);
}
elseif (isset($value)) {
$header = $key;
if (isset($headers[$key])) {
if (is_array($headers[$key])) {
$headers[$key] = array_merge($headers[$key], array($value));
}
else {
$headers[$key] = array_merge(array($headers[$key]), array($value));
}
}
else {
$headers[$key] = $value;
}
}
else {
$headers[0] = $key;
}
}

Это вообще легально?

Erfinder ()

Под такую задачу лучше подходит архитектура Event Sourcing + CQRS, предложенная тобой реализация больно костыльной становится на сложных схемах БД.

maxcom ★★★★★ ()
Ответ на: комментарий от Spoofing

а чем можно сделать дифф в пхп, не подскажете?

да хотя бы system(«diff ...»)

и ещё вопрос. это же, при сравнении двух старых ревизий — придётся накладывать диффы один за другим? чтобы сравнить два итоговых результата. а это очень ресурсоёмко получается! м?

да, но и историю изменений запрашивают гораздо реже, чем последнюю версию текста

Harald ★★★★★ ()

Очень запутано, непонятно и ненужно.

Бери 2 таблицы - пиши во вторую все изменения записей в первой - профит.

VictimOfLoveToLinux ()
Ответ на: комментарий от Harald

Сэр, а как же костыльно-ориентированное программирование?

Valkeru ★★★★ ()

Ты не тут спрашивай, ты сходи посмотри как это на лоре/в вики/любой кмс сделано.

zz ★★★★ ()

Значит, ключевая табличка blog_posts — тут храним посты в ЖЖ, всё как обычно. Однако, как вы могли заметить, здесь отсутствуют привычные нам post_title, post_content и прочие «как у всех» поля, потому что в blog_posts будут хранится лишь мета-данные о постах, вроде, «доступен ли пост для чтения» и всё в таком роде. в post_object можно хранить serialized-массив с такими мета-данными. Лишние поля не нужны.

Значит, ключевая деталь в моём велосипеде - колёса. С их помощью вся конструкция может катиться. Однако, как вы могли заметить у них отсутствуют привычные нам шины, протекторы и прочие «как у всех» навороты. Всё это можно будет хранить в рюкзаке, на спине у велосипедиста. Лишние детали не нужны.

crutch_master ★★★★★ ()

В SQL я лох

Это заметно еще на фазе сознания структуры БД.

crutch_master ★★★★★ ()

А ведь Я еще помню времена, когда спуф был простым сторожем.

/смахнул скупую мужскую слезу

gorgoot ()
Ответ на: комментарий от gorgoot

А ведь Я еще помню времена, когда спуф был простым сторожем.

Его уволили, что ли?

theNamelessOne ★★★★★ ()
Ответ на: комментарий от theNamelessOne

Оказался профнепригоден, скатился, пробил дно, теперь пишет на php

anonymous ()
Ответ на: комментарий от anonymous

теперь пишет на php

Так он же не ради денег, а для души™, т. е. для своего бложика.

theNamelessOne ★★★★★ ()
Последнее исправление: theNamelessOne (всего исправлений: 1)

любой пост можно редактировать и всё сохраняется

Чет не понял. А что где-нибудь есть не так?

filequest ()
Ответ на: комментарий от filequest

в том же wordpress, поля типа post_content, содержание поста — захардконы в таблицу wp_blog_posts.

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

Spoofing ★★★★★ ()
Ответ на: комментарий от Spoofing

а, ты про историю изменений. извини, не так понял. Да, че то мне кажется, не особо это и нужно. Только место на диске лишнее занимать. Часто что-ли откатывать приходится?

filequest ()
Ответ на: комментарий от Ghostwolf

ммм. значит моя информация устарела ;(

извиняйте.

Spoofing ★★★★★ ()
Ответ на: комментарий от filequest

делаю «чтоб было» :)

на ЛОРе такое есть, а я чем хуже? вот.

Spoofing ★★★★★ ()

Лол. Спуффи целую статью на лоре накатал :D

b0c0813f ()

Спуф! А давай стартап замутим?)

dk- ()

Вот я понимаю что не по теме, но все же задумайся. Не лучше ли вместо того чтобы лепить с нуля свой велосипед из говна(пхп) выучить ноду или RoR и коммитить в какой-нибудь приличный форумный движок? (nodeBB, discourse)

Zaskard ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.