Monatsarchiv für Februar 2010

 
 

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]

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

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

        $this-&gt;setAttribute(PDO::ATTR_STATEMENT_CLASS, array('<strong>ePDOStatement</strong>', array($this)));
        $this-&gt;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 <strong>exec</strong>($sql) {
    $this-&gt;increaseQueryCount();
    return parent::exec($sql);
}
public function <strong>query</strong>($sql) {
    $this-&gt;increaseQueryCount();
    $args = func_get_args();
    return call_user_func_array(array($this, 'parent::query'), $args);
}
public function getQueryCount() {
    return $this-&gt;queryCount;
}
public function increaseQueryCount() {
    $this-&gt;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-&gt;pdo = $pdo
   }

    public function execute($params = array()) {
        $this-&gt;pdo-&gt;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-&gt;query("SELECT 1");
$pdo-&gt;exec("SELECT 1");

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

echo $pdo-&gt;getQueryCount(); // 3

Policy based routing auf Debian Lenny/Etch mit Squid Proxyserver

Folgendes Szenario: Ein Debian Lenny Server und zwei Router mit je einer Internetverbindung befinden sich im selben (lokalen) Netzwerk. Wir wollen beide Router (sprich Internetverbindungen) für verschiedene Anwendungen nutzen.

Routing-Tabelle

Routing im privaten Netzwerk ist eigentlich ganz einfach: Liegt die Ziel-IP nicht im selben Subnetz, wird das Paket an den Default-Gateway geschickt. Das nennt sich dann static routing. Verwaltet wird das über sog. Routing-Tabellen. Unter Debian sieht das ungefähr so aus:

~$ sudo route
Kernel-IP-Routentabelle
Ziel            Router          Genmask         Flags Metric Ref    Use Iface
192.168.123.0   *                255.255.255.0   U     0      0        0 eth0
default         192.168.123.99   0.0.0.0         UG    0      0        0 eth0

192.168.123.0 ist das eigene, private Netz, 192.168.123.99 der Router an dem eine DSL oder Kabel-Verbindung hängt. Das sieht bei jedem Client-PC übrigens ähnlich aus. Windows-Benutzer müssen aber “route print” ins cmd tippen.

Policy Based Routing

So weit, so einfach. Was ist aber, wenn wir bestimmte Dienste über einen anderen Gateway leiten wollen? Dann brauchen wir policy based routing. Damit lassen sich unheimlich tolle Sachen machen – wir beschränken uns aber erstmal auf das unterschiedliche Routen anhand der Source-IP-Adresse.

Konkret: Der Lenny-Server bekommt eine zweite (virtuelle) IP, den Squid-Proxyserver binden wir exklusiv auf diese IP, und per PBR schicken wir alle Pakete die vom Squid-Proxy kommen über einen bestimmten Gateway. Der Rest geht nach wie vor zum Default-Gateway.

Mit PBR könnte man natürlich auch nach dem Desination-Port routen, und sich somit die zweite IP-Adresse sparen. Leider bietet  ”ip rule” keine Möglichkeit direkt den TCP destination port als Kriterium auszuwählen. Deshalb müssten die entsprechenden Pakete erst mit iptables markiert, und dann nach dieser Markierung gefiltert werden. Was diesen Post betrifft, blieben wir also bei zwei IPs und somit dem routing anhand der Source-IP.

Ein paar Zahlen zum Verständnis:

  • Lenny-Server: 192.168.123.1 & 192.168.123.2
  • Router1: 192.168.123.99
  • Router2: 192.168.123.98

Die Software

Als Software brauchen wir iproute2 und iptables. Beide Tools sollten selbst bei einer minimalen Debian Installation schon vorhanden sein. Wenn nicht, installieren wir die Tools nach: apt-get install iproute iptables.

Die Konfiguration

Routing-Tabelle anlegen

Zunächst legen wir zwei neue Routing-Tabellen an. Eine für jede ausgehende Leitung. Dazu fügen wir folgenden Zeilen an das Ende von /etc/iproute2/rt_tables hinzu.

#
# reserved values
#
255     local
254     main
253     default
0       unspec
#
# local
#
#1      inr.ruhep
100 arcor
200 kabelbw

Der Name ist jeweils frei wählbar. Ich habe hier die Namen der jeweiligen Provider genommen. Die ID ist auch frei wählbar, muss aber zwischen 1 und 254 liegen.

Routing-Konfiguration

Wir bearbeiten /etc/network/interfaces, fügen ein zweite, virtuelle IP-Adresse hinzu, und teilen Debian mit, mit welchem Adapter welche Routing-Tabellen genutzt werden sollen.

# The loopback network interface
auto lo
iface lo inet loopback
# The primary network interface
#allow-hotplug eth0
auto eth0
iface eth0 inet static
        address 192.168.123.1
        netmask 255.255.255.0
        network 192.168.123.0
        broadcast 192.168.123.255
        post-up ip route add default via 192.168.123.99 dev eth0 table main
        pre-down ip route del default via 192.168.123.99 dev eth0 table main
auto eth0:1
iface eth0:1 inet static
        address 192.168.123.2
        netmask 255.255.255.0
        network 192.168.123.0
        broadcast 192.168.123.255
        post-up ip route add default via 192.168.123.98 dev eth0:1 table arcor
        post-up ip rule add from 192.168.123.2 table arcor
        pre-down ip route del default via 192.168.123.98 dev eth0:1 table arcor
        pre-down ip rule del from 192.168.123.2 table arcor

Die post-up & pre-down Einträge legen automatisch die entsprechenden Regeln in der Routing-Tabelle an, wenn die Netzwerkkarte aktiviert bzw. deaktiviert wird. Von Hand angelegte Einträge verschwinden sonst nämlich nach einem Neustart. Der Traffic über eth0 (192.168.123.1) geht demnach an den Router mit der .99. Traffic über das virtuelle Interface eth0:1 (192.168.123.2) geht an den Router mit der .98.

Squid-Konfiguration

Da nun sämtlicher Traffic der IP 192.168.123.2 über einen speziellen Gateway geht, müssen wir nur noch den Squid-Proxy explizit auf diese IP binden. Dazu öffnen wir die Datei /etc/squid/squid.conf und passen die http_port-Zeile an:

http_port 192.168.99.2:3128

Der Port ist natürlich frei wählbar – wichtig ist hier nur die IP-Adresse. Nach einem Squid-Neustart sollte jetzt sämtlicher Proxy-Traffic über die 192.168.123.98 laufen – der restliche Traffic nach wie vor über die 192.168.123.99.

Sinn?

Gute Frage! PHP-Skripte können zum Beispiel über PHP-Proxy die zwei unterschiedlichen Leitungen benutzen. Selbiges gilt natürlich auch für Clients, die in Ihrem Browser die virtuelle IP des Squid als Proxy eintragen. Somit kann man mit wenigen Klicks mal eben die IP wechseln…