Шаблоны проектирования. Как создать объект базы данных/соединение только при необходимости?

У меня есть простое приложение, скажем, что у него есть несколько классов и "дополнительный", который обрабатывает запросы к базе данных. В настоящее время я создаю объект базы данных каждый раз, когда приложение используется, но в некоторых случаях нет необходимости в подключении к базе данных. Я делаю это так (PHP btw):

$db = new Database();    
$foo = new Foo($db); // passing the db

Но иногда объекту $foo не нужен доступ к db, так как вызываются только методы без действий базы данных. Поэтому мой вопрос: какой профессиональный способ справиться с такими ситуациями/как создать соединение/объект db только при необходимости?

Моя цель - избежать ненужных подключений к базе данных.

Ответы

Ответ 1

Примечание.. Хотя прямой ответ на вопрос ops "когда я могу только создавать/подключаться к базе данных, когда это необходимо, а не по каждому запросу", вставляет ее, когда вам это нужно, просто говоря, что не полезно. Я объясняю вам, как вы на самом деле делаете это правильно, так как в этом случае не очень много полезной информации в контексте неконкретной структуры, чтобы помочь в этом отношении.


Обновлено: "Старый" ответ на этот вопрос можно увидеть ниже. Это побудило шаблон локатора обслуживания, который является очень противоречивым и для многих "анти-шаблоном". Новый ответ добавлен тем, что я узнал из исследования. Сначала прочитайте старый ответ, чтобы узнать, как это произошло.

Новый ответ

После использования прыща какое-то время я много узнал о том, как он работает, и о том, как это вообще не удивительно. Это все еще довольно круто, но причина в том, что это всего лишь 80 строк кода, потому что он в основном позволяет создавать массив замыканий. Pimple много используется как локатор сервисов (потому что он настолько ограничен в том, что он действительно может делать), и это "анти-шаблон".

Во-первых, что такое локатор сервисов?

Шаблон локатора службы - это шаблон проектирования, используемый в разработке программного обеспечения для инкапсуляции процессов, связанных с получением услуги с сильным уровнем абстракции. В этом шаблоне используется центральный реестр, известный как "локатор сервисов", который по запросу возвращает информацию, необходимую для выполнения определенной задачи.

Я создавал прыщ в бутстрапе, определял зависимости, а затем передавал этот контейнер каждому экземпляру, который я создал.

Почему плохой локатор службы?

В чем проблема с этим? Основная проблема заключается в том, что этот подход скрывает зависимости от класса. Поэтому, если разработчик приступает к обновлению этого класса, и они не видели его раньше, они собираются увидеть контейнерный объект, содержащий неизвестное количество объектов. Кроме того, тестирование этого класса будет немного кошмаром.

Почему я сделал это изначально? Потому что я думал, что после того, как контроллер начнет делать вашу инъекцию зависимостей. Это неверно. Вы начинаете его сразу на уровне контроллера.

Если это так, как все работает в моем приложении:

Фронтальный контроллер Бутстрап Маршрутизатор Контроллер/метод Модель [Услуги | Объекты домена | Мапперы] Контроллер Просмотр Шаблон

... тогда контейнер инъекции зависимостей должен начать работать сразу на первом уровне контроллера.

Так что, если бы я все еще использовал прыщ, я бы определил, какие контроллеры будут созданы и что им нужно. Таким образом, вы бы ввели представление и что-нибудь из слоя модели в контроллер, чтобы он мог его использовать. Это Inversion Of Control и упрощает тестирование. Из вики Aurn (о чем я скоро расскажу):

В реальной жизни вы не строите дом, транспортируя весь хозяйственный магазин (надеюсь) на строительную площадку, чтобы вы могли получить доступ к любым частям, в которых вы нуждаетесь. Вместо этого бригадир (__construct()) запрашивает конкретные детали, которые будут необходимы (дверь и окно), и идет о их приобретении. Ваши объекты должны функционировать одинаково; они должны запрашивать только конкретные зависимости, необходимые для выполнения своих задач. Предоставление доступа к дому во всем хозяйственном магазине в лучшем случае является плохим стилем ООП и, в худшем случае, ночным кошмаром для новинок. - Из Auryn Wiki

Введите Auryn

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

Зависимости класса Auto-Wire от Auryn, основанные на сигнатуре конструктора классов. Что это означает, что для каждого запрошенного класса Auryn находит его, вычисляет, что ему нужно в конструкторе, создает то, что ему нужно, а затем создает экземпляр класса, который вы попросили изначально. Вот как это работает:

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

... и если вы знаете что-нибудь о отражение PHP, вы узнаете, что некоторые люди называют это "медленным". Итак, что делает Аурин об этом:

Возможно, вы слышали, что "отражение медленное". Дайте понять что-то: все может быть "слишком медленным", если вы делаете это неправильно. Отражение на порядок быстрее, чем доступ к диску, и на несколько порядков быстрее, чем получение информации (например) из удаленной базы данных. Кроме того, каждое отражение дает возможность кэшировать результаты, если вы беспокоитесь о скорости. Аурин кэширует любые отражения, которые он генерирует, чтобы свести к минимуму потенциальное воздействие на производительность.

Итак, теперь мы пропустили аргумент "отражение медленный", вот как я его использовал.

Как я использую Auryn

  • Я делаю Auryn частью моего автозагрузчика. Это так, что когда класс запрашивается, Аурин может уйти и прочитать класс и его зависимости, а также зависимости зависимостей (и т.д.) И вернуть их все в класс для создания экземпляра. Я создаю объект Auyrn.

    $injector = new \Auryn\Provider(new \Auryn\ReflectionPool);
    
  • Я использую интерфейс базы данных в качестве требования в конструкторе моего класса базы данных. Поэтому я говорю Auryn, какую конкретную реализацию использовать (это та часть, которую вы меняете, если хотите создать экземпляр базы данных другого типа в одной точке вашего кода, и все это будет работать).

    $injector->alias('Library\Database\DatabaseInterface', 'Library\Database\MySQL');
    

Если бы я захотел перейти на MongoDB, и я написал для него класс, я бы просто изменил Library\Database\MySQL на Library\Database\MongoDB.

  • Затем я передаю $injector в мой маршрутизатор, а при создании контроллера/метода это означает, что зависимости автоматически разрешаются.

    public function dispatch($injector)
    {
        // Make sure file / controller exists
        // Make sure method called exists
        // etc...
    
        // Create the controller with it required dependencies
        $class = $injector->make($controller);
        // Call the method (action) in the controller
        $class->$action();
    }
    

Наконец, ответьте на вопрос OP

Хорошо, поэтому, используя эту технику, скажем, у вас есть пользовательский контроллер, который требует User Service (пусть UserModel), который требует доступа к базе данных.

class UserController
{
    protected $userModel;

    public function __construct(Model\UserModel $userModel)
    {
        $this->userModel = $userModel;
    }
}

class UserModel
{
    protected $db;

    public function __construct(Library\DatabaseInterface $db)
    {
        $this->db = $db;
    }
}

Если вы используете код в маршрутизаторе, Auryn выполнит следующее:

  • Создайте библиотеку \DatabaseInterface, используя MySQL как конкретный класс (alias'd в boostrap)
  • Создайте "UserModel" с ранее созданной базой данных, введенной в нее
  • Создайте UserController с ранее созданным UserModel, введенным в него

Это рекурсия прямо там, и это "автоматическая проводка", о которой я говорил раньше. И это решает проблему OPs, потому что , только если иерархия классов содержит объект базы данных в качестве требования к конструктору. - объект, надутый, не при каждом запросе.

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

RE: Как сделать так, чтобы при необходимости вызывался метод connect. Это действительно просто.

  • Убедитесь, что в конструкторе вашего класса базы данных вы не создаете экземпляр объекта, вы просто передаете ему настройки (host, dbname, user, password).
  • Имейте метод подключения, который фактически выполняет объект new PDO(), используя настройки классов.

    class MySQL implements DatabaseInterface
    {
        private $host;
        // ...
    
        public function __construct($host, $db, $user, $pass)
        {
            $this->host = $host;
            // etc
        }
    
        public function connect()
        {
            // Return new PDO object with $this->host, $this->db etc
        }
    }
    
  • Итак, теперь каждый класс, который вы передаете базе данных, будет иметь этот объект, но пока не будет иметь соединение, потому что connect() не был вызван.

  • В соответствующей модели, которая имеет доступ к классу базы данных, вы вызываете $this->db->connect();, а затем продолжаете то, что хотите.

По сути, вы по-прежнему передаете свой объект базы данных требуемым классам, используя методы, описанные мной ранее, но для того, чтобы решить, когда выполнять соединение по методу по методу, вы просто запускаете метод подключения в требуемом. Нет, вам не нужен синглтон. Вы просто скажите, когда нужно подключиться, когда вы хотите, и это не произойдет, когда вы не скажете, чтобы он подключился.


Старый ответ

Я расскажу немного более подробно о контейнерах для инъекций зависимостей, и как они могут помочь вашей ситуации. Примечание. Знание принципов "MVC" поможет здесь значительно.

Проблема

Вы хотите создать некоторые объекты, но только некоторые из них нуждаются в доступе к базе данных. То, что вы сейчас делаете, это создание объекта базы данных по каждому запросу, что совершенно необязательно, а также полностью распространено перед использованием таких вещей, как контейнеры DiC.

Два примера объектов

Вот пример двух объектов, которые вы можете создать. Нужен доступ к базе данных, другой не нужен доступ к базе данных.

/**
 * @note: This class requires database access
 */
class User
{
    private $database;

    // Note you require the *interface* here, so that the database type
    // can be switched in the container and this will still work :)
    public function __construct(DatabaseInterface $database)
    {
        $this->database = $database;
    }
}

/**
 * @note This class doesn't require database access
 */
class Logger
{
    // It doesn't matter what this one does, it just doesn't need DB access
    public function __construct() { }
}

Итак, какой лучший способ создать эти объекты и обработать их соответствующие зависимости, а также передать объект базы данных только соответствующему классу? Нам повезло, эти два человека работают вместе в гармонии при использовании Контейнера для инъекций зависимостей.

Введите Pimple

Pimple - действительно классный контейнер для инъекций зависимостей (создателями структуры Symfony2), который использует Закрытие PHP 5.3+.

То, как прыщ делает это действительно здорово - объект, который вы хотите, не создается, пока вы его не попросите. Таким образом, вы можете настроить загрузку новых объектов, но пока вы их не попросите, они не создаются!

Вот действительно простой пример прыщей, который вы создаете в своем boostrap:

// Create the container
$container = new Pimple();

// Create the database - note this isn't *actually* created until you call for it
$container['datastore'] = function() {
    return new Database('host','db','user','pass');
};

Затем вы добавляете объект User и объект Logger здесь.

// Create user object with database requirement
// See how we're passing on the container, so we can use $container['datastore']?
$container['User'] = function($container) {
    return new User($container['datastore']);
};

// And your logger that doesn't need anything
$container['Logger'] = function() {
    return new Logger();
};

Awesome! Итак, как я могу использовать объект $container?

Хороший вопрос! Таким образом, вы уже создали объект $container в бутстрапе и настроили объекты и их необходимые зависимости. В своем механизме маршрутизации вы передаете контейнер своему контроллеру.

Примечание: пример элементарного кода

router->route('controller', 'method', $container);

В вашем контроллере вы получаете доступ к параметру $container, который передается, и когда вы запрашиваете у него объект пользователя, вы возвращаете новый объект User (factory -style) с уже введенным объектом базы данных!

class HomeController extends Controller
{
    /**
     * I'm guessing 'index' is your default action called
     *
     * @route /home/index
     * @note  Dependant on .htaccess / routing mechanism
     */
    public function index($container)
    {
        // So, I want a new User object with database access
        $user = $container['User'];

       // Say whaaat?! That it? .. Yep. That it.
    }
}

Что вы решили

Итак, теперь вы убили нескольких птиц (не только два) одним камнем.

  • Создание объекта БД по каждому запросу - не больше! Он создается только тогда, когда вы просите об этом из-за закрытия Pimple использует
  • Удаление новых ключевых слов с вашего контроллера. Да, это так. Вы передали эту ответственность за контейнер.

Примечание. Прежде чем продолжить, я хочу указать, насколько важна вторая точка маркера. Без этого контейнера, допустим, вы создали 50 пользовательских объектов во всем приложении. Затем в один прекрасный день вы хотите добавить новый параметр. OMG - теперь вам нужно пройти все ваше приложение и добавить этот параметр к каждому new User(). Однако с DiC - если вы используете $container['user'] всюду, вы просто добавляете этот третий параметр в контейнер один раз и это. Да, это потрясающе.

  • Возможность переключения баз данных. Вы меня слышали, и все дело в том, что если вы хотите перейти с MySQL на PostgreSQL, вы измените код в своем контейнере, чтобы вернуть новый тип базы данных, которую вы закодировали, и до тех пор, пока все это возвращает тот же материал, что и он! Возможность менять конкретные реализации, о которых каждый всегда говорит.

Важная часть

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

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

Ответ 2

Это примерно то, что я использую.

class Database {

    protected static $connection;

    // this could be public if you wanted to be able to get at the core database
    // set the class variable if it hasn't been done and return it
    protected function getConnection(){
        if (!isset(self::$connection)){
            self::$connection = new mysqli($args);
        }
        return self::$connection;
    }
    // proxy property get to contained object 
    public function __get($property){
        return $this->getConnection()->__get($property);
    }
    // proxy property set to contained object
    public function __set($property, $value){
        $this->getConnection()->__set($property, $value);
    }

    // proxy method calls to the contained object
    public function __call($method, $args){
        return call_user_func_array(array($this->getConnection(), $method), $args);
    }

    // proxy static method calls to the contained object
    public function __callStatic($method, $args){
        $connClass = get_class($this->getConnection());
        return call_user_func_array(array($connClass, $method), $args);
    }
}

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

Ответ 3

Вот пример простого подхода:

class Database {
  public $connection = null ;

  public function __construct($autosetup = false){
    if ($autosetup){
      $this->setConnection() ;
    }
  }

  public function getProducts(){//Move it to another class if you wish
    $this->query($sql_to_get_products);
  }

  public function query($sql) {
    if (!$connection || !$connection->ping()){
      $this->setupConnection() ;
    }
    return $this->connection->query($sql);
  }

  public function setConnection(){
    $this->connection = new MySQLi($a, $b, $c, $d) ;
  }

  public function connectionAvailable(){
    return ($connection && $connection->ping()) ;
  }
}

Ответ 4

Посмотрите на использование контейнера инъекций зависимостей, что-то вроде Pimple было бы неплохо начать. С контейнером для инъекций зависимостей вы научитесь контейнеру создавать объекты в своем приложении, они не создаются, пока вы не попросите их. С помощью Pimple вы можете настроить ресурс, который будет использоваться совместно, чтобы он когда-либо создавался только один раз во время запроса независимо от того, как часто вы запрашиваете у него контейнер.

Вы можете настроить классы для принятия контейнера в своем конструкторе или использовать метод setter для ввода в ваш класс.

Упрощенный пример может выглядеть так:

<?php

// somewhere in your application bootstrap

$container = new Pimple();
$container['db'] = $container->share(
  function ($c) {
    return new Database();
  }
);

// somewhere else in your application

$foo = new Foo($container);

// somewhere in the Foo class definition

$bar = $this->container['db']->getBars();

Надеюсь, что это поможет.

Ответ 5

У вас уже есть отличные ответы, причем большинство из них концентрируются на аспекте инъекционных зависимостей (что хорошо) и создают только объекты по запросу.

Другим аспектом является более важный: не помещайте код, который делает какую-либо тяжелую работу в ваших конструкторах. В случае объекта базы данных это означает: Не подключайтесь к базе данных внутри конструктора.

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

Создание объекта в PHP разумно быстро. Код класса обычно доступен в кеше кода операции, поэтому он вызывает только вызов автозагрузчика и затем выделяет некоторые байты в памяти для свойств объектов. После этого будет запускаться конструктор. Если единственное, что он делает, это копирование параметров конструктора в локальные переменные свойств, это даже оптимизировано PHP с помощью ссылок "копирование на запись". Таким образом, нет никакой реальной выгоды, если этот объект не будет создан в первую очередь, если вы не сможете его избежать. Если вы можете: еще лучше.

Ответ 6

Так я использую mysqli. Объект базы данных ведет себя так же, как объект mysqli, может добавлять мои собственные методы или переопределять существующие, и единственное различие заключается в том, что фактическое соединение с базой данных не устанавливается при создании объекта, а при первом вызове метода или свойства, которому требуется соединение.

class Database {
    private $arguments = array();
    private $link = null;

    public function __construct() {
        $this->arguments = func_get_args();
    }

    public function __call( $method, $arguments ) {
        return call_user_func_array( array( $this->link(), $method ), $arguments );
    }

    public function __get( $property ) {
        return $this->link()->$property;
    }

    public function __set( $property, $value ){
        $this->link()->$property = $value;
    }

    private function connect() {
        $this->link = call_user_func_array( 'mysqli_connect', $this->arguments );
    }

    private function link() {
        if ( $this->link === null ) $this->connect();
        return $this->link;
    }
}

Другим способом достижения такого же поведения является использование методов mysqli_init() и mysqli_real_connect(), конструктор инициализирует объект с помощью mysqli_init(), и когда вам нужно реальное соединение, используется метод mysqli_real_connect().

class Database {
    private $arguments = array();

    public function __construct() {
        $this->arguments = array_merge( array( 'link' => mysqli_init() ), func_get_args() );
    }

    public function __call( $method, $arguments ) {
        return call_user_func_array( array( $this->link(), $method ), $arguments );
    }

    public function __get( $property ) {
        return $this->link()->$property;
    }

    public function __set( $property, $value ) {
        $this->link()->$property = $value;
    }

    private function connect() {
        call_user_func_array( 'mysqli_real_connect', $this->arguments );
    }

    private function link() {
        if ( [email protected]$this->arguments['link']->thread_id ) $this->connect();
        return $this->arguments['link'];
    }
}

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

Ответ 7

Я родом из мира Java. Java резидентна в памяти по сравнению с запросами HTML без учета состояния. PHP нет. Это совершенно другая история - и что мне нравится в PHP.

Я просто использую:   $ conn = @pg_connect (DBConnection);

DBConnection - это определение, содержащее информацию о хосте и т.д. @Гарантирует, что текущее соединение используется или создается новый. Как я могу сделать это легче?

Данные о том, как подключиться к базе данных, являются стабильными. Само соединение может быть воссоздано во время запроса. Зачем мне лучше программировать людей PHP и воссоздавать @? Они сделали это для сообщества PHP, давайте использовать его.

Кстати, никогда не ставьте тяжелые объекты в конструктор и никогда не позволяйте конструктору выполнять какую-то тяжелую работу и не допускать, чтобы при построении объекта можно было исключить исключение. У вас может быть незавершенный объект, находящийся в вашей памяти. Предпочтительным является метод init. Я согласен с этим с Энрике Барселосом.

Ответ 8

interface IDatabase {
    function connect();
}

class Database implements IDatabase
{
    private $db_type;
    private $db_host;
    private $db_name;
    private $db_user;
    private $db_pass;
    private $connection = null;

    public function __construct($db_type, $db_host, $db_name, $db_user, $db_pass)
    {
        $this->db_type = $db_type;
        $this->db_host = $db_host;
        $this->db_name = $db_name;
        $this->db_user = $db_user;
        $this->db_pass = $db_pass;
    }

    public function connect()
    {
        if ($this->connection === null) {
            try {
                $this->connection = new PDO($this->db_type.':host='.$this->db_host.';dbname='.$this->db_name, $this->db_user, $this->db_pass);
                $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                return $this->connection;
            } catch (PDOException $e) {
                return $e;
            }
        } else {
            return $this->connection;
        }
    }
}

Как насчет этого? В connect() проверьте, установлено ли соединение, если да, верните его, если нет, создайте его и верните. Это не даст вам открыть много связей. Скажем, в действии вашего контроллера вы хотите вызвать два метода UserRepository (которые зависят от базы данных), getUsers() и getBlockedUsers(), если вы вызовете эти методы, connect() будет вызываться в каждом из них, с этой проверкой он вернет уже существующий экземпляр.

Ответ 9

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

$db = DB::instance();

где DB:: instance объявлен как-то вроде этого

class DB {

    //...

    private static $instance;    

    public static function instance() {
        if (self::$instance == null) {
            self::$instance = new self();
        }
    }

    //...

}

Ответ 10

 <?php

    mysql_select_db('foo',mysql_connect('localhost','root',''))or die(mysql_error());
    session_start();

    function antiinjection($data)
    {
        $filter_sql = stripcslashes(strip_tags(htmlspecialchars($data,ENT_QUOTES)));
        return $filter_sql;
    }

    $username = antiinjection($_POST['username']);
    $password = antiinjection($_POST['password']);

    /* student */
        $query = "SELECT * FROM student WHERE username='$username' AND password='$password'";
        $result = mysql_query($query)or die(mysql_error());
        $row = mysql_fetch_array($result);
        $num_row = mysql_num_rows($result);
    /* teacher */
    $query_teacher = mysql_query("SELECT * FROM teacher WHERE username='$username' AND password='$password'")or die(mysql_error());
    $num_row_teacher = mysql_num_rows($query_teacher);
    $row_teahcer = mysql_fetch_array($query_teacher);
    if( $num_row > 0 ) { 
    $_SESSION['id']=$row['student_id'];
    echo 'true_student';    
    }else if ($num_row_teacher > 0){
    $_SESSION['id']=$row_teahcer['teacher_id'];
    echo 'true';

     }else{ 
            echo 'false';
    }   

    ?>

и в файле php вставьте javascript

   <script>
                    jQuery(document).ready(function(){
                    jQuery("#login_form1").submit(function(e){
                            e.preventDefault();
                            var formData = jQuery(this).serialize();
                            $.ajax({
                                type: "POST",
                                url: "login.php",
                                data: formData,
                                success: function(html){
                                if(html=='true')
                                {
                                    window.location = 'folder_a/index.php';  
                                }else if (html == 'true_student'){
                                    window.location = 'folder_b/index.php';  
                                }else
                                {
                                    { header: 'Login Failed' };
                                }
                                }
                            });
                            return false;
                        });
                    });
                    </script>

другое соединение

    <?php

  class DbConnector {

   var $theQuery;
   var $link;

   function DbConnector(){

    // Get the main settings from the array we just loaded
    $host = 'localhost';
    $db = 'db_lms1';
    $user = 'root';
    $pass = '';

    // Connect to the database
    $this->link = mysql_connect($host, $user, $pass);
    mysql_select_db($db);
    register_shutdown_function(array(&$this, 'close'));

}

    //*** Function: query, Purpose: Execute a database query ***
function query($query) {

    $this->theQuery = $query;
    return mysql_query($query, $this->link);

}

//*** Function: fetchArray, Purpose: Get array of query results ***
function fetchArray($result) {

    return mysql_fetch_array($result);

}

//*** Function: close, Purpose: Close the connection ***
function close() {

    mysql_close($this->link);

}

  }

  ?>