20

私は単純なアプリケーションを持っています。いくつかのクラスと、データベース要求を処理する「追加の」クラスがあると言います。現在、アプリを使用するたびにデータベース オブジェクトを作成していますが、データベース接続が不要な場合もあります。私はこのようにやっています(PHP btw):

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

ただし、$fooデータベース アクションのないメソッドのみが呼び出されるため、オブジェクトが db アクセスを必要としない場合もあります。だから私の質問は次のとおりです:このような状況を処理する専門的な方法は何ですか/必要な場合にのみデータベース接続/オブジェクトを作成する方法は?

私の目標は、不要なデータベース接続を避けることです。

4

10 に答える 10

45

注: opsの質問に対する直接的な答えは、「すべての要求ではなく、必要なときにのみデータベースを作成/接続できるのはいつですか」は、必要なときに注入することですが、単に役に立たないと言っています。この点に関して、非特定のフレームワークのコンテキストでは実際に役立つ情報があまりないため、実際にどのようにそれを正しく行うかを説明しています。


更新:この質問に対する「古い」回答は以下を参照してください。これにより、非常に物議を醸し、多くの人にとって「アンチパターン」であるサービスロケーターパターンが助長されました。調査から学んだことで新しい回答が追加されました。最初に古い回答を読んで、これがどのように進行したかを確認してください。

新しい答え

にきびをしばらく使用した後、私はそれがどのように機能するか、そして実際にはそれほど驚くべきことではないことについて多くのことを学びました. それでもかなりクールですが、コードが 80 行しかないのは、基本的にクロージャーの配列を作成できるためです。Pimple はサービス ロケータとしてよく使用されます (実際にできることは非常に限られているため)。これは「アンチパターン」です。

まず、サービスロケーターとは何ですか?

サービス ロケーター パターンは、ソフトウェア開発で使用される設計パターンであり、強力な抽象化レイヤーを使用してサービスの取得に関連するプロセスをカプセル化します。このパターンは、「サービス ロケータ」と呼ばれる中央レジストリを使用します。このレジストリは、要求に応じて、特定のタスクを実行するために必要な情報を返します。

ブートストラップでピンプルを作成し、依存関係を定義してから、インスタンス化したすべてのクラスにこのコンテナを渡しました。

サービスロケーターが悪いのはなぜですか?

あなたが言うこの問題は何ですか?主な問題は、このアプローチが依存関係をクラスから隠していることです。したがって、開発者がこのクラスを更新しようとしていて、これまでに見たことがない場合、不明な量のオブジェクトを含むコンテナ オブジェクトが表示されることになります。また、このクラスをテストするのは少し悪夢のようです。

なぜ私はもともとこれをしたのですか?コントローラーのに依存性注入を開始すると思ったからです。これは間違っています。コントローラーレベルですぐに開始します。

これが私のアプリケーションでの動作方法である場合:

フロントコントローラー-->ブートストラップ-->ルーター-->コントローラー/メソッド-->モデル [サービス|ドメインオブジェクト|マッパー] -->コントローラー-->ビュー-->テンプレート

...その後、依存性注入コンテナーは、最初のコントローラー レベルですぐに動作を開始する必要があります。

つまり、まだ pimple を使用しているとしたら、作成するコントローラーと必要なものを定義することになります。したがって、ビューとモデル レイヤーからコントローラーに何かを挿入して、それを使用できるようにします。これは制御の反転であり、テストがはるかに簡単になります。Aurn wiki から (これについてはすぐに説明します):

実生活では、必要な部品にアクセスできるように、金物店全体を (できれば) 建設現場に運ぶことで家を建てることはありません。代わりに、職長 (__construct()) は、必要になる特定の部品 (ドアと窓) を要求し、それらの調達に取り掛かります。オブジェクトは同じように機能する必要があります。ジョブを実行するために必要な特定の依存関係のみを要求する必要があります。House にハードウェア ストア全体へのアクセスを許可することは、せいぜい貧弱な OOP スタイルであり、最悪の場合、保守性の悪夢です。- Auryn Wiki より

オーリンに入る

その点で、週末に紹介された Rdlowrey によって書かれた Auryn という素晴らしいもの紹介したいと思います。

Auryn は、クラス コンストラクターのシグネチャに基づいて、クラスの依存関係を「自動配線」します。これが意味することは、要求されたクラスごとに、Auryn がそれを見つけ、コンストラクターで必要なものを把握し、最初に必要なものを作成してから、最初に要求したクラスのインスタンスを作成するということです。仕組みは次のとおりです。

プロバイダーは、コンストラクター メソッドのシグネチャで指定されたパラメーターの型ヒントに基づいて、クラスの依存関係を再帰的にインスタンス化します。

...そして、 PHP のリフレクションについて少しでも知っていれば、それを「遅い」と呼ぶ人がいることを知っているでしょう。そこで、Auryn はそれについて次のように説明します。

「反射が遅い」と聞いたことがあるかもしれません。何かを明確にしましょう。間違ったやり方をすると、何でも「遅すぎる」可能性があります。リフレクションは、ディスク アクセスよりも 1 桁速く、(たとえば) リモート データベースから情報を取得するよりも数桁高速です。さらに、速度が心配な場合は、リフレクションごとに結果をキャッシュする機会が提供されます。Auryn は、潜在的なパフォーマンスへの影響を最小限に抑えるために、生成したリフレクションをキャッシュします。

「反射が遅い」という議論は飛ばしてしまいました。

オーリンの使い方

  • Auryn をオートローダーの一部にしています。これは、クラスが要求されたときに、Auryn が離れてクラスとその依存関係、およびその依存関係の依存関係 (など) を読み取り、それらすべてをインスタンス化のためにクラスに返すことができるようにするためです。Auyrn オブジェクトを作成します。

    $injector = new \Auryn\Provider(new \Auryn\ReflectionPool);
    
  • データベース クラスのコンストラクターの要件としてデータベースインターフェイスを使用します。そこで、どの具体的な実装を使用するかを Auryn に伝えます (これは、別の種類のデータベースをインスタンス化したい場合に変更する部分であり、コード内の 1 つのポイントで、すべて機能します)。

    $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 は次のことを行います。

  • MySQL を具象クラス (ブーストラップでエイリアス) として使用して、Library\DatabaseInterface を作成します。
  • 以前に作成したデータベースを挿入して「UserModel」を作成します
  • 以前に作成した UserModel を注入して UserController を作成します。

それが再帰であり、これが先ほど話した「自動配線」です。クラス階層にコンストラクター要件としてデータベースオブジェクトが含まれている場合にのみ、オブジェクトがインスタンス化されるため、これにより OP の問題が解決されます

また、各クラスには、コンストラクターで機能するために必要な要件が正確に含まれているため、サービス ロケーター パターンの場合のような隠れた依存関係はありません。

RE: 必要に応じて connect メソッドが呼び出されるようにする方法。これは本当に簡単です。

  1. Database クラスのコンストラクターで、オブジェクトをインスタンス化せず、その設定 (ホスト、データベース名、ユーザー、パスワード) を渡すだけであることを確認してください。
  2. new PDO()クラスの設定を使用して、実際にオブジェクトを実行する connect メソッドを用意します。

    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 メソッドを実行するだけです。 1。いいえ、シングルトンは必要ありません。必要なときにいつ接続するかを指示するだけで、接続するように指示しないと指示しません。


古い回答

依存性注入コンテナーについてもう少し詳しく説明し、それらがどのように状況に役立つかを説明します。注: 「MVC」の原則を理解すると、ここで非常に役立ちます。

問題

いくつかのオブジェクトを作成したいが、データベースにアクセスする必要があるのは特定のオブジェクトだけです。現在行っているのは、各 requestでデータベース オブジェクトを作成することです。これはまったく不要であり、DiC コンテナーなどを使用する前にはまったく一般的です。

2 つのサンプル オブジェクト

作成する可能性のある 2 つのオブジェクトの例を次に示します。1 つはデータベース アクセスを必要とし、もう 1 つはデータベース アクセスを必要としません。

/**
 * @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() { }
}

では、これらのオブジェクトを作成し、関連する依存関係を処理し、関連するクラスにのみデータベース オブジェクトを渡す最善の方法は何でしょうか? 幸運なことに、依存性注入コンテナーを使用すると、これら 2 つが調和して機能します。

にきびに入る

Pimpleは、 PHP 5.3+ のクロージャーを利用する (Symfony2 フレームワークのメーカーによる) 非常にクールな依存性注入コンテナーです。

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

解決したこと

つまり、1 つの石で (2 羽だけでなく) 複数の鳥を殺したということです。

  • リクエストごとに DB オブジェクトを作成する- もう必要ありません! Pimple が使用するクロージャのため、要求したときにのみ作成されます
  • コントローラーから「新しい」キーワードを削除する- そうです。この責任をコンテナに引き渡しました。

注:先に進む前に、箇条書き 2 がいかに重要かを指摘したいと思います。このコンテナーがない場合、アプリケーション全体で 50 個のユーザー オブジェクトを作成したとします。そしてある日、新しいパラメータを追加したいとします。OMG - アプリケーション全体を調べて、このパラメータを every に追加する必要がありますnew User()。ただし、DiC を使用すると$container['user']、どこでも使用している場合は、この 3 番目のパラメーターをコンテナーに一度追加するだけで済みます。はい、それはまったく素晴らしいです。

  • データベースを切り替える機能- お聞きしましたが、これの要点は、MySQL から PostgreSQL に変更したい場合、コンテナ内のコードを変更して、コーディングした新しい異なるタイプのデータベースを返すということです。すべてが同じ種類のものを返す限り、それだけです! 誰もが常に口論する具体的な実装を交換する機能

重要な部分

これはコンテナーの使用方法の1 つであり、ほんの始まりにすぎません。これを改善するには多くの方法があります。たとえば、コンテナをすべてのメソッドに渡す代わりに、リフレクションや何らかのマッピングを使用して、コンテナのどの部分が必要かを判断できます。これを自動化すれば、あなたはゴールデンです。

これがお役に立てば幸いです。ここで行った方法により、少なくともかなりの量の開発時間が短縮されました。起動するのはとても楽しいです!

于 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 に答える
1

依存性注入コンテナーの使用を検討してください。Pimpleのようなものから始めるのがよいでしょう。依存性注入コンテナを使用すると、アプリケーションでオブジェクトを作成する方法をコンテナに「教える」ことができます。オブジェクトは、要求するまでインスタンス化されません。Pimple を使用すると、リソースを共有するように構成できるため、コンテナーに要求する頻度に関係なく、要求中にインスタンス化されるのは 1 回だけです。

コンストラクターでコンテナーを受け入れるようにクラスをセットアップするか、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 (データベースに依存) の 2 つのメソッド、getUsers() と getBlockedUsers() を呼び出したいとします。これらのメソッドを呼び出すと、それぞれで connect() が呼び出されます。このチェックを行うと、既存のインスタンスが返されます。

于 2013-10-17T17:31:00.077 に答える
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'];
    }
}

両方のアプローチのメモリ消費量をテストしたところ、まったく予想外の結果が得られました。2 番目のアプローチは、データベースに接続してクエリを実行するときに使用するリソースが少なくなります。

于 2013-05-20T13:04:33.713 に答える