设计模式:如何仅在需要时创建数据库对象/连接?

时间:2013-05-09 23:37:36

标签: php oop design-patterns database-connection

我有一个简单的应用程序,比如它有一些类和一个处理数据库请求的“额外”类。目前我每次使用应用程序时都会创建数据库对象,但在某些情况下,不需要数据库连接。我这样做(PHP顺便说一句):

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

但有时$foo对象不需要db访问,因为只调用没有数据库操作的方法。所以我的问题是:处理这种情况的专业方法是什么/如何仅在需要时创建数据库连接/对象?

我的目标是避免不必要的数据库连接。

10 个答案:

答案 0 :(得分:44)

  

注意:虽然直接回答到操作问题,"何时我只能在需要时创建/连接到数据库,而不是每次请求都会创建/连接到数据库&#34 ;是在你需要的时候注入,只是说有帮助。我在这里解释你如何正确地解决这个问题,因为在非特定框架环境中确实没有很多有用的信息来帮助解决这个问题。

     
     

已更新:' old'回答这个问题可以在下面看到。这鼓励了服务定位器模式,这是一个非常有争议的模式和许多“反模式”。新的答案补充了我从研究中学到的东西。 请先阅读旧答案,看看这是如何发展的。

新答案

在使用疙瘩一段时间之后,我学到了很多关于它是如何工作的,以及它毕竟不是实际上那么神奇。它仍然很酷,但它只有80行代码的原因是因为它基本上允许创建一个闭包数组。疙瘩被广泛用作服务定位器(因为它实际上可以做到如此限制),这是一种反模式"。

首先,什么是服务定位器?

  

服务定位器模式是软件开发中使用的设计模式,用于封装获取具有强抽象层的服务所涉及的过程。此模式使用称为"服务定位器的中央注册表"根据要求返回执行某项任务所需的信息。

我在引导程序中创建了疙瘩,定义了依赖项,然后将此容器传递给我实例化的每个单独的类。

为什么服务定位器不好?

你说的问题是什么?主要问题是这种方法隐藏了类中的依赖。因此,如果开发人员要更新此课程而他们之前还没有看过,那么他们将会看到一个包含未知数量的对象的容器对象。此外,测试这个课程将是一个噩梦。

为什么我最初这样做? 因为我认为控制器之后的是你开始进行依赖注入的地方。这是错误。你可以直接在控制器级别启动它。

如果这是我的应用程序中的工作方式:

  

前端控制器 - > Bootstrap - > 路由器 - > 控制器/方法 - > 模型[服务|域对象|映射器] - > 控制器 - > 查看 - >的模板

...然后依赖注入容器应该在第一个控制器级别立即开始工作。

所以,如果我仍然使用疙瘩,我将定义将要创建哪些控制器,以及他们需要什么。因此,您将将视图和任何内容从模型层注入控制器,以便它可以使用它。这是Inversion Of Control,使测试更容易。来自Aurn wiki,(我很快就会谈到):

  

在现实生活中,您不会通过将整个五金店(希望)运送到施工现场来建造房屋,这样您就可以访问所需的任何部件。相反,领班(__construct())要求提供所需的特定部分(门和窗)并开始采购它们。你的对象应该以相同的方式运作;他们应该只询问完成工作所需的具体依赖性。让房子进入整个五金店是最糟糕的OOP风格,最糟糕的是可维护性的噩梦。 - From the Auryn Wiki

输入Auryn

就此而言,我想向您介绍一些名为Auryn的精彩内容,由Rdlowrey撰写,我周末被介绍过。

Auryn' auto-wires'基于类构造函数签名的类依赖项。这意味着,对于请求的每个类,Auryn找到它,在构造函数中找出它需要的内容,首先创建它需要的内容,然后创建最初请求的类的实例。以下是它的工作原理:

  

Provider根据构造函数方法签名中指定的参数type-hints递归地实例化类依赖项。

...如果您对PHP's reflection了解任何信息,您就会知道有些人称之为“慢”'所以这就是Auryn对此所做的事情:

  

你可能听说过"反思很慢"。让我们明白一些事情:任何事情都可能过得太慢"如果你做错了。反射比磁盘访问快一个数量级,比从远程数据库检索信息(例如)快几个数量级。此外,如果您担心速度,每次反射都可以缓存结果。 Auryn会缓存它产生的任何反射,以最大限度地降低潜在的性能影响。

所以现在我们已经跳过"反思很慢"争论,这就是我如何使用它。

我如何使用Auryn

  • I make Auryn part of my autoloader。这样,当一个类被要求时,Auryn可以离开并阅读该课程及其依赖关系,以及它的依赖关系'依赖项(etc),并将它们全部返回到类中进行实例化。我创建了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's required dependencies
        $class = $injector->make($controller);
        // Call the method (action) in the controller
        $class->$action();
    }
    

最后,回答OP的问题

好的,所以使用这种技术,让我们说你有用户控制器需要用户服务(让我们说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将执行以下操作:

  • 创建Library \ DatabaseInterface,使用MySQL作为具体类(在boostrap中使用别名)
  • 创建' UserModel'将之前创建的数据库注入其中
  • 创建UserController,并将先前创建的UserModel注入其中

这就是那里的递归,这就是“自动布线”#39;我之前谈的是。这解决了OP问题,因为只有当类层次结构包含数据库对象作为构造函数要求时才是实例化的对象,不是每次请求

此外,每个类都完全具有在构造函数中运行所需的要求,因此没有隐藏的依赖关系就像服务定位器模式一样。

RE:如何使它在需要时调用connect方法。这很简单。

  1. 确保在Database类的构造函数中,您没有实例化对象,只需传入它的设置(主机,dbname,用户,密码)。
  2. 有一个实际执行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
        }
    }
    
  3. 现在,您传递数据库的每个类都将拥有此对象,但由于尚未调用connect(),因此无法建立连接。

  4. 在有权访问数据库类的相关模型中,您调用$this->db->connect();,然后继续执行您想要执行的操作。
  5. 本质上,您仍然使用我之前描述的方法将数据库对象传递给需要它的类,但是要决定何时在逐个方法的基础上执行连接 ,您只需运行所需的连接方法。不,你不需要单身人士。你只需告诉它什么时候连接就可以了,当你没有告诉它连接时它就没有了。


    旧答案

      

    我将更深入地解释依赖注入容器,以及它们如何可以帮助您的情况。注意:理解' 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+'s closures

    痘痘做的方式真的很酷 - 你想要的对象在你直接要求之前不会被实例化。所以你可以设置一堆新对象,但在你要求它们之前,它们是不是被创造出来了!

    这是一个非常简单的疙瘩示例,您在 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();
    };
    

    真棒!那么..我如何实际使用$ container对象?

    好问题!因此,您已经在引导程序中创建了$container对象,并设置了对象及其所需的依赖项。在路由机制中,将容器传递给控制器​​。

    注意:示例基本代码

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

    在您的控制器中,您访问传入的$container参数,当您从中请求用户对象时,您将返回一个新的User对象(工厂样式),数据库对象已经注入!

    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's it? .. Yep. That's it.
        }
    }
    

    你解决了什么

    所以,你现在用一块石头杀死了多只鸟(不仅仅是两只)。

    • 在每个请求上创建数据库对象 - 不再了!它只是在你要求它时创建的,因为关闭了Pimple使用
    • 删除' new'控制器中的关键字 - 是的,这是对的。您已将此责任交给集装箱。

    注意:在继续之前,我想指出第二点是多么重要。如果没有这个容器,请假设您在整个应用程序中创建了50个用户对象。然后有一天,您想要添加一个新参数。 OMG - 您现在需要浏览整个应用程序并将此参数添加到每个new User()。但是,使用DiC - 如果您在任何地方使用$container['user'],只需将第三个参数添加到容器一次,就可以了。是的,这完全是真棒。

    • 切换数据库的能力 - 您听说过我,其重点是如果您想从MySQL更改为PostgreSQL - 您更改容器中的代码以返回新的不同您编码的数据库类型,只要它们都返回相同类型的内容,那就是它! 交换具体实现的能力,每个人都会大肆宣传。

    重要部分

    这是一种使用容器的方式,它只是一个开始。有很多方法可以做到这一点 - 例如,您可以使用反射/某种映射来决定容器的哪些部分是必需的,而不是将容器交给每个方法。自动化,你就是金色的。

    我希望你发现这很有用。我在这里完成它的方式至少为我减少了大量的开发时间,并且开机很有趣!

答案 1 :(得分:3)

这与我的使用情况差不多。

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方法中的后期静态绑定。

答案 2 :(得分:2)

以下是一个简单方法的示例:

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()) ;
  }
}

答案 3 :(得分:1)

考虑使用依赖注入容器,像Pimple之类的东西将是一个很好的起点。使用依赖注入容器,您可以“教”容器如何在应用程序中创建对象,在您要求它们之前,它们不会被实例化。使用Pimple,您可以将资源配置为 shared ,这样无论您多久向容器请求它,它都只会在请求期间进行一次实例化。

您可以设置类以在其构造函数中接受容器,或使用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();

希望它有所帮助。

答案 4 :(得分:0)

你已经得到了一些很好的答案,大多数人都专注于注入依赖关系(这是一件好事),只是按需创建对象。

另一个方面是更重要的一个方面:不要将任何繁重工作的代码放入构造函数中。对于数据库对象,这意味着:不要连接到构造函数内的数据库。

为什么这更重要?因为如果始终创建using对象,但是并不总是运行查询,因为没有创建使用对象也不会创建数据库对象,因此不是真正的优化。

在PHP中创建对象是合理的。类代码通常在操作码缓存中可用,因此它只触发对自动加载器的调用,然后在内存中为对象的属性分配一些字节。构造函数将在此之后运行。如果它唯一能做的就是将构造函数参数复制到本地属性变量,这甚至可以通过PHP使用“copy-on-write”引用进行优化。因此,如果您不能首先创建此对象,那么没有真正的好处,如果您无法避免它。如果你能:甚至更好。

答案 5 :(得分:0)

这是我使用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 ( !@$this->arguments['link']->thread_id ) $this->connect();
        return $this->arguments['link'];
    }
}

我测试了两种方法的内存消耗并得到了意想不到的结果,第二种方法在连接数据库并执行查询时使用的资源更少。

答案 6 :(得分:0)

我来自Java世界。 Java通过无状态HTML请求驻留在内存中。 PHP不是。这是一个完全不同的故事 - 我喜欢PHP。

我只是使用:     $ conn = @pg_connect(DBConnection);

DBConnection是一个包含有关主机等信息的定义。 @确保使用当前连接或创建新连接。我怎样才能更容易地做到这一点?

如何连接数据库的数据是稳定的。可以在请求期间重新创建连接本身。为什么我应该比PHP的人更好地编程并重新创建@?他们为PHP社区做了这个,让我们使用它。

顺便说一句,永远不要在构造函数中放置重对象,也不要让构造函数做一些繁重的工作,也不要让构造函数在构造对象时抛出异常。你的遗体中可能有一个未完成的物体。 init方法是首选方法。我同意Henrique Barcelos的意见。

答案 7 :(得分:0)

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()将在每个方法中调用,通过此检查,它将返回已存在的实例。

答案 8 :(得分:-1)

您可以使用单例模式来实现此功能,并在每次需要数据库时请求数据库对象。这会产生类似的结果

$db = DB::instance();

其中DB :: instance声明为此类

class DB {

    //...

    private static $instance;    

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

    //...

}

答案 9 :(得分:-1)

 <?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);

}

  }

  ?>