Вопрос по записи данных в разные таблицы БД и ORM

Пробую написать плагин вопросов. Делаю по аналогии с топиками. В БД создаю две таблицы для вопросов:

CREATE TABLE IF NOT EXISTS `prefix_question` (
  `question_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) unsigned NOT NULL,
  `question_title` varchar(200) NOT NULL,
  `question_tags` varchar(250) NOT NULL COMMENT 'tags separated by a comma',
  `question_date_add` datetime NOT NULL,
  `question_date_edit` datetime DEFAULT NULL,
  `question_user_ip` varchar(20) NOT NULL,
  `question_publish_index` tinyint(1) NOT NULL DEFAULT '0',
  `question_count_read` int(11) unsigned NOT NULL DEFAULT '0',
  `question_cut_text` varchar(100) DEFAULT NULL,
  `question_text_hash` varchar(32) NOT NULL,
  PRIMARY KEY (`question_id`),
  KEY `user_id` (`user_id`),
  KEY `question_date_add` (`question_date_add`),
  KEY `question_text_hash` (`question_text_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;


CREATE TABLE IF NOT EXISTS `prefix_question_content` (
  `question_id` int(11) unsigned NOT NULL,
  `question_text` longtext NOT NULL,
  `question_text_short` text NOT NULL,
  `question_text_source` longtext NOT NULL,
  `question_extra` text NOT NULL,
  PRIMARY KEY (`question_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


В ActionQuestions.class.php заполняю их:

$oQuestion=Engine::GetEntity('PluginQuestions_Question');
		$oQuestion->_setValidateScenario('question');
		/**
		 * Заполняем поля для валидации
		 */
		$oQuestion->setTitle(strip_tags(getRequestStr('question_title')));
		$oQuestion->setTextSource(getRequestStr('question_text'));
		$oQuestion->setTags(getRequestStr('question_tags'));
		$oQuestion->setUserId($this->oUserCurrent->getId());
		$oQuestion->setDateAdd(date("Y-m-d H:i:s"));
		$oQuestion->setUserIp(func_getIp());


В итоге получаю ошибку: SQL Error: Unknown column 'text_source' in 'field list'.

На сколько я понимаю, данная ошибка появляется потому что мы пытаемся найти столбец text_source в таблице prefix_question, тогда как он расположен в таблице prefix_question_content. И, если правильно понял, за то, откуда считывать/записывать данные отвечает:

$oQuestion=Engine::GetEntity('PluginQuestions_Question');


Вопрос: правильно ли я все понял и как в данном случае лучше поступить: объединить таблицы в одну или есть способ указать в какую и что писать?

38 комментариев

avatar
Надо записать так (плюс создать соответсвующую сущность):
oQuestion=Engine::GetEntity('PluginQuestions_Question_Content');
В конфиг:
$config['table']['question_content']  = '___db.table.prefix___question_content';
Вроде так. Пробуй. Указать можно.
avatar
В конфиге все прописано. Если написать Engine::GetEntity('PluginQuestions_Question_Content'); вместо того, что есть — данные будут записываться в text_source, а по остальным столбцам будут ошибки.
avatar
Делай одну таблицу.
  • ort
  • 0
avatar
Спасибо!
avatar
обычно делается так
$oQuestion=Engine::GetEntity('PluginQuestions_Question');
$oQuestion->_setValidateScenario('question');
/**
 * Заполняем поля для валидации
 */
$oQuestion->setTitle(strip_tags(getRequestStr('question_title')));
....

if ($oQuestionNew = $oQuestion->Add()) {
   $oQuestionContent=Engine::GetEntity('PluginQuestions_Question_Content');
   $oQuestionContent->setQuestionId($oQuestionNew->getId());
   $oQuestionContent->setTextSource(getRequestStr('question_text'));
   ...
   $oQuestionContent->Add();
avatar
Проблема правильно составить поля и сущности. То таблицы такой нет. То поля такого нет.
avatar
Не знаю таких проблем…
avatar
Спасибо!
avatar
Если что, обращайся)
avatar
Ок ). Тут как раз пара вопросов назрела: зачем вообще в топике контент вынесен в отдельную таблицу? Для удобства, потому как типы топиков разные? Добавление тегов (в третью таблицу) по аналогии делать, так?
avatar
Насчет контента не знаю, а теги для удобства. Отдельная таблица с тегами упрощает в дальнейшем создание скажем отдельного блока «теги»
avatar
Отдельная таблица с тегами упрощает в дальнейшем создание скажем отдельного блока «теги»

Ее ведь надо заполнять в момент создания топика? Т.е. что-то типа этого:

if ($this->PluginQuestions_Question_AddQuestion($oQuestion)) {
    $aTags=explode(',',$oTopic->getTags());
    foreach ($aTags as $sTag) {
        $oTag=Engine::GetEntity('PluginQuestions_Question_QuestionTag');
        $oTag->setQuestionId($oQuestion->getId());
        $oTag->setUserId($oQuestion->getUserId());
        $oTag->setText($sTag);
        $this->PluginQuestions_Question_AddQuestionTag($oTag);
    }
}
avatar
… поспешил: $oTopic = $oQuestion
avatar
Да, именно так.
При редактировании еще нужно удалять все старые теги
$aOldTags = $this->PluginQa_Qa_GetTagItemsByQaId($oQaEdit->getId());
foreach ($aOldTags as $oOldTag) { $oOldTag->Delete(); }

это перед $aTags = explode(',',$oQa->getTags());
Связку можно юзать
avatar
'tags'=>array(self::RELATION_TYPE_HAS_MANY,'PluginQa_ModuleQa_EntityTag','qa_id')

Можно конечно m2m, но смысла нет
avatar
Это в EntityQuestion добавить, так?
avatar
да, но есть ньюанс
если у вас есть поле question_tags в таблице
значит нужно либо поле в бд переименовать либо в связке
avatar
При редактировании еще нужно удалять все старые теги

Ага, разобрался, как раз до этого дошел.
avatar
Подскажите, как происходит валидация полей?

При добавлении записи в экшене запускаю проверку:

/**
		 * Проверка корректности полей формы
		 */
		if (!$this->checkQuestionFields($oQuestion)) {
			return false;
		}


Далее, там же, добавляю функцию:

/**
   * Проверка полей формы
   *
   * @return bool
   */
  protected function checkQuestionFields($oQuestion) {
    $this->Security_ValidateSendForm();

    $bOk=true;
    /**
     * Валидируем вопрос
     */
    if (!$oQuestion->_Validate()) {
      $this->Message_AddError($oQuestion->_getValidateError(),$this->Lang_Get('error'));
      $bOk=false;
    }

    return $bOk;
  }


В сущности определяю правила:

/**
	 * Определяем правила валидации
	 */
	public function Init() {
		parent::Init();
		$this->aValidateRules[]=array('question_title','string','max'=>200,'min'=>2,'allowEmpty'=>false,'label'=>$this->Lang_Get('plugin.questions.question_title'));
		$this->aValidateRules[]=array('question_text_source','string','max'=>Config::Get('plugin.questions.question_max_length'),'min'=>2,'allowEmpty'=>false,'label'=>$this->Lang_Get('plugin.questions.question_text'));
		$this->aValidateRules[]=array('question_tags','tags','count'=>15,'label'=>$this->Lang_Get('plugin.questions.question_tags'),'allowEmpty'=>false);
		$this->aValidateRules[]=array('question_text_source','question_unique');
	}

  /**
	 * Проверка вопроса на уникальность
	 *
	 * @param string $sValue	Проверяемое значение
	 * @param array $aParams	Параметры
	 * @return bool|string
	 */
	public function ValidateQuestionUnique($sValue,$aParams) {
		$this->setTextHash(md5($sValue.$this->getTitle()));
		if ($oQuestionEquivalent=$this->PluginQuestions_Question_GetQuestionByUserIdAndTextHash($this->getUserId(),$this->getTextHash())) {
			if ($iId=$this->getId() and $oQuestionEquivalent->getId()==$iId) {
				return true;
			}
			return $this->Lang_Get('plugin.questions.questions_create_text_error_unique');
		}
		return true;
	}


И даже при полностью пустых полях запись публикуется (пустая), т.е. поля проходят валидацию. Что я упускаю?
avatar
Все, извиняюсь, разобрался.
avatar
Комрады, а подскажите по функциям GetItemsByArray и Update: добавляю дату прочтения записи

		if ($this->oUserCurrent) {
			$oQuestionRead=Engine::GetEntity('PluginQuestions_Question_QuestionRead');
			$oQuestionRead->setQuestionId($oQuestion->getId());
			$oQuestionRead->setUserId($this->oUserCurrent->getId());
			$oQuestionRead->setDateRead(date("Y-m-d H:i:s"));

			if ($this->PluginQuestions_Question_GetQuestionReadItemsByArray($oQuestionRead->getQuestionId(),$oQuestionRead->getUserId())) {
				$this->PluginQuestions_Question_UpdateQuestionRead($oQuestionRead);
			} else {
				$this->PluginQuestions_Question_AddQuestionRead($oQuestionRead);
			}
		}


в итоге получаю ошибки:

Warning: array_merge() [function.array-merge]: Argument #2 is not an array in Z:\home\livestreet\www\engine\classes\ModuleORM.class.php on line 725

Warning: array_fill() [function.array-fill]: Number of elements must be positive in Z:\home\livestreet\www\engine\classes\MapperORM.class.php on line 62

Warning: array_map() [function.array-map]: Argument #4 should be an array in Z:\home\livestreet\www\engine\classes\MapperORM.class.php on line 62

Warning: implode() [function.implode]: Invalid arguments passed in Z:\home\livestreet\www\engine\classes\MapperORM.class.php on line 62
SQL Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1 at Z:\home\livestreet\www\engine\classes\MapperORM.class.php line 64
Array ( [code] => 1064 [message] => You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1 [query] => UPDATE prefix_question_read SET `question_id`='3', `user_id`='1', `date_read`='2015-12-22 12:31:38' WHERE 1=1 AND [context] => Z:\home\livestreet\www\engine\classes\MapperORM.class.php line 64 ) 


Подскажите, как правильно записать GetQuestionReadItemsByArray и UpdateQuestionRead?

И оффтопом: а где в движке используется дата прочтения топика?
avatar
Для обновления достаточно:
$oQuestionRead->Update();
Или:
$oQuestionRead->Save();
Сохранение сущности в БД (если новая то создается)
avatar
Я думал такие короткие — это в движке если, а в плагине необходимо полностью писать. Спасибо!
avatar
Повторюсь. Смотри плагин форум. По нему учил :-)
avatar
Та я смотрю, и здесь параллельно все перечитываю, в голове уже каша. )
avatar
запись
if ($this->PluginQuestions_Question_GetQuestionReadItemsByArray($oQuestionRead->getQuestionId(),$oQuestionRead->getUserId())) {

в корне не верна.

ты можешь юзать либо:
// массив айдишников
$aQuestionIds = array(
  $oQuestionRead->getQuestionId()
);
...ItemsByArrayQuestionId($aQuestionIds)</codeline>
либо если еще нужен фильтр по юзеру
<code>
// массив айдишников
$aQuestionIds = array(
  $oQuestionRead->getQuestionId()
);
...ItemsAll(array(
  '#where' => array(
    'question_id IN (?a)' => array($aQuestionIds),
    'user_id = ?d' => array($oQuestionRead->getUserId()),
  )
);

но насколько я понял твой код тебе подойдет следующее выражение:
if ($this->oUserCurrent) {
  if (!$oQuestionRead = $this->PluginQuestions_Question_GetQuestionReadByQuestionIdAndUserId($oQuestion->getId(),$this->oUserCurrent->getId())) {
    $oQuestionRead=Engine::GetEntity('PluginQuestions_Question_QuestionRead');
    $oQuestionRead->setQuestionId($oQuestion->getId());
    $oQuestionRead->setUserId($this->oUserCurrent->getId());
  }
  $oQuestionRead->setDateRead(date("Y-m-d H:i:s"));
  $oQuestionRead->Save();
}
avatar
Спасибо!
avatar
И оффтопом: а где в движке используется дата прочтения топика?
Для выделения непрочитанных комментов
avatar
Как-то так и предполагал, спасибо!
avatar
Я, может, уже надоел со своими вопросами, так что заранее прошу прощения, но тем не менее спрошу еще. :)

Дошел до навешивания автокомплита на поле тегов. Сделал следующее:

1. на инпут тегов навесил класс autocomplete-question-tags.

2. в script.js шаблона плагина прописал:
jQuery(document).ready(function($){
	// Автокомплит
	ls.autocomplete.add($(".autocomplete-question-tags"), aRouter['questionsajax']+'autocompleter/tag/', true);
});


3. Добавил файл ActionAjax.class.php, а в него:
<?php

class PluginQuestions_ActionAjax extends ActionPlugin {
  /**
	 * Текущий юзер
	 *
	 * @var ModuleUser_EntityUser|null
	 */
	protected $oUserCurrent=null;

  /**
   * Инициализация экшена
   */
  public function Init() {
    /**
		 * Достаём текущего пользователя
		 */
    $this->oUserCurrent=$this->User_GetUserCurrent();
		$this->Viewer_SetResponseAjax('json');
  }
  /**
   * Регистрируем евенты
   */
  protected function RegisterEvent() {
		$this->AddEventPreg('/^autocompleter$/i','/^tag$/','EventAutocompleterQuestionTag');
  }

  /**********************************************************************************
	 ************************ РЕАЛИЗАЦИЯ ЭКШЕНА ***************************************
	 **********************************************************************************
	 */

	/**
	 * Автоподставновка тегов
	 *
	 */
	protected function EventAutocompleterQuestionTag() {
		/**
		 * Первые буквы тега переданы?
		 */
		if (!($sValue=getRequest('value',null,'post')) or !is_string($sValue)) {
			return ;
		}
		$aItems=array();
		/**
		 * Формируем список тегов
		 */
		$aTags=$this->PluginQuestions_Question_GetQuestionTagsByLike($sValue,10);
		foreach ($aTags as $oTag) {
			$aItems[]=$oTag->getText();
		}
		/**
		 * Передаем результат в ajax ответ
		 */
		$this->Viewer_AssignAjax('aItems',$aItems);
	}
}
?>


4. в Question.class.php написал:
<?php

class PluginQuestions_ModuleQuestion extends ModuleORM {
  protected $oMapper;

  /**
	 * Получает список тегов по первым буквам тега
	 *
	 * @param string $sTag	Тэг
	 * @param int $iLimit	Количество
	 * @return bool
	 */
	public function GetQuestionTagsByLike($sTag,$iLimit) {
		if (false === ($data = $this->Cache_Get("tag_like_{$sTag}_{$iLimit}"))) {
			$data = $this->oMapper->GetQuestionTagsByLike($sTag,$iLimit);
			$this->Cache_Set($data, "tag_like_{$sTag}_{$iLimit}", array("question_update","question_new"), 60*60*24*3);
		}
		return $data;
	}

}
?>


5. в Question.mapper.class.php:
<?php

class PluginQuestions_ModuleQuestion_MapperQuestion extends MapperORM {
	protected $oDb;

  /**
	 * Получает список тегов по первым буквам тега
	 *
	 * @param string $sTag	Тэг
	 * @param int $iLimit	Количество
	 * @return bool
	 */
	public function GetQuestionTagsByLike($sTag,$iLimit) {
		$sTag=mb_strtolower($sTag,"UTF-8");
		$sql = "SELECT
				question_tag_text
			FROM
				".Config::Get('plugin.questions.table.question_tag')."
			WHERE
				question_tag_text LIKE ?
			GROUP BY
				question_tag_text
			LIMIT 0, ?d
				";
		$aReturn=array();
		if ($aRows=$this->oDb->select($sql,$sTag.'%',$iLimit)) {
			foreach ($aRows as $aRow) {
				$aReturn[]=Engine::GetEntity('PluginQuestions_Question_QuestionTag',$aRow);
			}
		}
		return $aReturn;
	}
}
?>


6. ну и в конфиг —
Config::Set('router.page.questionsajax', 'PluginQuestions_ActionAjax');


В итоге постоянный ui-autocomplete-loading, а консоль выдает следующее:

Fatal error: Call to a member function GetQuestionTagsByLike() on null in C:\xampp\htdocs
\livestreet\plugins\questions\classes\modules\question\Question.class.php
on line 25

25-я строчка:
$data = $this->oMapper->GetQuestionTagsByLike($sTag,$iLimit);


Что я делаю не так?
avatar
Пропустил:
public function Init() {
parent::Init();
$this->oMapper=Engine::GetMapper(__CLASS__);
}
avatar
Ага, спасибо, заработало! Я почему-то решил, что при использовании ORM инит не нужно писать (в прошлом плагине данный файл пустой был).
avatar
Вопрос по предпросмотру записи: предпросмотр в топиках вызывается с помощью ls.topic.preview, хочу переписать под себя но чет не могу найти где это расположено. Не подскажете?
avatar
Решено, разобрался.
avatar
Параллельно нашел мелкий баг в текущей версии движка: при предпросмотре топика, если не заполнено поле «тект» предпросмотр все равно срабатывает, хотя валидация данного поля заложена, только там вместо topic_text_source прописано topic_text.
avatar
Возник еще вопросик: прикручиваю голосование, в принципе, все вышло, но есть нюанс… При голосовании за запись все отображается как и положено, но стоит обновить страницу и снова появляются кнопки голосования. При попытке проголосовать еще раз вылетает «вы уже голосовали». Автор записи видит все как положено (оценку, количество голосов).

Покопавшись обнаружил, что не срабатывают условия типа
{if $oVote}
т.е. не вытаскивается $oVote. На сколько понял, надо в сущность добавить $aRelations. Сделал так:

'vote' => array(self::RELATION_TYPE_BELONGS_TO,'ModuleVote_EntityVote','user_voter_id'),


и получаю ошибку: Fatal error: Uncaught exception 'Exception' with message 'Undefined method module: getVoteById'.

Подскажите, пожалуйста, в нужную ли сторону я вообще смотрю и в чем тут ошибка.
avatar
avatar
Спасибо, уже разобрался. Думал может можно одним релэйшионом обойтись.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.