Архитектура доступа к данным в PHP
🕛 18.02.2008, 18:05
Современные системы бизнес уровня, а также информационные сайты и блоги редко обходятся без использования СУРБД, реже других источников данных. Наиболее удобная реализация взаимодействия бизнес логики, источника данных и представления зависит от планирования архитектуры приложения. Объектно-ориентированное программирование позволяет реализовать вашу систему так, что отдельные модули можно использовать в других системах. В данной статье мы реализуем архитектуру взаимодействия с источником данных. И назовем этот модуль Data Source Object.Дело в том, что источников данных может быть несколько в одном приложении, что затрудняет ее структуру. Хотелось бы иметь некий абстрактный интерфейс для работы с источником данных, т.е. желательно использовать полиморфные классы для сокрытия реализации подключения к источнику, извлечения и изменения данных. Полиморфизмом называется возможность применения методов с одинаковыми именами в группе классов, связанных наследованием. Применяя подобные классы, в связке с паттерном Фабрика, который позволяет манипулировать различными вариациями в зависимости от необходимого случая, работа с данными становится намного проще. Давай рассмотрим два случая, когда данные хранятся в двух различных СУРБД, MySQL и MSSQL.
Первым делом мы реализуем класс, моделирующий данные для подключения к источнику данных.
<? class Db { private $user; private $password; private $host; private $name; public function _construct($user, $password, $host, $name) { $this->user = $user; $this->password = $password; $this->host = $host; $this->name = $name; } public function getUser() { return $this->user; } public function getPassword() { return $this->password; } public function getHost() { return $this->host; } public function getName() { return $this->name; } } ?>
Действительно, php функции подключения к СУРБД имеют одинаковый интерфейс. Класс представляет необходимые параметры в объектном виде. Члены класса можно объявить как public, это дело вкуса, но я привык именно так работать :). Так как мы только моделируем функциональность, мы воспользуемся абстрактными классами. Если в классе объявлены абстрактные методы, то и класс тоже должен быть объявлен как абстрактный. Абстрактный класс может содержать и обычные методы и поля. Нельзя создавать объект абстрактного класса, но его нужно переопределять классами потомками. Т.е. абстрактные классы служат прототипами классов, которые наследуют их основу. Мы безоговорочно должны принять правила, диктуемые нам абстрактными классами. Если мы объявили абстрактный метод с двумя параметрами, то в последующих классах мы должны реализовать одноименный метод именно с тем же числом параметров.
<? abstract class Connection { protected $db; protected $connection; final public function _construct($db) { $this->db = $db; $this->Open(); } abstract protected function Open(); final public function getConnection() { return $this->connection; } } ?>
В данном случае мы видим абстрактный класс, моделирующий подключение уже для конкретной СУРБД, т.е. класс, реализующий подключение к MySQL будет наследоваться от данного класса. Класс имеет два protected члена - $db и $connection. Первый - это объект типа Db из предыдущего листинга, второй - указатель на соединение, необходимый для выполнения запросов. В конструкторе класса реализуется инициализация объекта типа Db, а также вызывается метод, открывающий соединение с источником данных. Метод getConnection() соответственно открывает доступ извне к указателю на соединение. Давай рассмотрим реализацию подключения к БД на примере MySQL. Для других источников данных архитектура окажется подобной.
<? class MySQLConnection extends Connection { protected function Open() { $host = $this->db->getHost(); $pas = $this->db->getPassword(); $user = $this->db->getUser(); $name = $this->db->getName(); if(!$this->connection = @mysql_pconnect($host, $user, $pas)) { $this->Errors[]="Не удается установить подключение"; return FALSE; } if(! @mysql_select_db($name)) { $this->Errors[]="Не удается выбрать базу данных";\ return FALSE; } } } ?>
Как видно из листинга, класс MySQLConnection является потомком класса Connection, т.е. расширяет предыдущий класс до нового путём расширения функциональности, т.е. новый класс будет содержать те же методы (которые, кстати, могут и изменить свою реализацию) и свойства прежнего класса. Также видно, что класс реализует метод подключения к источнику. Количество строк можно сократить, но написано именно так для наглядности :). Очевидно, что для других СУРБД реализация будет схожей.
Настало время подумать над тем, как мы будем взаимодействовать с данными. И так, мы должны открыть соединение с СУРБД, выполнить запрос, если запрос на извлечение данных, представить их в удобном виде. Например, массив данных удобнее представить в массиве и т.д.
Идея состоит в том, что ниже реализованные методы функциональны в рамках одного SQL запроса, т.е. результат, возвращаемый ими, будет зависеть от самого запроса.
<? abstract class DataAccess { protected $db; protected $connection; protected $result; protected $Errors; final public function _construct($db) { $this->db = $db; } abstract public function Query($sql); abstract public function db2Array(); abstract public function dbCol2Array($num); abstract public function dbRow2Array($num); abstract public function db2Var($row, $col); abstract public function dbNumRows(); final public function getErrors() { return $this->Errors; } } ?>Данные класса состоят из protected членов $db, объект типа DataBase; $connection представляет указатель на соединение; $result, является результатом запроса; $Errors - массив ошибок, возникших в процессе работы.
Функциональность представим следующими абстрактными методами.
* Query($sql) - входной параметр данного метода есть SQL запрос. Сам же метод подключается к источнику данных, выполняет запрос, инициализирует результат выполнения запроса. Если запрос выполнился удачно, возвращаем истину, если нет, то кричим, что ложь :). Данный метод в дальнейшем мы будем использовать для запросов как на извлечение данных, так и на их изменение. * db2Array() - метод возвращает массив данных, в зависимости от запроса. * dbCol2Array($num) - представляет данные таблицы в виде одного столбца. Входной параметр имеет тип String. * dbRow2Array($num) - возвращает данные одной конкретной строки. Входной параметр - integer. * db2Var($row, $col) - представляет пересечение столбца и ячейки. * dbNumRows() - возвращает количество строк, выбранных запросом * getErrors()
Давайте теперь рассмотрим реализацию вышеприведенных методов.
<? class MySQLDataAccess extends DataAccess { public function Query($sql) { $con = new MySQLConnection($this->db); $this->connection = $con->getConnection(); if(!$this->result = @mysql_query($sql, $this->connection)) { $this->Errors[]="Не удается выполнить запрос: $sql"; return FALSE; } else return TRUE; } public function setCharset($charset) { $sql = "SET NAMES '$charset'"; $this->Query($sql); } public function db2Array() { $array = array(); while($rows = @mysql_fetch_array($this->result, MYSQL_ASSOC)) { $array[] = $rows; } return $array; } public function dbCol2Array($num) { while($rows = @mysql_fetch_array($this->result, MYSQL_ASSOC)) { $data[] = $rows[$num]; } if(count($data)<0) { $this->Errors[]="Массив столбцов пуст"; return FALSE; } else return $data; } public function dbRow2Array($num) { $num = (int)$num; $i=0; while($rows = @mysql_fetch_array($this->result, MYSQL_ASSOC)) { if($i === $num) { $data = $rows; break; } $i++; } if(count($data)<0) { $this->Errors[]="Массив строк пуст"; return FALSE; } else return $this->data; } public function db2Var($row, $col) { $i=0; while($rows = @mysql_fetch_array($this->result, MYSQL_ASSOC)) { if($i == $row) { $data = $rows[$col]; break; } $i++; } if(!empty($data)) { return $data; } else { $this->Errors[]="Не удается извлечь $row x $col ячейку"; return false; } } public function dbNumRows() { $numRows = @mysql_num_rows($this->result); if($numRows > 0) return $numRows; else { $this->Errors[]="Не удается получить число выбранных строк"; return false; } } } ?>Теперь осталось сделать так, что бы при необходимом условии выполнялась определенная реализация. Пишем класс…
class DataAccessor { private $type; public function _construct($type) { $this->type = $type; } public function getObj($db) { switch($this->type) { case 'MySQL': $obj = new MySQLDataAccess($db); break; case 'MSSQL': $obj = new MSSQLDataAccess($db); break; default: $obj = new MySQLDataAccess($db); break; } return $obj; } } ?>
Как видно из листинга, есть дополнительный метод, который устанавливает кодировку, необходимую для запроса. Данный метод не был включен в общий список абстрактных методов, т.к. является специфическим для данной СУРБД.
Принцип действия данного класса заключается в том, что, в зависимости от параметра type, возвращается необходимый объект.
Рассмотрим, как все это реализуется в коде.
<? $dbo = new Db('$user', '$password', '$host', '$database'); $dba = new DataAccessor('MySQL'); $db = $dba->getObj($dbo); $db->setCharset('cp1251'); ?>В первой строке создается объект, представляющей параметры подключения. Во второй, создаем объект, конструктор которого инициализирует тип источника данных. В третей строке мы получаем необходимый объект. В последней происходит задание кодировки, в которой будет происходить взаимодействие с базой данных.