20

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

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

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

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

4

10 回答 10

45

注意:虽然直接回答ops 问题,“我什么时候只能在需要时而不是在每个请求时创建/连接到数据库”是在您需要时注入它,只是说这没有帮助。我在这里解释你实际上是如何正确地做到这一点的,因为在非特定框架上下文中确实没有很多有用的信息可以在这方面提供帮助。


更新:这个问题的“旧”答案可以在下面看到。这鼓励了服务定位器模式,这种模式非常有争议,对许多人来说是“反模式”。新的答案加上我从研究中学到的东西。请先阅读旧答案,看看进展如何。

新答案

在使用 pimple 一段时间后,我了解了很多关于它的工作原理,以及它实际上并没有那么神奇。它仍然很酷,但它只有 80 行代码的原因是它基本上允许创建闭包数组。Pimple 经常用作服务定位器(因为它的实际功能非常有限),这是一种“反模式”。

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

服务定位器模式是一种用于软件开发的设计模式,用于封装获得具有强大抽象层的服务所涉及的过程。此模式使用称为“服务定位器”的中央注册表,它根据请求返回执行特定任务所需的信息。

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

为什么服务定位器不好?

你说这个有什么问题?主要问题是这种方法隐藏了类的依赖关系。因此,如果开发人员要更新这个类并且他们以前没有见过它,他们将看到一个包含未知数量对象的容器对象。此外,测试这个类将是一场噩梦。

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

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

Front Controller --> Bootstrap --> Router --> Controller/Method --> Model [Services|Domain Objects|Mappers] --> Controller --> View --> Template

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

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

在现实生活中,您不会通过将整个五金店(希望如此)运送到建筑工地来建造房屋,这样您就可以访问您需要的任何零件。相反,工头 (__construct()) 询问将需要的特定部件(门和窗)并着手采购它们。您的对象应该以相同的方式运行;他们应该只询问完成工作所需的特定依赖项。让 House 访问整个硬件商店充其量是糟糕的 OOP 风格,最坏的情况是可维护性的噩梦。- 来自 Auryn 维基

进入奥林

在这方面,我想向您介绍一个名为Auryn的出色的东西,它是由Rdlowrey写的,我是在周末被介绍给我的。

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

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

...如果您对PHP 的反射有所了解,您就会知道有些人称它为“慢”。所以这就是 Auryn 的做法:

您可能听说过“反射很慢”。让我们澄清一下:如果你做错了,任何事情都可能“太慢”。反射比磁盘访问快一个数量级,比从远程数据库检索信息(例如)快几个数量级。此外,如果您担心速度,每个反射都提供了缓存结果的机会。Auryn 缓存它生成的任何反射,以最大限度地减少潜在的性能影响。

所以现在我们已经跳过了“反射很慢”的论点,这就是我一直在使用它的方式。

我如何使用 Auryn

  • 我让 Auryn 成为我的 autoloader 的一部分。这样当一个类被请求时,Auryn 可以离开并读取该类及其依赖项,以及依赖项的依赖项(等等),并将它们全部返回到类中以进行实例化。我创建了 Auyrn 对象。

    $injector = new \Auryn\Provider(new \Auryn\ReflectionPool);
    
  • 我在数据库类的构造函数中使用数据库接口作为要求。所以我告诉 Auryn 使用哪个具体的实现(如果你想在代码中的一个点实例化不同类型的数据库,这是你改变的部分,它仍然可以工作)。

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

如果我想更改为 MongoDB 并为它编写了一个类,我会简单地更改Library\Database\MySQLLibrary\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的问题

好的,所以使用这种技术,假设您有一个用户控制器,它需要用户服务(比如用户模型),它需要数据库访问。

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 中使用别名)
  • 使用之前创建的数据库创建“用户模型”注入其中
  • 创建 UserController 并将之前创建的 UserModel 注入其中

这就是递归,这就是我之前所说的“自动布线”。这解决了 OPs 问题,因为只有当类层次结构包含数据库对象作为构造函数要求时,才会实例化对象,而不是在每个请求时

此外,每个类都有它们在构造函数中运行所需的确切要求,因此没有像服务定位器模式那样隐藏的依赖关系。

RE:如何制作以便在需要时调用连接方法。这真的很简单。

  1. 确保在数据库类的构造函数中,不要实例化对象,只需传入它的设置(主机、数据库名、用户、密码)。
  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. 在可以访问 Database 类的相关模型中,您调用$this->db->connect();然后继续您想做的事情。

本质上,您仍然使用我之前描述的方法将您的数据库对象传递给需要它的类,但是要决定何时在逐个方法的基础上执行连接,您只需运行所需的 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是一个非常酷的依赖注入容器(由 Symfony2 框架的制造商提供),它利用了PHP 5.3+ 的闭包

pimple 的做法真的很酷——你想要的对象在你直接请求之前不会被实例化。因此,您可以设置大量新对象,但在您请求它们之前,它们不会被创建!

这是您在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传入的参数,当您从它请求用户对象时,您会返回一个新的用户对象(工厂样式),其中数据库对象已经注入!

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.
    }
}

你解决了什么

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

  • 在每个请求上创建一个 DB 对象- 不再是!它仅在您请求时创建,因为 Pimple 使用了闭包
  • 从控制器中删除“新”关键字- 是的,没错。您已将此责任移交给容器。

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

  • 切换数据库的能力——你听我说,重点是如果你想从 MySQL 更改为 PostgreSQL——你更改容器中的代码以返回你编码的新的不同类型的数据库,并且作为只要它都返回相同的东西,就是这样!交换每个人都在谈论的具体实现的能力。

重要的部分

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

我希望你觉得这很有用。我在这里完成的方式至少为我节省了大量的开发时间,而且启动起来很有趣!

于 2013-05-17T09:20:49.193 回答
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 方法中的后期静态绑定。

于 2013-05-17T02:48:11.947 回答
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()) ;
  }
}
于 2013-05-09T23:44:33.067 回答
1

考虑使用依赖注入容器,像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();

希望能帮助到你。

于 2013-05-10T01:25:21.593 回答
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(),有了这个检查,它将返回已经存在的实例。

于 2013-10-17T17:31:00.077 回答
0

您已经得到了一些很好的答案,大多数人都集中在注入依赖项方面(这是一件好事),并且只按需创建对象。

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

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

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

于 2013-05-18T00:59:57.633 回答
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'];
    }
}

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

于 2013-05-20T13:04:33.713 回答
0

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

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

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

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

顺便说一句,永远不要把重的对象放在构造函数中,不要让构造函数做一些繁重的工作,也不要让构造对象的过程中发生异常。你的记忆中可能有一个未完成的对象。首选初始化方法。我同意 Henrique Barcelos 的观点。

于 2013-05-21T15:03:19.223 回答
-1

您可以使用单例模式来实现这一点,并在每次需要数据库时请求数据库对象。这会导致这样的事情

$db = DB::instance();

其中 DB::instance 声明如下

class DB {

    //...

    private static $instance;    

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

    //...

}
于 2013-05-09T23:44:59.153 回答
-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);

}

  }

  ?>
于 2017-12-07T16:57:21.493 回答