ActiveRecord, почему я отказался и в пользу чего

Ирокез

бессмертный пони
Команда форума
Партнер клуба
Итак, почему и в пользу чего, я отказался от ActiveRecord.

Для начала - Почему!
Собственно я кривлю душой, говоря, что я не использую AR, да в моейм ФВ он есть, но я потихонечку от него отказываюсь. Есть он у меня в таком виде:
PHP:
class Client extends SqlObject
{
    public function  __construct() {        
        parent::__construct('clients_schema');
        $this->INT('id')->INT('cl_ownership',0,'ownership')->VARCHAR('cl_name', 256, 'name')->INT('cl_state',1,'state')->
            VARCHAR('cl_contact_name', 128, 'contact_name')->PHONE('cl_contact_phone','contact_phone')->
            VARCHAR('cl_contact_email', 64, 'contact_email')->INT('cl_contact_post', 3, 'contact_post')->INT('cl_contact_access', 0, 'contact_access')->
            VARCHAR('cl_license', 64, 'document')->    
            PHONE('cl_fax','contact_fax')->PHONE('cl_phone','phone')->
            DATE('cl_date_create','date_create')->INT('cl_manager',0,'manager_id')->
            VIRTUAL('cl_address_post', array('country'=>32,'city'=>48,'region'=>64,'area'=>64,'address'=>256,'zip'=>24),'address_post')->
            VIRTUAL('cl_address_office', array('country'=>32,'city'=>48,'region'=>64,'area'=>64,'address'=>256,'zip'=>24),'address_office')->
            VIRTUAL('cl_account_details', array('number'=>32,'unn'=>32,'okpo'=>32,'ru_number'=>40,'ru_bik'=>40,'ru_inn'=>40,'ru_cor'=>40,'ru_kpp'=>40,'ru_okpo'=>40,'ru_ogrn'=>40), 'account_details')->
            VIRTUAL('cl_bank', array('name'=>128,'suboffice'=>128,'code'=>16,'location'=>'128'), 'bank')->
            VIRTUAL('cl_passport', array('number'=>32,'pin'=>64,'date'=>16,'issued'=>'128'), 'passport')->
            VIRTUAL('cl_ip', array('license'=>64,'date'=>16), 'ip')->INT('cl_owner',1,'company')->
            RS('workflow','id','client_id','Workflow');
}
Что есть что, просты типы, думаю понятны, VIRTUAL - это тип сохраняет и восстанавливает из поля blob->array. При этом получается вот такая штука
PHP:
$Object->address_post['country']
.
RS - сокращенно от RecordsSet. Сложные типы данных VIRTUAL, RS, OBJ - реализуют паттерн Lazy Initialization.

Впринципе для более менее простых таблиц очень удобно. Почему отказываюсь, потому-как большие объемы и разнородность вытягиваемых данных с которыми приходится работать, не поощрают описывание полей до момента их вытягивания.
Резюмируя, для чего может быть полезен паттерн, для более менее статических запросов (User, Client и т.д.), для запросов, когда из одной таблицы вытягиваются разные поля или необходимы сложные JOIN-ы, паттерн использовать можно, но в моем случае не целесообразно.
Я опущу описание статических методов SqlObject::Object, SqlObject::Recordset (один получает объект, второй массив объектов).

Дальнейшее развитие, паттерна привело к перегрузке магического метода __get, __set. В которых начала появляться более сложная логика получения данных привязанных к объекту.

Как-я и писал, на определенном этапе, меня перестало это устраивать, описание полей + некоторые аспекты повышения производительности кода, натолкнули на решение отказаться от AR в чистом виде и появился некий симбиоз

Что получилось!

Теперь я использую прямые запросы к БД, минуя QueryBuilder-ы. Выглядит это примерно так
PHP:
class Client extends TObject
{
	protected function address_post()
	{
		return unserialize('cl_address_post');
	}

	public function workflow($fromDate=false,$toDate=false)
	{
		return sql::recordset(sql::query("SELECT * FROM ... WHERE client_id=%d",$this->id),false,'Workflow');
	}

	static public function Get($ClientId)
	{
		return sql::object(sql::query("SELECT id,cl_name as `name`, .... cl_address_post FROM clients_schema WHERE id=%d",$ClientId),'Client');
	}

	static public function Update($DataSet,$ClientId=false) { ... }
	
}
Что мне это дало.
- Гибкость - теперь можно не используя сложное наследование и сложные описание извлекаемых (обновляемых) данных, использовать прелести AR. При этом сохранилась основная возможность LazyInitialization + Lazy Load
- Увеличилась производительность (в основном за счет отсутсвия инициализации полей + отсутствия query builder-а)
- Добавилась некоторая удобная функциональность, теперь к примеру можно использовать переменную класса в таком виде
PHP:
	$Object->Workflow;
	# Либо расширенный функционал
	$Object->Wokflow($fromDate,$toDate);
- Появилась возможность контролируемо JOIN-нить поля (в основном это относится к оптимизации индексов, по которым выбираются данные)
- Прозрачность и понятность запроса, что позволяет точно знать какой именно запрос выполняется + возможность его оптимизации и отладки (EXPLAIN, EXPLAIN EXTENDED)

Что потеряли:
- Да, теперь запросы, даже примитивные придется писать вручную, впрочем, как дабавление и обновление данных.
- Из чего вытекает более жесткий контроль за данным (помним про sql инъекции).

Где это имеет место быть?
Основное, это когда идут большие объемы данных, где надо четко контролировать запрос, где есть сложные JOIN-ы.

Вот собственно два подхода и мое обоснование, почему я ухожу от AR в чистом виде.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Поздравляю с переходом на светлую сторону силы.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
ты как-то слишком холиварно написал тему: непонятный код, абстрактные фразы

цель AR - уйти от sql-запросов, от работы с базой вообще, просто работать с объектом, используя автокомплит ide,
а твой AR больше похож на QueryBuilder
 

Ирокез

бессмертный пони
Команда форума
Партнер клуба
ты как-то слишком холиварно написал тему: непонятный код, абстрактные фразы
Я не описывал паттерн, а описывал путь почему я ушел от AR и к чему пришел
цель AR - уйти от sql-запросов, от работы с базой вообще, просто работать с объектом, используя автокомплит ide,
а твой AR больше похож на QueryBuilder
Эммм, ну AR по любому выполнит запрос к БД, просто для тебя он будет скрыт, автокомплит это лишь объявление комментария в нотификации phpDoc (javaDoc)
от QueryBuilder - это очень далеко
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
мне кажется, что ты ушел не от AR, а от более замороченного QB к другому, менее замороченному
это правильно
а AR - это еще более простой и понятный алгоритм
короче, возьми нормальный AR для начала :)
 

Ирокез

бессмертный пони
Команда форума
Партнер клуба
короче, возьми нормальный AR для начала :)
эмм, суть поста не выборе "нормального" AR (коих я посмотрел достаточно), а почему я от них ушел и в сторону чего.

QueryBuilder это набор, фэйков над синтаксисом SQL, да он присутствуюет у меня в первом варианте, но повторюсь, что я пришел к тому, чтобы убрать QB и писать прямы запросы
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
я ж говорю, слишком холиварно и беспредметно
влом обсуждать дальше, мы друг друга не понимаем
 

atv

Новичок
убрать QB и писать прямы запросы
Т.е. да здравствует конкатенация строк с многочисленными "WHERE 1", join(' AND ', $conditions), многочисленными if-ами и т.д.? Тада да, добро пожаловать на "светлую сторону" :D
 
  • Like
Реакции: NeD

Ирокез

бессмертный пони
Команда форума
Партнер клуба
Н-да, боюсь что grigori, atv вы замкнулись на QB, коего впринципе у меня нет.

Беспредметно, что?

Причем тут конкатенация и выдранный кусок из QB, если четко написано, что используются прямые SQL запросы.
 

С.

Продвинутый новичок
Если корректно смапить базу данных на модель, то даже в достаточно сложном приложении с трудом наберется и десяток SQL запросов (если конечно без копипасты). Лепить какую-то прокладку между ними просто не имеет смысла. Лучше уж конкатенации строк и максимальная оптимизация под синтаксис конкретной БД.

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

atv

Новичок
Даже в совсем несложном приложении найдётся с десяток SQL запросов, у которых отличается только условие WHERE, и вот тут поступают по разному, либо копипастят весь запрос и создают десяток методов на каждую комбинацию условий WHERE, либо начинают склеивать условие WHERE из строк и делают метод с десятком параметров. Второй случай встречается чаще и вот во втором случае QB очень помогает, вплоть до того что условие может собираться за пределами метода а в метод передаваться как аргумент.
Но да, согласен, каждый сходит с ума по своему :D
 

AmdY

Пью пиво
Команда форума
atv
+1, как минимум where builder должен быть, а весь остальной запрос можно хоть билдером, хоть руками делать.
PHP:
public function workflow($fromDate=false,$toDate=false)
    {
        return sql::recordset(sql::query("SELECT * FROM ... WHERE client_id=%d",$this->id),false,'Workflow');
    }
вот собственно в этом методе уже видна проблема, здесь не хватает обработки ($fromDate=false,$toDate=false), а это значит что будет каша.


у меня как-то так. запрос можно составлять любым способом, where умеет __toString
PHP:
$obj = new Model_Name();
$obj->setWhere('visible = ?', 1);
$obj->getWhere()->addAnd('id = ? AND update_date = ?', array(42, $obj->getExpr('NOW()')));
$obj->findOne(); // рузультат доступен через ArrayAccess, чтобы не заводить лишних переменных $data = $obj->findOne()
echo $obj['id'];  

// внутри where builder умеющий билдить строку where  .... и собирать пачку данных для prepared statement
$where = Db_Where::create()->addAnd('id = ?', 666);
$sql = "SELECT * FROM tableName $where LIMIT 20";
$this->assertEquals("SELECT * FROM tableName \n WHERE (id = ?) LIMIT 20", $sql);
$this->assertEquals(array(666), $where->getData());
 

Ирокез

бессмертный пони
Команда форума
Партнер клуба
atv
+1, как минимум where builder должен быть, а весь остальной запрос можно хоть билдером, хоть руками делать.
вопрос в подходе, where builder, для простых запросов он удобен для сложных вызов кучи методов имхо, для сайтов это имеет место, для более менее серьезных - врятли

вот собственно в этом методе уже видна проблема, здесь не хватает обработки ($fromDate=false,$toDate=false), а это значит что будет каша.
Это пример, но никак не код готового метода, врятли тут может быть каша...

PHP:
public function workflow($fromDate=false,$toDate=false)
    {
        return sql::recordset(sql::query("SELECT * FROM ... WHERE client_id=%d AND cdate BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)",$this->id,$fromDate,$toDate),false,'Workflow');
    }
у меня как-то так. запрос можно составлять любым способом, where умеет __toString
PHP:
$obj = new Model_Name();
$obj->setWhere('visible = ?', 1);
$obj->getWhere()->addAnd('id = ? AND update_date = ?', array(42, $obj->getExpr('NOW()')));
$obj->findOne(); // рузультат доступен через ArrayAccess, чтобы не заводить лишних переменных $data = $obj->findOne()
echo $obj['id'];  

// внутри where builder умеющий билдить строку where  .... и собирать пачку данных для prepared statement
$where = Db_Where::create()->addAnd('id = ?', 666);
$sql = "SELECT * FROM tableName $where LIMIT 20";
$this->assertEquals("SELECT * FROM tableName \n WHERE (id = ?) LIMIT 20", $sql);
$this->assertEquals(array(666), $where->getData());
куча вызовов, методов для выполнения простых запросов радует :)
уж боюсь предположить что к более менее сложному запросу надо будет написать столько ->addJoin()->addCondition->addAnd() ,и др....

вопрос подхода, такой вариант конечно-же имеет право на существование, но я писал выше почему я отказался от всего этого методавызываяние непонятно для чего.
 

AmdY

Пью пиво
Команда форума
Ирокез
нет кучи вызовов, он ровно один для простого запроса
$obj->setWhere('client_id= ?', 1);
если нужна ещё проверка, то добавляется ещё одно условие.
PHP:
if ($fromDate) $obj->getWhere()->addAnd("cdate >= ?", $fromDate);
if ($toDate) $obj->getWhere()->addAnd("cdate <= ?", $toDate);
аналогичная реализация без билдера предполагает значительно много ветвистых if
PHP:
$where = "";
if ($clientId) {
    $where = "client_id=%d";
}
if ($fromDate) {
   $where = ($clientId? " AND cdate >= %d", "cdate >= %d"); // ручной контроль был ли до этого условия
}
.....
$sql = "SELECT * FROM tableName ".($where ?  "WHERE $where" :""); // проверка есть ли фильтр и добавление соответственного WHERE
sql::recordset(sql::query($sql, [здесь вообще будет каша, так как нужно проверять какие переменные передовать]));
в моей реализации больше можно вовсе не создавать метод в модели для простого запроса. для сложного весь запрос пишется без всяких ->addJoin(), в примере есть этот момент
$sql = "SELECT * FROM tableName $where LIMIT 20"; сам sql может быть насколько угодно сложным, но билдится лишь where часть

p.s. Прелесть подхода, что он никак не влияет на весь оставшийся код и можно писать как плоские запросы, так и пользоваться "полноценным" QB c addJoin, setLimit, подход не навязывается, всё на усмотрение разработчика
 

AmdY

Пью пиво
Команда форума
вот, кстати, сама реализация, может кто захочет присоединиться к моему светлому пути
PHP:
class Kiss_Db_Where {
    protected $_sql = '';
    protected $_data = array();

    public function __construct($where = '', $params = array()) {
        if ($where) {
            $this->addAnd($where, $params);
        }
    }
    /**
     * @return Kiss_Db_Where
     */
    protected function _pushData($data = array()) {
        foreach ((array) $data as $v) {
            $this->_data[] = $v;
        }
        return $this;
    }
    /**
     * @return Kiss_Db_Where
     */
    public static function create($where='', $params = array()) {
        return new self($where, $params); //
    }
    /**
     * @return Kiss_Db_Where
     */
    public function addAnd($sql, $data = array()) {
        $this->_pushData($data);
        if ($this->_sql) {
            $this->_sql .= " AND ($sql) ";
        } else {
            $this->_sql .= " ($sql) ";
        }
        return $this;
    }
    /**
     * @return Kiss_Db_Where
     */
    public function addOr($sql, $data = array()) {
        $this->_pushData($data);
        if ($this->_sql) {
            $this->_sql .= " OR ($sql) ";
        } else {
            $this->_sql .= " ($sql) ";
        }
        return $this;
    }
    public function getSql($withWhere = true) {
        return ( $this->_sql ? ($withWhere ? "\n WHERE " : "") . $this->_sql : '');
    }
    public function __toString() {
        return $this->getSql(true);
    }
    public function getData() {
        return $this->_data;
    }
    /**
     * @return Kiss_Db_Where
     */
    public function clear($andSql = null, $data = array()) {
        $this->_sql = '';
        $this->_data = array();
        if ($andSql) {
            return $this->addAnd($sql, $data);
        } else {
            return $this;
        }
    }
}
 

Ragazzo

TDD interested
AmdY
код наверное еще под 5.2 был написан? из-за постоянных Kiss_*, а не use Kiss ;)
 

AmdY

Пью пиво
Команда форума
Ragazzo
у меня весь код 5.2 совместимый.
 

Ирокез

бессмертный пони
Команда форума
Партнер клуба
AmdY
Ну значит ты не уловил сути, хотя вроде я как описывал, твоя реализация ни чем не лучше и не хуже, тех что сделаны практически в любом ФВ,у меня была похожая, но я писал что она меня не устраивает как по каше из вызовов методов условий так и с точки зрения производительности.

На всякий случай я уточняю, что,
sql::eek:bject (sql::recordset), возвращает инстанс класса модели (массив объектов модели)

PHP:
$obj->Workflow; #Это геттер который получит по умолчанию, SELECT * FROM... WHERE client_id=%d

#Но можно воспользоваться не как геттером, а как методом

$obj->Workflow($from,$to); # Соответсвенно выберет за даты
дело не в QB, WB и др. я говорил о подходе для работы с данными, а некаким способом обратиться к БД и построить запрос.

Т.е. в моем случае я точно знаю, что если вызывается как геттер, то мне не надо проверять кучу условий и билдить запрос, а если установлены параметры, то соответсвенно будет select, в котором никаких конкатенаций не будет.
 

Ирокез

бессмертный пони
Команда форума
Партнер клуба
Ирокез
PHP:
$where = "";
if ($clientId) {
    $where = "client_id=%d";
}
if ($fromDate) {
   $where = ($clientId? " AND cdate >= %d", "cdate >= %d"); // ручной контроль был ли до этого условия
}
.....
$sql = "SELECT * FROM tableName ".($where ?  "WHERE $where" :""); // проверка есть ли фильтр и добавление соответственного WHERE
sql::recordset(sql::query($sql, [здесь вообще будет каша, так как нужно проверять какие переменные передовать]));
Ого ты спецом так написал? Такой жесткий код я себе не позволяю писать. Для верности можно былоб цикл еще сюда вставить ;)
 
Сверху