web-dev-qa-db-ja.com

データベースクエリはページ自体から抽象化する必要がありますか?

PHPでページ生成を作成するとき、データベースクエリが散らかっている一連のファイルを作成していることがよくあります。たとえば、次のように、投稿に関するデータをデータベースから直接フェッチしてページに表示するクエリがあるとします。

$statement = $db->prepare('SELECT * FROM posts WHERE id=:id');
$statement->bindValue(':id', $id, PDO::PARAM_INT);
$statement->execute();
$post = $statement->fetch(PDO::FETCH_ASSOC);
$content = $post['content']
// do something with the content

これらの迅速な1回限りのクエリは通常は小さいですが、データベースの対話コードの大部分が、かなり乱雑に見え始める場合があります。

場合によっては、ポスト関連のdbクエリを処理する単純な関数のライブラリを作成し、コードのブロックを単純なものに短縮することで、この問題を解決しました。

$content = post_get_content($id);

そして、それは素晴らしいことです。または、少なくとも何か他のことをする必要があるまでです。たぶん、最新の5つの投稿をリストに表示する必要があります。ええと、私はいつでも別の機能を追加できました:

$recent_posts = post_get_recent(5);
foreach ($recent_posts as $post) { ... }

しかし、それはSELECT *クエリ。これは通常は本当に必要ありませんが、複雑すぎて合理的に抽象化できないことがよくあります。最終的に私は、すべての単一のユースケース用のデータベース相互作用関数の大規模なライブラリ、またはすべてのページのコード内の一連の乱雑なクエリのいずれかで終わります。そして、これらのライブラリを構築した後でも、以前に使用したことがない小さな結合を1つ実行する必要があることに気づき、その仕事を行うために、突然、別の高度に専門化された関数を作成する必要があります。

もちろん、一般的なユースケースや特定のやり取りのクエリに関数を使用することはできますが、生のクエリを書き始めるとすぐに、すべてに直接アクセスできるようになります。それとも、怠惰になって、PHPループで実際にMySQLクエリで直接実行する必要があるループで物事を開始します。

インターネットアプリケーションの作成に詳しい方にお願いしたいのですが、保守性の向上は、コードの追加の行と、抽象化がもたらす非効率の可能性に見合うだけの価値があるのでしょうか。または、直接クエリ文字列を使用するだけで、データベースの相互作用を処理するための許容できる方法ですか?

10
Alexis King

特殊なクエリ関数が多すぎる場合は、それらを構成可能なビットに分解してみることができます。例えば

_$posts = posts()->joinWithComments()->orderBy("post.post_date")->first(5);
_

また、覚えておくと便利な抽象化レベルの階層もあります。あなたが持っている

  1. mysql API
  2. your select( "select * from posts where foo = bar");などのmysql関数。またはselect("posts")->where("foo = bar")->first(5)としてより構成可能かもしれません
  3. アプリケーションドメインに固有の関数、たとえばposts()->joinWithComments()
  4. commentsToBeReviewed($currentUser)など、特定のページに固有の関数

この抽象化の順序を尊重することは、保守のしやすさの点で大きな負担になります。ページスクリプトではレベル4の関数のみを使用し、レベル4の関数はレベル3の関数で記述します。これには少し時間がかかることは事実ですが、保守コストを長期にわたって一定に保つのに役立ちます(「あらまあ、彼らは別の変更を望んでいるのではありません!!!」)

7
xpmatteo

関心事の分離は、読む価値のある原則です。ウィキペディアの記事を参照してください。

http://en.wikipedia.org/wiki/Separation_of_concerns

読む価値のあるもう1つの原則はカップリングです。

http://en.wikipedia.org/wiki/Coupling_(computer_science

2つの異なる懸念事項があります。1つはデータベースからのデータのマーシャリングで、もう1つはそのデータのレンダリングです。本当にシンプルなアプリケーションでは、おそらく心配する必要はほとんどありません。データベースアクセスおよび管理レイヤーとレンダリングレイヤーを密に結合していますが、小さなアプリの場合、これは大した問題ではありません。問題は、Webアプリケーションが進化する傾向にあり、パフォーマンスや機能など、何らかの方法でWebアプリをスケールアップしたい場合に、いくつかの問題が発生することです。

ユーザーが作成したコメントのWebページを生成するとします。先のとがった髪のボスが来て、ネイティブアプリ(iPhone/Androidなど)のサポートを開始するように求めます。JSON出力が必要です。ここで、HTMLを生成していたレンダリングコードを抽出する必要があります。これを完了すると、2つのレンダリングエンジンを備えたデータアクセスライブラリが完成し、すべて正常に機能します。ビジネスロジックをレンダリングから分離することもできます。

上司がやって来て、ウェブサイトに投稿を表示したい顧客がいて、XMLが必要であり、ピーク時のパフォーマンスが毎秒約5000必要であると伝えました。次に、XML/JSON/HTMLを生成する必要があります。以前と同じように、レンダリングを再度分離できます。ただし、必要なパフォーマンスを快適に得るには、100台のサーバーを追加する必要があります。現在、データベースは100台のサーバーからアクセスされており、サーバーごとに数十の接続があり、それぞれが異なる要件や異なるクエリなどの3つの異なるアプリに直接公開されています。各フロントエンドマシンでデータベースにアクセスすることは、セキュリティ上のリスクであり、成長しています1つですが、そこには行きません。次に、パフォーマンスをスケーリングする必要があります。各アプリには異なるキャッシュ要件、つまり異なる懸念事項があります。これを1つの密結合レイヤー、つまりデータベースアクセス/ビジネスロジック/レンダリングレイヤーで試して管理できます。各レイヤーの懸念が相互に干渉し始めています。つまり、データベースからのデータのキャッシュ要件は、レンダリングレイヤーとは非常に異なる可能性があります。ビジネスレイヤーにあるロジックは、 SQLは後方に移動します。そうしないと、レンダリングレイヤーに流れ込んでしまう可能性があります。これは、すべてを1つのレイヤーに含めることで私が見た最大の問題の1つです。それは、鉄筋コンクリートをアプリケーションに注ぐようなもので、良い方法ではありません。

これらのタイプの問題、つまりWebサービスのHTTPキャッシング(squid/ytsなど)に取り組むための標準的な方法があります。 memcached/redisなどのWebサービス自体内のアプリケーションレベルのキャッシュ。また、データベースのスケールアウトを開始すると、複数の読み取りホストと1つのマスター、またはホスト間で分割されたデータに問題が発生します。ユーザー「usera」がすべての書き込みリクエストで「[table/database] foo」にハッシュする場合、書き込みまたは読み取りリクエストに基づいて、またはシャーディングされたデータベースで異なる、データベースへのさまざまな接続を管理する100台のホストが必要ではありません。

懸念の分離はあなたの友人であり、いつ、どこでそれを行うかを選択することは、アーキテクチャ上の決定であり、ちょっとした芸術です。非常に異なる要件を持つように進化するものの密結合は避けてください。物事を分離しておく理由は他にもたくさんあります。つまり、テスト、変更の展開、セキュリティ、再利用、柔軟性などが簡素化されます。

5
Harry

「ページ自体」とは、HTMLを動的に生成するPHPソースファイル)を意味すると思います。

データベースをクエリして、同じソースファイルにHTMLを生成しないでください。

PHPソースファイルであっても、データベースをクエリするソースファイルは「ページ」ではありません。

HTMLを動的に作成するPHPソースファイルでは、データベースにアクセスするPHPソースファイルで定義されている関数を呼び出すだけです。

1

ほとんどの中規模プロジェクトで使用するパターンは次のとおりです。

  • すべてのSQLクエリは、サーバー側のコードとは別の場所に配置されます。

    C#での作業は、部分クラスを使用することを意味します。つまり、単一のクラスからクエリにアクセスできる場合、クエリを別のファイルに配置します(以下のコードを参照)。

  • それらのSQLクエリは定数です。これは、SQLクエリをオンザフライで作成する誘惑を防ぐので重要です(そのため、SQLインジェクションのリスクが高まると同時に、後でクエリを確認することがより困難になります)。

C#での現在のアプローチ

例:

// Demo.cs
public partial class Demo : DataRepository
{
    public IEnumerable<Stuff> LoadStuff(int categoryId)
    {
        return this
            .Query(Queries.LoadStuff)
            .With(new { CategoryId = categoryId })
            .ReadRows<Stuff>();
    }

    // Other methods go here.
}

public partial class Demo
{
    private static class Queries
    {
        public const string LoadStuff = @"
select top 100 [StuffId], [SomeText]
    from [Schema].[Table]
    where [CategoryId] = @CategoryId
    order by [CreationUtcTime]";

        // Other queries go here.
    }
}

このアプローチには、クエリが別のファイルにあるという利点があります。これにより、開発者によるコミットと競合することなく、DBAはクエリを確認および変更/最適化し、変更されたファイルをソース管理にコミットできます。

関連する利点は、DBAのアクセスをクエリを含むファイルのみに制限し、残りのコードへのアクセスを拒否するようにソース管理を構成できることです。

PHPで行うことは可能ですか?

PHPには部分クラスと内部クラスの両方がないため、そのままではPHPに実装できません。

クラスにどこからでもアクセスできる場合、定数を含む個別の静的クラス(DemoQueries)を使用して個別のファイルを作成できます。さらに、グローバルスコープの汚染を回避するために、すべてのクエリクラスを専用の名前空間に配置できます。これはかなり冗長な構文を作成しますが、冗長性を回避できないと思います。

namespace Data {
    public class Demo inherit DataRepository {
        public function LoadStuff($categoryId) {
            $query = \Queries\Demo::$LoadStuff;
            // Do the stuff with the query.
        }

        // Other methods go here.
    }
}

namespace Queries {
    public static class Demo {
        public const $LoadStuff = '...';

        // Other queries go here.
    }
}
0