На сайте есть несколько типов контента. Например, Обычные страницы, Записи блога, Рецензии и др. У каждого типа контента есть своя Таблица в базе (плюс свои Модель+Контроллер).
Нужно реализовать возможность добавления комментариев к этим страницам. Но писать для каждого типа свою таблицу и свзяку вида КомментарииСтраницы или КомментарииЗаписи_в_блоге не хочется.
Хочется реализовать единую таблицу в БД вида КомментарииКонтент:
ID коммента
ID контроллера (модели, контента)
ID материала
Которая хранила бы идентификатор коммента в таблице Комментарии и идентификатор типа контента и id материала в нем. Т.е. для комментариев у нас используются 2 таблицы: Комментарии и КомментарииКонтент. Первая хранит сами комментарии, а вторая принадлежность комментариев к типам контента.
Возможно ли сделать подобное стандартными средствами Yii с прописыванием Relations у каждого типа материала?
Что значит возможно ли? Структура БД от Yii некак не зависит. Создавайте структуру, создавайте модели и связывайте. В Relations тут ничего сложного не встретите.
Это очень просто делается стандартными средствами Yii. Здесь дело в правильном проектировании такой БД. То как вам хочется это спроектировать - неправильно! Советую посмотреть как это сделано в Drupal. Есть общая для всего контента таблица content, она содержит основные метаданные любого материала, будь то обычные страницы, статьи, продукты магазина и прочее. Остальные таблицы, типа product, article связаны с ней отношением один к одному. Есть таблица comment, которая связана с content отношением много к одному. Так как все идентификаторы раздает таблица content никаких лишних полей в виде ID контроллера в таблице комментариев не требуется.
Исходную задачу хотел решить создав базовую модель Node с общими полями т.е. id, title, body, created, updated, authorId и.т.д. а далее все модели контента наследовать от Node с добавление relation к модели со специфическими полями. Но мне показалось это слишком жестко для недорогих хостингов т.к. за такую гибкость приходиться платить запросами с кучей join ов либо просто кучей запросов)).
Поэтому сделал так. имеем табличку NodeId(id, nodeType), где id - autoinc. В таблице Node убираем автоинкремент с id и добавляем триггер на вставку insert into NodeId SET nodeType=NEW.nodeType; SET NEW.id=LAST_INSERT_ID; И так для всех таблиц с материалами. И теперь все материалы сайта наследуем от Node с указанием своих собственных таблиц(у которых есть все базовые поля+свои дополнительные). Итого в разных табл. имеем уникальный идентификатор материала. Поэтому просто связываем и теги и комментарии и т.п.
P.S. сейчас обдумываю насколько правильная такая реализация…
Да, это решение в принципе подходит. Остается понять насколько это оптимально в плане нагрузки на базу (о чем и писал puritania). Правильно ли я понимаю, чтобы извлечь N статей из content, то мы в запросе должны сделать JOIN на ‘подгрузку’ данных из article?
БД в Drupal не верх совершенства, но она как минимум правильно спроектирована.
to mcast
Не понимаю вашего беспокойства по поводу нагрузки. Да, таблица content будет большая, но нужно учесть что абсолютно все поля в ней проиндексированы. Что касается кучи join’ов, то “куча” - понятие относительное. Если хорошо подумать, то будет всего 1 join, что совсем не страшно, особенно если учесть, что связь будет по индексированному полю contentID.
Приведу пример как будет выглядеть запрос к любому материалу:
Отсюда видно, что у всех конкретных типов контента в модели будет следующий relations()
class Post extends CActiveRecord()
{
...
function relations()
{
return array(
'content'=>array(self::BELONGS_TO,'Content','contentID'),
);
}
}
Что касается модели Content, то есть связь comments с отношением типа self::HAS_MANY
Как видите, все совершенно наоборот.
to puritania
Это и была плохая мысль, которая загубила хорошую идею и вызвала напрасные опасения по поводу кучи join’ов и нагрузки. Нужно было думать о добавлении relation на Node в модель со специфичискими полями, а не о засер…нии Node кучей ненужных relations.
Неоправданный оверкодинг и потеря переностимости БД. Я думаю, что времени на то, чтобы осознать "правильность" такой реализации много не надо. Ключевые слова "триггер" и левая таблица NodeId, потеря переностимости. Нужны ли такие усложнения для элементарной задачи по проектированию?
Оверкодинга абсолютно никакого - только сложность поддерживать одинаковые определения общих полей в в таблицах контента, зато избавляемся от одного join’a. Со словом триггер знаком нынче таже MySql - я думаю это не страшно.
Разница в том, что когда мне нужно добавить в систему новый вид контента, я не полезу в модель Nodes править метод relations(). Представьте себе, что у вас CMS, которая позволяет добавлять модули в систему. Эта процедура должна будет править relations() в Nodes. А это как то идеологически неправильно. Почему? Вы же наверное в курсе про инверсию зависимостей? (Мартин Фаулер, Разработка корпоративных приложений). Другое дело, если бы в Yii были динамические связи(CActiveRecord::addRelation()), но их пока к сожалению нет.
Понимаете, оверкодинга не может быть чуть чуть или много , он или есть, или его нет. Страшен не тригер, страшна его непереносимость. К тому же вы забываете про 1 лишнюю таблицу. И ещё… сколько телодвижений вам нужно будет совершить, если вдруг разонравится имя таблицы NodeId? Я в свою очередь поправлю лишь tableName() в модели.
Есть еще один вопрос по поводу таблицы content. Предположим, я хочу получить все записи (любых типов контента), начиная с определенной даты. Я делаю запрос к content. Как мне подгружать данные из таблиц типов контента? Т.е. в таблице content мы должны в таком случае хранить еще Id материалов и ‘название’ таблиц типов контента?
UPD:
Хранить нам наверное нужно тогда только ‘название’ таблиц типов контента…
Исходную ноду я партачить и не собирался. Напр. то что в моей задаче (агенство недвиж.) примерно так:
Имеем модель Node(id, title, body) на таблице Node,
class NodeRealty extends Node{
//нода недвижимости title как адрес, body как описание
public function relations(){
return array_merge(parent::relations(), array(
'realty'=>array(self::HAS_ONE,'Realty', 'nodeId'),
));
}
public function defaultScope(){
return array(
'condition'=>'nodeRealty LIKE :nodeType',
'params'=>array(
'nodeType'=>get_class($this).'%',//ей безразницы что там или NodeRealtyRoom или NodeRealtyApartament и т.д. <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/mellow.gif' class='bbc_emoticon' alt=':mellow:' />
));
}
}
class Realty extends CActiveRecord{
//модель всей недвижимости
//таблица Realty (nodeId, rooms, areaTotal, areaKitchen, areaLive, price)
// знает только про общме поля для недвиж: rooms, areaTotal, price
public function tableName(){
return 'Realty';
}
}
class NodeRealtyRoom extends NodeRealty{
//нода комнаты
public function relations(){
return array_merge(parent::relations(), array(
'realty'=>array(self::HAS_ONE,'RealtyRoom', 'nodeId'),
));
}
}
class RealtyRoom extends Realty{
//эта модель Комнаты знает еще поля rooms, areaTotal, price
}
class NodeRealtyApartamnent extends NodeRealty{
//Нода квартиры
public function relations(){
return array_merge(parent::relations(), array(
'realty'=>array(self::HAS_ONE,'RealtyApartament', 'nodeId'),
));
}
}
class RealtyApartament extends Realty{
//эта модель Квартиры знает еще поля rooms, areaTotal, areaKitchen, areaLive, price
}
Создаем всю логику в контроллере NodeRealtyController у которого определено public $realtyModel=’’;
И наследуем от него NodeRealtyRoomController и NodeRealtyApartamentController, в которых из всего кода присутствует только инициализация $realtyModel=‘RealtyRoom’ и $realtyModel=‘RealtyApartament’ соотв.
теперь:
все объекты недвижимости /nodeRealty/list
все комнаты /nodeRealtyRoom/list
все квартиры /nodeRealtyApartament/list
Как эту беду упростить? только способом про который писал выше, тогды избавляемся от моделей типа NodeRealty, NodeRoom и т.ю. остаются только RealtyApartament, RealtyRoom
Непереносимость куда?
Лишняя таблица - нестрашно, при изменении имени таблицы - просто пересоздаем триггер: один текст для всех таблиц конетента.
В общем списке получиться получить только общие свойства контента из таблицы content а вот сслыки на просмотр например можно формировать из значений поля типа контента. Если в нем хранить имя модели напр. News, то ссылку неа просмотр можно получить как
Че-то я запутался немного Точнее, как сделать более правильно, чтобы потом не переделывать, когда понадобится что-то изменить или добавлять.
Предположим, что у нас есть модели Content и Post. И, соответственно, таблицы в базе: Content и Post. И, например, я создаю пост. Тогда в экшенсе PostController’а actionCreate я делаю:
$post = new Post;
$content = new Content;
$content->attributes = $_POST[‘Post’]; //
$post->attributes = $_POST[‘Post’];
В модели Content прописаны свои safeAttributes, а у Post свои.
Забудьте про это, в Yii это ничего не даст. Вы где нибудь в доке или в примерах видели чтобы одна модель наследовалась от другой? Все модели наследуются от CActiveRecord. По крайней мере сейчас это задумано так. В django ActiveRecord покруче, но и тут позже все будет.
Приведу модели Content и Post, чтобы все было понятно.
class Post extends CActiveRecord
{
...
function relations()
{
return array(
'content'=>array(self::BELONGS_TO,'Content','contentID'),
);
}
}
Что касается описания модели Content, то там по вкусу. Ввиду того, что там хранятся метаданные общие для всего контента, я там обычно храню флаги createTime, updateTime, isPublished, isCommentable, authorID, а в relations() связь с моделью User и Comment. Все больше модель Content никогда не трогается, разве что добавляются различные Behaviors, наподобие AutoTimestampBehavior.
Вот тут вы немножко ошиблись, Post не HAS_ONE Content, а BELONGS_TO Content, как видно из моего описания выше. Вообще любой контент по факту в relations() имеет self::BELONGS_TO связь с таблицей Content.
Что касается использования, то вот как я вижу оптимальное использование такой схемы:
public function actionCreate()
{
$article=new Article;
if(isset($_POST['Article']))
{
$article->attributes=$_POST['Article'];
$content=new Content;
$content->type='Article'; //можно обойтись без этого поля
$content->authorID=Yii::app()->user->id;
$content->isCommentable=true;
$valid=$article->validate();
$valid=$content->validate() && $valid;
if($valid)
{
$content->save(false);
$article->id=$content->id;
$article->save(false);
$this->redirect(array('preview','id'=>$article->id));
}
}
$this->render('create',compact('article'));
}
to puritania
ИМХО в Yii пока так делать не стоит. Смысла нет. Зачем вы это делаете?
Вначале в MSSQL, затем в Oracle, потом в Postgres, SQLite и обратно в MySQL. Это конечно надумано. Но тригер для такой элементарной задачи не нужен и не стоит таких потерь.
ИМХО уж если использовать ORM, то сохранением всех преимуществ, которые она дает.
Я конечно могу ошибаться, но для такой задачи могла бы здорово подойти парадигма проектирования БД под названием E-A-V (Entity-Attribute-Value). Хотя она довольно спорная и в некоторых кругах считается антипаттерном, в случае сложных объектов наподобие объектов недвижимости она позволит делать многое без создания сотен таблиц. При этом довольно быстро работает и может осуществлять параметрический поиск разных объектов по любым критериям одним SQL запросом. Например найти дом и 2-ух комнатные квартиры общей площадью 60кв.м. стоимостью в промежутке от 10000 до 20000 евро.
Смысл в том, что в NodeRealty relation ‘realty’ отображается на общее понятие Realty, а в NodeRealtyRoom ‘realty’ отображается уже на ‘RealtyRoom’ и NodeRealtyApartament на ‘RealtyApartament’.
надо над этим подумать, тогда возможно получиться указывать дополнительные поля в зависимости от категории к которым относится объект. Но и текущий вариант решает все одной таблицей, в которой собраны все нужные поля, а модели уже используют те, про которые они "знают".
Задача понятна, но наследование моделей тут вредно с той позиции, что оно даже заставляет совершать больше телодвижений: перекрытие tableName(), установка правильного ‘realty’.
Это замечательно, но E-A-V + соответствующий Behavior позволяет фантастически прозрачно управлять дополнительными полями не описанными в моделях и получаемыми не на основании ‘SHOW CRATE TABLE’.