Sollten Datenbankabfragen von der Seite selbst abstrahiert werden?

10

Beim Schreiben der Seitengenerierung in PHP schreibe ich häufig eine Reihe von Dateien, die mit Datenbankabfragen übersät sind. Beispielsweise könnte ich eine Abfrage haben, um einige Daten zu einem Beitrag direkt aus der Datenbank abzurufen und auf einer Seite anzuzeigen, wie folgt:

$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

Diese schnellen, einmaligen Abfragen sind normalerweise klein, aber manchmal habe ich große Teile des Datenbank-Interaktionscodes, der ziemlich chaotisch aussieht.

In einigen Fällen habe ich dieses Problem gelöst, indem ich eine einfache Funktionsbibliothek für meine postbezogenen Datenbankabfragen erstellt und diesen Codeblock auf ein einfaches gekürzt habe:

$content = post_get_content($id);

Und das ist großartig. Zumindest ist es so lange, bis ich etwas anderes tun muss. Vielleicht muss ich die fünf neuesten Beiträge in einer Liste anzeigen lassen. Nun, ich könnte immer eine andere Funktion hinzufügen:

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

Aber das endet mit einer SELECT *Abfrage, die ich normalerweise sowieso nicht brauche, die aber oft zu kompliziert ist, um einigermaßen abstrakt zu sein. Am Ende habe ich entweder eine riesige Bibliothek mit Datenbankinteraktionsfunktionen für jeden einzelnen Anwendungsfall oder eine Reihe unordentlicher Abfragen im Code jeder Seite. Und selbst wenn ich diese Bibliotheken erstellt habe, muss ich einen winzigen Join ausführen, den ich zuvor noch nicht verwendet habe, und plötzlich muss ich eine weitere hochspezialisierte Funktion schreiben, um die Aufgabe zu erledigen.

Sicher, ich könnte die Funktionen für allgemeine Anwendungsfälle und Abfragen für bestimmte Interaktionen verwenden, aber sobald ich anfange, Rohabfragen zu schreiben, schlüpfe ich für alles wieder in den direkten Zugriff. Entweder das, oder ich werde faul und fange an, Dinge in PHP-Schleifen zu tun, die eigentlich direkt in den MySQL-Abfragen erledigt werden sollten.

Ich möchte diejenigen fragen, die mehr Erfahrung mit dem Schreiben von Internetanwendungen haben: Lohnt sich die Erhöhung der Wartbarkeit für die zusätzlichen Codezeilen und möglichen Ineffizienzen, die durch die Abstraktionen entstehen können? Oder ist die Verwendung direkter Abfragezeichenfolgen eine akzeptable Methode für den Umgang mit Datenbankinteraktionen?

Alexis King
quelle
Vielleicht können Sie gespeicherte Prozeduren verwenden, um unordentliche selects - si zu "wickeln". Sie müssen solche Prozeduren nur mit einigen Parametern
aufrufen, die

Antworten:

7

Wenn Sie zu viele spezialisierte Abfragefunktionen haben, können Sie versuchen, diese in zusammensetzbare Bits zu zerlegen. Zum Beispiel

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

Es gibt auch eine Hierarchie von Abstraktionsebenen, die Sie möglicherweise als nützlich erachten. Du hast

  1. MySQL-API
  2. Ihre MySQL-Funktionen, wie z. B. select ("select * from posts where foo = bar"); oder vielleicht komponierbarer alsselect("posts")->where("foo = bar")->first(5)
  3. Funktionen, die beispielsweise für Ihre Anwendungsdomäne spezifisch sind posts()->joinWithComments()
  4. Funktionen, die für eine bestimmte Seite spezifisch sind, wie z commentsToBeReviewed($currentUser)

Es lohnt sich sehr, diese Reihenfolge der Abstraktionen zu respektieren. Die Seitenskripte sollten nur Funktionen der Ebene 4 verwenden, Funktionen der Ebene 4 sollten in Bezug auf Funktionen der Ebene 3 geschrieben werden und so weiter. Es ist wahr, dass dies im Voraus etwas länger dauert, aber es wird Ihnen helfen, Ihre Wartungskosten über die Zeit konstant zu halten (im Gegensatz zu "Oh mein Gott, sie wollen eine weitere Änderung !!!").

xpmatteo
quelle
2
+1 Diese Syntax erstellt im Wesentlichen Ihr eigenes ORM. Wenn Sie sich wirklich Sorgen darüber machen, dass der Datenbankzugriff komplex wird, und nicht viel Zeit damit verbringen möchten, an den Details zu basteln, würde ich empfehlen, ein ausgereiftes Webframework (z. B. CodeIgniter ) zu verwenden, das dieses Problem bereits gelöst hat. Oder versuchen Sie es zumindest, um zu sehen, welche Art von syntaktischem Zucker es Ihnen gibt, ähnlich wie es xpmatteo gezeigt hat.
Hartley Brody
5

Die Trennung von Bedenken ist ein lesenswertes Prinzip, siehe den Wikipedia-Artikel dazu.

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

Ein weiteres lesenswertes Prinzip ist die Kopplung:

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

Sie haben zwei unterschiedliche Bedenken: Zum einen das Marshalling der Daten aus der Datenbank und zum anderen das Rendern dieser Daten. In wirklich einfachen Anwendungen gibt es wahrscheinlich wenig Grund zur Sorge. Sie haben Ihre Datenbankzugriffs- und Verwaltungsebene eng mit Ihrer Rendering-Ebene verknüpft, aber für kleine Apps ist dies keine große Sache. Das Problem ist, dass sich Webanwendungen tendenziell weiterentwickeln. Wenn Sie jemals eine Webanwendung in irgendeiner Weise skalieren möchten, dh in Bezug auf Leistung oder Funktionalität, treten einige Probleme auf.

Nehmen wir an, Sie generieren eine Webseite mit benutzergenerierten Kommentaren. Der spitzhaarige Chef kommt und bittet Sie, native Apps wie iPhone / Android usw. zu unterstützen. Wir benötigen eine JSON-Ausgabe. Jetzt müssen Sie den Rendering-Code extrahieren, der HTML generiert hat. Wenn Sie dies getan haben, haben Sie jetzt eine Datenzugriffsbibliothek mit zwei Rendering-Engines und alles ist in Ordnung, Sie haben funktional skaliert. Möglicherweise haben Sie es sogar geschafft, alles vom Rendern der Geschäftslogik zu trennen.

Der Chef kommt und sagt Ihnen, dass er einen Kunden hat, der die Beiträge auf seiner Website anzeigen möchte, XML benötigt und etwa 5000 Anfragen pro Sekunde Spitzenleistung benötigt. Jetzt müssen Sie XML / JSON / HTML generieren. Sie können Ihr Rendering wie zuvor wieder trennen. Jetzt müssen Sie jedoch 100 Server hinzufügen, um die gewünschte Leistung zu erzielen. Jetzt wird Ihre Datenbank von 100 Servern mit möglicherweise Dutzenden von Verbindungen pro Server aufgerufen, von denen jeder direkt drei verschiedenen Apps mit unterschiedlichen Anforderungen und unterschiedlichen Abfragen usw. ausgesetzt ist. Der Datenbankzugriff auf jedem Frontend-Computer ist ein Sicherheitsrisiko und wächst Eins, aber ich werde nicht dorthin gehen. Jetzt müssen Sie die Leistung skalieren. Jede App hat unterschiedliche Caching-Anforderungen, dh unterschiedliche Bedenken. Sie können versuchen, dies in einer eng gekoppelten Schicht zu verwalten, dh in Ihrer Schicht für Datenbankzugriff / Geschäftslogik / Rendering. Die Bedenken der einzelnen Ebenen beginnen sich nun gegenseitig zu stören, dh die Caching-Anforderungen der Daten aus der Datenbank können sich stark von denen der Rendering-Ebene unterscheiden. Die Logik, die Sie in der Business-Ebene haben, wird wahrscheinlich in die Ebene übergehen SQL, dh rückwärts bewegen oder vorwärts in die Rendering-Ebene bluten. Dies ist eines der größten Probleme, die ich gesehen habe, wenn sich alles in einer Ebene befindet. Es ist, als würde man Stahlbeton in Ihre Anwendung gießen und nicht auf gute Weise.

Es gibt Standardmethoden, um diese Art von Problemen anzugehen, z. B. HTTP-Caching der Webdienste (Squid / Yts usw.). Caching auf Anwendungsebene innerhalb der Webdienste selbst mit etwas wie memcached / redis. Sie werden auch auf Probleme stoßen, wenn Sie beginnen, Ihre Datenbank zu skalieren, dh mehrere gelesene Hosts und einen Master oder Sharded-Daten zwischen Hosts. Sie möchten nicht, dass 100 Hosts verschiedene Verbindungen zu Ihrer Datenbank verwalten, die sich aufgrund von Schreib- oder Leseanforderungen oder in einer Sharded-Datenbank unterscheiden, wenn ein Benutzer "usera" für alle Schreibanforderungen in "[table / database] foo" hascht.

Die Trennung von Anliegen ist Ihr Freund. Die Entscheidung, wann und wo dies zu tun ist, ist eine architektonische Entscheidung und ein Kunstwerk. Vermeiden Sie eine enge Kopplung von Gegenständen, die sich aufgrund sehr unterschiedlicher Anforderungen entwickeln. Es gibt eine Reihe anderer Gründe, die Dinge getrennt zu halten, dh das Testen, Bereitstellen von Änderungen, Sicherheit, Wiederverwendung, Flexibilität usw. werden vereinfacht.

Harry
quelle
Ich verstehe, woher du kommst, und ich bin mit nichts, was du gesagt hast, nicht einverstanden, aber das ist momentan ein kleines Problem. Das fragliche Projekt ist ein persönliches, und der größte Teil meines Problems mit meinem aktuellen Modell beruht auf dem Instinkt meines Programmierers, eine enge Kopplung zu vermeiden, aber ich bin wirklich ein Neuling in der komplexen serverseitigen Entwicklung, daher ging das Ende ein wenig über meinen Kopf. Trotzdem +1 für einen meiner Meinung nach guten Rat, auch wenn ich ihn für dieses Projekt möglicherweise nicht vollständig befolge.
Alexis King
Wenn das, was Sie tun, klein bleibt, würde ich es so einfach wie möglich halten. Ein gutes Prinzip ist YAGNI .
Harry
1

Ich gehe davon aus, dass Sie mit "die Seite selbst" die PHP-Quelldatei meinen, die dynamisch HTML generiert.

Fragen Sie die Datenbank nicht ab und generieren Sie HTML in derselben Quelldatei.

Die Quelldatei, in der Sie die Datenbank abfragen, ist keine "Seite", obwohl es sich um eine PHP-Quelldatei handelt.

In der PHP-Quelldatei, in der Sie den HTML-Code dynamisch erstellen, rufen Sie nur die Funktionen auf, die in der PHP-Quelldatei definiert sind, in der auf die Datenbank zugegriffen wird.

Tulains Córdova
quelle
0

Das Muster, das ich für die meisten mittelgroßen Projekte verwende, ist das folgende:

  • Alle SQL-Abfragen werden außer dem serverseitigen Code an einem separaten Speicherort abgelegt.

    Wenn Sie mit C # arbeiten, müssen Sie Teilklassen verwenden, dh die Abfragen in einer separaten Datei ablegen, da auf diese Abfragen von einer einzelnen Klasse aus zugegriffen werden kann (siehe den folgenden Code).

  • Diese SQL-Abfragen sind Konstanten . Dies ist wichtig, da es die Versuchung verhindert, SQL-Abfragen im laufenden Betrieb zu erstellen (was das Risiko einer SQL-Injection erhöht und gleichzeitig die spätere Überprüfung der Abfragen erschwert).

Aktueller Ansatz in C #

Beispiel:

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

Dieser Ansatz hat den Vorteil, dass sich die Abfragen in einer separaten Datei befinden. Auf diese Weise kann ein DBA die Abfragen überprüfen und ändern / optimieren und die geänderte Datei der Quellcodeverwaltung übergeben, ohne dass dies mit den von den Entwicklern vorgenommenen Festschreibungen in Konflikt steht.

Ein damit verbundener Vorteil besteht darin, dass die Quellcodeverwaltung so konfiguriert werden kann, dass der Zugriff von DBA nur auf die Dateien beschränkt wird, die die Abfragen enthalten, und der Zugriff auf den Rest des Codes verweigert wird.

Ist das in PHP möglich?

PHP fehlen sowohl Teilklassen als auch innere Klassen, so wie es ist, kann es nicht in PHP implementiert werden.

Sie können eine separate Datei mit einer separaten statischen Klasse ( DemoQueries) erstellen, die die Konstanten enthält, da auf die Klasse von überall aus zugegriffen werden kann. Um zu vermeiden, dass der globale Bereich verschmutzt wird, können Sie außerdem alle Abfrageklassen in einem dedizierten Namespace ablegen. Dies wird eine ziemlich ausführliche Syntax erzeugen, aber ich bezweifle, dass Sie die Ausführlichkeit vermeiden können:

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.
    }
}
Arseni Mourzenko
quelle