• MySQL-Datensätze nach Jahr & Monat katalogisiert ausgeben

    Fassung: 1
    Verfasst am: 17.11.2008
    Autor: Stephan Altmann

    Beispielsweise viele Blogscripte bieten die Möglichkeit, nur Einträge eines bestimmten Monats nach Klick auf dem entsprechenden Link auszugeben. Eine solche Funktion lässt sich relativ einfach ohne größeren Aufwand realisieren.

    Wichtig für die Umsetzung ist zuallererst, dass man sich dafür entscheidet, wie man die Datensätze mit einer genauen Zeitangabe versieht. Ich persönlich ziehe den UNIX-Timestamp dem MySQL-Timestamp vor, da ich zumeist mittels PHP auf die Daten zugreife und sich Datumsangaben in diesem Format mit Hilfe von PHP relativ einfach handhaben lassen. Ebenso bestünde die Möglichkeit, die einzelnen Bestandteile des Datums auf mehrere Spalten aufzuteilen, was bei einer derartigen Funktion, wie wir sie hier realisieren wollen, durchaus einen Vorteil mit sich brächte. Allerdings bin ich persönlich der Ansicht, dass, wenn ich ohne bedeutenden Mehraufwand mit weniger Spalten auskomme, ich auch mit weniger Spalten arbeiten sollte, da sich so UPDATE- und INSERT-Anfragen kürzer und somit übersichtlicher halten lassen

    Erläuterung zum Unterschied von UNIX- & MySQL-Timestamp:
    Ein UNIX-Timestamp entspricht der Anzahl der Sekunden seit dem 1. Januar 1970 0.00 Uhr. Ein MySQL-Timestamp hingegen wird in der Form „YYYY-MM-DD HH:MM:SS“ dargestellt.

    In PHP erzeuge ich einen den UNIX-Timestamp der aktuellen Uhrzeit ganz einfach durch das Aufrufen der Funktion time() ohne Angabe irgendwelcher Parameter.

    PHP-Code:
    //Ausgabe der aktuellen Uhrzeit als UNIX-Timestamp
    echo time(); 
    Einen MySQL-Timestamp der aktuellen Zeit erzeuge ich unter MySQL ganz einfach unter der Verwendung der Funktion now().

    //Ausgabe der aktuellen Uhrzeit als MySQL-Timestamp
    SELECT NOW();
    Ein UNIX-Timestamp lässt sich innerhalb einer MySQL-Anweisung problemlos in einen MySQL-Timestamp mittels der Funktion FROM_UNIXTIME(timestamp) umwandeln. Durch Angabe des 2. Parameters können wir festlegen, wie das Ergebnis formatiert werden soll, um beispielsweise auf einzelne Bestandteile des Datums zugreifen zu können. Wir werden später darauf zurückgreifen.

    //Umwandlung eines UNIX-Timestamps
    SELECT FROM_UNIXTIME(`spalte`) FROM `tabelle`
    //Umwandlung und formatierte Ausgabe des Jahres
    SELECT FROM_UNIXTIME(`spalte`, '%Y-%m') FROM `tabelle`
    //Ergebnis (November 2008): 2008-11
    Einen MySQL-Timestamp können wir mittels PHP unter der Verwendung der Funktion substr() problemlos in seine Bestandteile zerlegen. Die Umwandlung in einen UNIX-Timestamp kann dann durch Verwendung der Funktion mktime() realisiert werden. Eine abermalige Rückumwandlung wäre dann unter Zuhilfenahme der Funktion date() möglich.

    Die Funktionsweise werde ich an dieser Stelle nur anhand eines gekürzten Beispiels darstellen. Mit den hier vorgestellten Funktionen werden wir im Verlaufe des Tutorials arbeiten, weshalb ich hier grob darauf eingehen werde. Für ausgiebige Informationen bezüglich jeweiligen Funktion empfehle ich es, einen Blick ins PHP-Handbuch (http://www.php.net/manual/de/) zu werfen.

    PHP-Code:
    //Auszug MySQL-Timestamp (2008-11)
    //Auslesen von Jahr und Monat
    $jahr substr(“2008-11”04);
    $monat substr(“2008-11”52);

    //Umwandlung in UNIX-Timestamp
    $timestamp mktime($stunde$minute$sekunde$monat$tag$jahr);

    //Rückumwandlung in Ausgangsstring (2008-11)
    echo $date(“Y-m”$timestamp); 
    Anmerkung zum Unix-Timestamp:
    Wenn wir mit vorzeichenbehafteten 32-Bit-Integerwerten (Wertebereich −2.147.483.648 bis +2.147.483.647) arbeiten, ergibt sich daraus das Problem, dass sich ein Timestamp von vor dem 13. Dezember 1902 und nach dem 19. Januar 1938 nicht darstellen lässt. In einem solchen Fall erhalten wir einen Integerüberlauf. Ebenso werden derzeit keine negativen Werte für Timestamps unter Windows unterstützt. Für die Speicherung von beispielsweise Geburtsdaten empfiehlt sich der Timestamp daher nicht.

    Schritt 1: Anlegen der Datenbank
    Zuallererst legen wir folgende Tabelle an.

    CREATE TABLE IF NOT EXISTS `blog_entries` (
    `id` int(11) NOT NULL auto_increment,
    `title` varchar(255) NOT NULL,
    `content` text NOT NULL,
    `date` int(11) NOT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=0 ;
    Kurz zur Erläuterung, die Spalte „title“ enthält den Titel, in „content“ hinterlegen wir den eigentlichen Text des jeweiligen Beitrags und „date“ enthält den zugehörigen UNIX-Timestamp.

    Ich kann und will in diesem Tutorial nicht auf sämtliche Grundlagen eingehen. Ich gehe davon aus, dass allgemein bekannt ist, wie man eine MySQL-Verbindung aus einem PHP-Script heraus aufbaut und wie man Daten in einer Tabelle speichert. Für letzteres will ich kurz ein Beispiel geben:

    PHP-Code:
    $date time();
    mysql_query("INSERT INTO `blog_entries` (`title`, `content`, `date`) VALUES ('" addslashes($_REQUEST["title"]) . "', '" addslashes($_REQUEST["content"]) . "', '$date')"); 
    Anmerkung:
    Beim Arbeiten mit Benutzereingaben empfiehlt es sich grundsätzlich, diese mittels addslashes() zu maskieren, um MySQL-Injections zu verhindern.

    Schritt 2: Kategorienauswahl generieren
    Zuerst definieren wir folgendes Array:

    PHP-Code:
      $monate = array("01" => "Januar",
                      
    "02" => "Feber",
                      
    "03" => "März",
                      
    "04" => "April",
                      
    "05" => "Mai",
                      
    "06" => "Juni",
                      
    "07" => "Juli",
                      
    "08" => "August",
                      
    "09" => "September",
                      
    "10" => "Oktober",
                      
    "11" => "November",
                      
    "12" => "Dezember"); 
    Dieses benötigen wir, um später die Monatsnamen in deutscher Sprache ausgeben zu können. Die Monatsauswahl erzeugen wir wie folgt:

    PHP-Code:
      $back_year 0;
      
      
    $query mysql_query("SELECT FROM_UNIXTIME(`date`, '%Y%m') AS ym, COUNT(`id`) AS count FROM `blog_entries` GROUP BY ym DESC");
      while(
    $_MONTHS mysql_fetch_assoc($query))
      {
        
    $_YEAR substr($_MONTHS["ym"], 04);
        
    $_MONTH substr($_MONTHS["ym"], 42);
        
        
        if(
    $_YEAR != $back_year) echo "<br /><b>" $_YEAR "</b><br />";
        echo 
    "<a href=\"index.php?date=$_MONTHS[ym]\">" $monate[$_MONTH] . "</a> (" $_MONTHS["count"] . ")<br />";
        
        
    $back_year $_YEAR;
      } 
    Erläuterung:
    Wir initialisieren zuerst eine Variable namens „$back_year“ indem wir ihr den Wert 0 zuweisen. Eigentlich ist dies in PHP nicht unbedingt erforderlich, da Variablen anders als beispielsweise in C nicht ohne explizite Wertzuweisung auf jeden möglichen Speicherbereich verweisen können. Dennoch rate ich gerade bei wirklich komplexen Skripten dazu, dies zu tun, da es durchaus vorkommen, dass eine Variable gleichen Namens bereits verwendet wurde.

    PHP-Code:
      $query mysql_query("SELECT FROM_UNIXTIME(`date`, '%Y%m') AS ym, COUNT(`id`) AS count FROM `blog_entries` GROUP BY ym DESC"); 
    Wirklich interessant ist die eigentliche Datenbankabfrage. Wie bereits zuvor erläutert, extrahieren wir das Jahr und den Monat aus dem UNIX-Timestamp unter Verwendung der Funktion „FROM_UNIXTIME(`date`, '%Y%m')“. Der 2. Parameter dieser Funktion ist die eigentliche Formatanweisung bewirkt, dass wir Jahr und Monat in der Form YYYYMM als String aus der Datenbank herauslesen. Mittels „AS“ definieren wir einen Alias, so dass wir später aus PHP heraus einfacher auf den ausgelesenen Wert zugreifen können. Um zu vermeiden, dass für jeden Beitrag eines Monats der Monatsname abermals in der zu erzeugenden Liste auftritt gruppieren wir die entsprechenden Einträge durch Verwendung von „GROUP BY“. Dies ist zudem erforderlich, damit wir die einem Monat zugehörigen Beiträge mittels der Funktion „COUNT()“ auszählen können.

    PHP-Code:
      while($_MONTHS mysql_fetch_assoc($query))
      {
        
      } 
    Zeile für Zeile, also quasi Monat für Monat durchlaufen wir nun die Datensätze und weisen die Werte dem assoziativen Array „$_MONTHS“ zu. Ich persönlich ziehe grundsätzlich die Funktion mysql_fetch_assoc() den Funktionen mysql_fetch_row() und mysql_fetch_array() vor, da sich ein assoziatives Array zumeist einfacher handhaben lässt und mysql_fetch_array() sowohl ein Array mit numerischen Indizies als auch ein assoziatives Array erzeugt, was nur unnötig Rechenleistung erfordert und Speicher belegt.

    PHP-Code:
        $_YEAR substr($_MONTHS["ym"], 04);
        
    $_MONTH substr($_MONTHS["ym"], 42); 
    Um mit den ausgelesenen Werten arbeiten zu können, zerlegen wir unter Verwendung der Funktion substr() unsere Zeichenkette und weisen Jahr und Monat den Variaben “$_YEAR” und “$_MONTH” zu.

    PHP-Code:
    if($_YEAR != $back_year) echo "<br /><b>" $_YEAR "</b><br />";
    echo 
    "<a href=\"index.php?date=$_MONTHS[ym]\">" $monate[$_MONTH] . "</a> (" $_MONTHS["count"] . ")<br />";
        
      
    $back_year $_YEAR
    Jetzt erzeugen wir die Ausgabe. Damit nicht jedem Monat das Jahr in der Ausgabe vorangestellt wird, bedienen wir uns der Variable „$back_year“. Erst wenn sich das Jahr eines Datensatzes vom Jahr des vorhergehenden Datensatzes unterscheidet, soll dieses ausgegeben werden. Deshalb weisen wir nach jedem Durchlauf dieser Variable den Wert des jeweiligen Jahres zu.

    Den eigentlichen Monat geben wir als Link aus. Wie zuvor angekündigt bedienen wir uns an dieser Stelle des Arrays mit den deutschen Datumsnamen. Über den Link übergeben wir der nach einem Klick aufgerufenen Seite unsere Datumszeichenkette.

    Schritt 3: Ausgabe der Datensätze

    Abschließend geben wir die Datensätze wie folgt aus:

    PHP-Code:
      if($_REQUEST["date"])
      {
        
    $year substr($_REQUEST["date"], 04);
        
    $month substr($_REQUEST["date"], 42);

        
    //$min = erster Tag des Monat 00:00:00
        
    $min mktime(000$month1$year);

        
    //$max = letzter Tag des Monats; 23:59:59
        
    $max mktime(000$month 11$year);

        
    $query $database->db_query("SELECT * FROM `blog_entries` WHERE `date` >= '" addslashes($min) . "' and `date` < '" addslashes($max) . "' ORDER BY `date`DESC");
      }
      else 
    $query $database->db_query("SELECT * FROM `blog_entries` ORDER BY `date`DESC");
      while(
    $_ARTICLE mysql_fetch_assoc($query))
      {
        echo 
    "<br /><b>" $_ ARTICLE[“title”] . "</b><br />" $_ ARTICLE[“content”];
      } 
    PHP-Code:
      if($_REQUEST["date"])
      {
        
    //…
      
    }
      else 
    $query $database->db_query("SELECT * FROM `blog_entries` ORDER BY `date`DESC");
      while(
    $_ARTICLE mysql_fetch_assoc($query))
      {
        echo 
    "<br /><b>" $_ ARTICLE[“title”] . "</b><br />" $_ ARTICLE[“content”];
      } 
    Wurde kein Monat ausgewählt, geben wir ganz einfach sämtliche Beiträge aus. Dies bedarf wohl keiner weiteren Erläuterung.

    PHP-Code:
      if($_REQUEST["date"])
      {
        
    $year substr($_REQUEST["date"], 04);
        
    $month substr($_REQUEST["date"], 42);

        
    //$min = erster Tag des Monats 00:00:00
        
    $min mktime(000$month1$year);

        
    //$max = erster Tag des nächsten Monats 00:00:00
        
    $max mktime(000$month 11$year);

        
    $query $database->db_query("SELECT * FROM `blog_entries` WHERE `date` >= '" addslashes($min) . "' and `date` < '" addslashes($max) . "' ORDER BY `date`DESC");
      } 
    Wir zerlegen wieder wie bereits zuvor unsere Datumszeichenkette. Anschließend erzeugen wir 2 Timestamps, nämlich den Timestamp der ersten Sekunde des jeweiligen Monats und den Timestamp der ersten Sekunde des darauffolgenden Monats.

    In der WHERE-Klausel der Abfrage fügen wir abschließend die obige Bedingung ein, um zu erwirken, dass lediglich Beiträge zwischen diesen 2 Zeitpunkten ausgelesen werden. Da es unter Umständen vorkommen kann, dass ein Beitrag Punkt 00:00:00 am darauffolgenden Monat erstellt werden kann, müssen wir, damit dieser nicht dem falschen Monat zugewiesen wird, vom Vergleichsoperator „<“ Gebrauch machen, während wir beim Minimalwert „>=“ verwenden.

    Korrektur
    Wenn ich schon darauf hinweise, dass ich Benutzereingaben manuell mittels addslashes() maskiere, sollte ich diese natürlich auch wieder demaskieren.

    Daher sollte der Auslesevorgang sich wie folgt darstellen:
    PHP-Code:
    echo "<br /><b>" stripslashes($_ ARTICLE[“title”]) . "</b><br />" stripslashes($_ ARTICLE[“content”]); 
    Zudem sollte ich anmerken, dass man auf diese Weise, sobald die Funktion magic_quotes_gpc in der PHP-Konfigurationsdatei aktiviert ist, man Benutzereingaben doppelt maskiert.. Daher unterbinde ich am Beginn eines jeden Scriptes diese Funktionalität. Alternativ könnte man vor jedem Aufruf der Funktion addslashes() eine entsprechende Abfrage im Code platzieren.

    Wie handhabe ich dies?
    Ganz einfach mittels
    PHP-Code:
    if(get_magic_quotes_gpc()) 
    überprüfe ich, ob diese Funktion aktiv ist und wende stripslashes() auf jedes einzelne Element der Arrays $_REQUEST, $_GET und $_POST an.

    Dies ist nicht Gegenstand des Tutorials, sollte aber, da dieses in erster Linie für Neulinge geschrieben wurde, doch angemerkt werden.

    weitere Anmerkungen
    Ich wurde darauf hingewiesen, dass ebenso von der Funktion mysql_real_escape_string() anstelle von addslashes() Gebrauch gemacht werden kann bzw. diese sogar dieser vorzuziehen ist. Grundsätzlich muss ich dem zustimmen, allerdings nicht aus sicherheitsrelevanten Gründen. (Stichworte: konsequente Maskierung, Zeichensatzaspekte, ...)

    Ich persönlich nehme Abstand von dieser Funktion, da diese eine geöffnete Datenbankverbindung erfordert und diese einem beispielsweise, wenn man SQL-Anweisungen für externe Scripte aufbereitet, nicht zur Verfügung steht.
     


     
    Kommentare 1 Kommentar
    1. Avatar von Yaslaw
      Yaslaw -
      Nunja, PHP kennt Tiemstamp-Felder. Es ist mMn schöner auch mit denen zu arbeiten, da MySQL entsprechende Funktion bereit hält.

      zB. um die Kombination Jahr+Monat zu kriegen:
      Code sql:
      1
      2
      3
      4
      
      -- Dein Vorschlag:
      SELECT FROM_UNIXTIME(`date`, '%Y%m') AS ym
      --Mit Timestamp als Spaltenformat
      SELECT EXTRACT(YEAR_MONTH FROM `date`) AS ym

      Zudem rate ich ab, Spalten gleich zu benennen wie MySQL funktionen. `date` ist ein ganz schlechter Spaltenname. `posting_date` oder sowas eignet sich besser.