Archiv der Kategorie ‘»Featured‘

 
 

MySQL’s GROUP_CONCAT()

Die GROUP_CONCAT-Funktion von MySQL ist eine tolle Erweiterung zu GROUP BY.  Insbesondere wenn man es mit one-to-many oder many-to-many Beziehungen zu tun hat. GROUP_CONCAT() ist seit der Version 4.1 Bestandteil von MySQL.

Beispiel: Personen haben Hobbys. Die Personen werden in der Personen-Tabelle mit dem Namen (name) und einer eindeutigen ID (person_id) gespeichert. Die zugehörigen Hobbys stehen in der Hobbys-Tabelle. Die Hobbys haben ebenfalls eine fortlaufende ID (hobby_id). Der Name des Hobbys steht in der Spalte hobby, die Verknüpfung zur Person wird über die person_id-Spalte hersgestellt.

mysql> SELECT * FROM personen;
+-----------+-------+
| person_id | name  |
+-----------+-------+
|         1 | Peter |
|         2 | Paul  |
+-----------+-------+

mysql> SELECT * FROM hobbys;
+----------+-----------+-----------+
| hobby_id | person_id | hobby     |
+----------+-----------+-----------+
|        1 |         1 | Schwimmen |
|        2 |         1 | Reiten    |
|        3 |         2 | Radfahren |
+----------+-----------+-----------+

Da eine Person mehrere Hobbys haben, und das selbe Hobby von mehreren Personen ausgeübt werden kann, wäre hier eine many-to-many Beziehung mit extra Join-Tabelle sinnvoller. Der Einfachheit halber belassen wir es aber bei zwei Tabellen. Die Funktionsweise von GROUP_CONCAT() ist in beiden Fällen sowieso die selbe.

Peter hat also die Hobbys “Schwimmen und Reiten“, Paul hat das Hobby Radfahren. Um jetzt mit PHP eine einfache Tabelle auszugeben, in der jeder Name mit den jeweiligen Hobbys aufgelistet wird, könnte man zunächst alle Namen holen, und dann pro Reihe die jeweiligen Hobbys in einem separatem Query. Das würde aber bedeuten, dass wir für n Datensätze n+1 Queries brauchen. Viel zu viel!

Packt man das alles in ein einziges  Query, und joint die Hobbys-Tabelle, taucht im Result der Peter leider doppelt auf. Auch doof…

Abhilfe schafft die GROUP_CONCAT()-Funktion in Verbindung mit einem GROUP BY. GROUP_CONCAT() konkateniert (=verbindet) alle Werte einer Gruppe zu einem String. Null-Werte werden ignoriert. Standardmäßig wird das ganze Komma-getrennt.

SELECT
    p.name
    , GROUP_CONCAT(h.hobby SEPARATOR ', ') AS hobbys
FROM
    personen p
LEFT JOIN
    hobbys h ON h.person_id = p.person_id
GROUP BY
    p.person_id

Mit der Angabe von SEPARATOR innerhalb der GROUP_CONCAT-Funktion trennen wir die einzelnen Hobbys zusätzlich mit einer Leertaste. Ergebnis:

+-------+-------------------+
| name  | hobbys            |
+-------+-------------------+
| Peter | Schwimmen, Reiten |
| Paul  | Radfahren         |
+-------+-------------------+

Perfekt! Für die besonders aufmerksamen Leser gibt’s hier nochmals das komplette Beispiel als SQL-File zum auswendig lernen ;)

SPAM im Griff mit Catch-All E-Mail-Adresse und Subdomain

Die meisten web2.0-Anwendungen verlangen eine Anmeldung, und somit auch eine E-Mail-Adresse, bevor man in den Genuss des kostenlosen Dienstes kommt. Eine Wegwerf-E-Mail-Adresse bietet sich hier nicht immer an. Schließlich will man später eventuell von Updates erfahren. So wird die verwendete E-Mail-Adresse früher oder später zugespammt. Doch welcher der vielen Anbieter, bei denen diese E-Mail-Adresse registriert ist, hat meine Adresse an Spammer verkauft? Oder anders gefragt:

Wer spammt mich hier eigentlich zu?

Wenn ein und dieselbe Adresse für mehrere Dienste benutzt wird, lässt sich das leider nicht herausfinden. Das Anlegen einzelner E-Mail-Adressen für jeden Dienst ist auch mehr als umständlich. Deshalb richten wir eine Catch-All E-Mail-Adresse ein. Damit landen alle E-Mails einer (Sub-)Domain, die an einen nicht existierenden Account gerichtet sind, in einem bestimmten Postfach.

Catch-All auf der Hauptdomain ist generell eine schlechte Idee. Hier ist das Grundrauschen von vornherein viel höher. Deshalb richten wir den Catch-All nur auf einer speziellen Subdomain ein. Zum Beispiel mail.example.org mit catchall@mail.example.org als POP3/IMAP-Account.

Bei Anmeldung an einer Webseite verwenden wir den Namen der selbigen als local-part unser Mailadresse. Beispiel: facebook@mail.example.org oder myspace@mail.example.org. Dank Catch-All laden all diese Mails in unserem catchall@-Account.

Nun lässt sich ganz leicht feststellen, wo der Spam denn herkommt.  Das hilf einem jetzt vielleicht auch nicht unbedingt weiter, aber zumindest lässt sich nun der gesamte Spam anhand der Empfänger-Adresse blockieren. Zum Beispiel mit einer einfachen Sieve-Filterregel. So sollte das Konto auch über längere Zeit sauber bleiben.

PHP’s PDO-Klasse erweitern

PDO ist eine PHP-Erweiterung, die einen konsistenten Zugriff auf Datenbanken ermöglicht. Seit PHP 5.1.0 ist PDO fester Bestandteil von PHP – also eine native (in C geschriebene) PHP-Extension. Dennoch lässt sich PDO mit PHP erweitern. Wozu? Ein Query-Counter, also das simple Mitzählen der ausgeführten SQL-Queries, ist wohl das einfachste Beispiel. Nicht weit davon entfernt sind dann so nette Dinge wie Query-Log oder Query-Zeitmessung.

PDO kommt mit zwei Klassen: PDO und PDOStatement*. Letztere kommt nur bei Prepared Statements ins Spiel. Wir beginnen also mit der PDO-Klasse. Diese muss ihre Eigenschaften an unsere eigene PDO-Klasse vererben. Das geht wie gewöhnlich mit dem Schlüsselwort “extends”. Der Name unserer eigenen PDO-Klasse ist ePDO (e wie enhanced, extended, was auch immer..).

[* und PDOException, welche für dieses Beispiel aber irrelevant ist]

<?php
class ePDO extends pdo {
    private $queryCount;
    public function __construct($dsn, $user = NULL, $pass = NULL, $options = NULL) {
        $this->queryCount = 0;

        parent::__construct($dsn, $user, $pass, $options);

        $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('ePDOStatement', array($this)));
        $this->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    }
}
?>

Wichtig ist hier immer den parent-contructor aufzurufen. Die benötigten Parameter gibt’s im Manual. Außerdem legen wir das private Attribut $queryCount an, in welchem wir die Anzahl der ausgeführten SQL-Queries speichern. Weil wir auch die PDOStatement-Klasse erweitern wollen, teilen wir PDO mittels setAttribute mit, unsere eigene PDOStatement-Klasse zu verwenden. Diese hat sinnigerweise den Namen ePDOStatement (e wie .. ihr wisst schon ..). Der Konstruktor bietet sich übrigens auch dazu an, gewisse Standard-Optionen für PDO zu setzen. In diesem Beispiel die DEFAULT_FETCH_MODE.

Der Beispielcode ist so schon voll funktionsfähig – auch wenn er noch nicht viel macht. Beim Herstellen der Datenbank-Verbindung schreiben wir statt $pdo = new PDO(..) einfach
$pdo = new ePDO(..). Die Parameter, sowie auch alles andere, bleiben die selben.

Um nun die Anzahl der Queries mitzuzählen, müssen wir alle Methoden die Queries ausführen erweitern. Das sind bei der PDO-Klasse: exec() und query():

public function exec($sql) {
    $this->increaseQueryCount();
    return parent::exec($sql);
}
public function query($sql) {
    $this->increaseQueryCount();
    $args = func_get_args();
    return call_user_func_array(array($this, 'parent::query'), $args);
}
public function getQueryCount() {
    return $this->queryCount;
}
public function increaseQueryCount() {
    $this->queryCount++;
}

getQueryCount() ist eine getter-Methode für das privat-Attribut $queryCount. Damit der Query-Count auch von unserer ePDOStatement-Klasse aus erhöht werden kann, packen wir die Logik dazu in die Public-Methode increaseQueryCount(). Da die query()-Methode variable Argumente (siehe PHP-Manual) hat, müssen wir alle übergebenen Argumente mit call_user_func_array an parent::query() übergeben.

Prepared Statements werden von der PDOStatement-Klasse ausgeführt, namentlich von der execute()-Methode. Um auch diese Queries zu zählen, müssen wir uns auch hier der Vererbung bedienen.

class ePDOStatement extends PDOStatement {
    private $pdo;

    protected function __construct(ePDO $pdo) {
        $this->pdo = $pdo
   }

    public function execute($params = array()) {
        $this->pdo->increaseQueryCount();
        return parent::execute($params);
    }
}

Der Konstruktor bekommt das ePDO-Objekt übergeben, in dem unser aktueller Query-Count steht. Wir speichern es als private Eigenschaft ab. In der execute()-Methode rufen wir die increaseQueryCount()-Methode des ePDO-Objektes auf, um unseren Query-Counter zu erhöhen.

Die fertigen Klassen gibt’s es hier nochmal zur Übersicht zum Download: ePDO und ePDOStatement.

So oder so ähnlich wird das dann benutzt:

$pdo = new ePDO("mysql:host=127.0.0.1;dbname=mydb", 'myuser', 'mypass');

$pdo->query("SELECT 1");
$pdo->exec("SELECT 1");

$stmt = $pdo->prepare("SELECT 1");
$stmt->execute();

echo $pdo->getQueryCount(); // 3