Archive for August, 2008

Veränderte URLs Google mitteilen: Redirect mit 301

Mittwoch, August 20th, 2008

Angenommen man hat schon eine Menge Seiten im Google-Index. Nun möchte man das Schema der URLs dieser Seiten verändern um es beispielsweise ein bisschen SEO zu betreiben.

Die naheliegendste Vorgehensweise dabei wäre, eine neue Action im Controller zu schreiben und von dort aus auf die neue URL umzuleiten:


def some_action_old
redirect_to some_action_url(params[:id])
end

Oder so ähnlich. Für den Besucher sieht das prima aus, denn er sieht nur die neue URL im Browser. Googlebot bekommt beim nächsten Crawl ebenfalls die Weiterleitung mitgeteilt, man könnte daher annehmen er würde ab dann die neue URL speichern. Das wird aber nicht passieren, warum?

redirect_to liefert den Status-Code 302 (Moved temporarily) zurück. Wie der Name schon sagt, gilt diese Umleitung nur vorübergehenderweise- nur für diesen speziellen Seitenaufruf. Im normalen Gebrauch macht das auch Sinn: Angenommen man klickt auf einen Logout-Link, so wird man anschließend zur Startseite umgeleitet. Das bedeutet jedoch nicht, dass ab jetzt die URL der Startseite die gleiche Bedeutung wie die Logout-URL hat.

Für diesen Fall gibt es den Status-Code 301 (Moved permanently). Um diesen zurückzugeben, muss man direkt in den Header zugreifen. (Ich fand diesen Trick über Snippets.dzone.com).


def some_action_old
redirect_to post_url(@post), :status=>301
end

Jetzt wird der Googlebot nach und nach die alten URLs gegen die neuen austauschen. Software zum auswerten der Serverlogs (ich verwende AWStats) zeigt eine Tabelle mit den häufigsten HTTP-Statuscodes an. Dort kann man durch die Zahl der 301-Redirects abschätzen, wie oft dieser Aufruf tatsächlich genutzt wurde.

mysql_stmt_row_tell-Fehler mit Instant-Rails

Donnerstag, August 14th, 2008

Bei einer InstantRails-Installation habe ich heute auf Rails 2.1 geupgraded (es gibt noch keine InstantRails-Version mit vorinstalliertem Rails 2.1).

Beim Start einer Rails-Anwendung und dem ersten Zugriff auf http://localhost:3000 erhielt ich die Fehlermeldung ‘Der Prozedureinsprungpunkt “mysql_stmt_row_tell” wurde in der DLL “LIBMYSQL.dll” nicht gefunden’

Nach kurzem Googlen fand ich die Lösung hier:

Einfach im InstantRails-Verzeichnis die Datei libmysql.dll von <InstantRails>\mysql\bin nach <InstantRails>\ruby\bin kopieren und es geht. Ich weiss nicht ob es speziell mit Rails 2.1 zusammenhängt, aber InstantRails 2 “out-of-the-box” hatte ich dieses Problem nicht.

Ach übrigens, das Upgrade auf Rails 2.1 wird durchgeführt in dem man auf der InstantRails-Konsole “gem rails update” eingibt.

SQL-Abfragen optimieren mit :select und :include

Sonntag, August 3rd, 2008

Um bei Rails mit der Datenbank zu kommunizieren, muss man sich dank ActiveRecord-Pattern nicht mehr um SQL-Befehle kümmern. Diese Kapselung ist sehr praktisch und verhindert auch Fehler, die sich bei komplizierten Abfragen schnell einschleichen.

Dieser Vorteil wird allerdings teuer erkauft, da ActiveRecord nicht weiss, wie die Daten von der Anwendung verwendet werden sollen. So benötigt man beispielsweise nicht immer alle Felder (oder Spalten) einer Tabelle- oder aber man weiss schon im Vorfeld, dass man auch Datensätze aus verbundenen Tabellen benötigt (belongs_to).

Nur benötigte Felder laden mit :select

Wird ein Datensatz über find() geladen, so führt ActiveRecord ein “SELECT * FROM …” aus. Das ist schlecht, da es eine Menge speicher verschwenden kann.

Beispiel: Eine Blog-Anwendung soll die 100 am meisten gelesenen Blog-Beiträge in einer Liste ausgeben, wobei nur die Überschriften angezeigt werden. Die Blog-Einträge sind in einer Tabelle gespeichert, welche Spalten für Id, Datum, Anzahl der Leser, Titel und Inhalt des Blogbeitrags hat. Zum erstellen dieser Liste benötigt man nur den Titel und die Id (um den Link zu erstellen).

Die Abfrage könnte daher lauten:

@most_viewed = BlogPost.find(:all, :order => 'views DESC', :limit => 100, :select => 'id, title')

Angenommen, jeder Beitrag (Inhalts-Feld) ist im Schnitt 10 KB groß dann fallen wärend dieser Abfrage mindestens 1 MB weniger Daten aus Datenbank an (die sowieso nicht verwendet werden). Das hört sich vielleicht zunächst nicht nach einem Problem an, allerdings geht es hier nicht nur um den Arbeitsspeicher des Webservers. Läuft der Datenbank-Server nämlich auf einem anderen Rechner als der Webserver, zum Beispiel bei einem größeren Projekt mit mehreren Webservern, dann müssen für die optimierte Abfrage mindestens 1 MB weniger an Daten übers Netzwerk übertragen werden. Bedenkt man jetzt noch, dass eine solche auch von vielen Besuchern gleichzeitig ausgelöst werden kann, summieren sich schnell mehrere MB unnützer Daten deren Übertragung für die Performance nicht folgenlos bleiben kann.

Mögliche Komplikationen: Bei einem Datensatz der mit :select-Anweisung geladen wurde sind die nicht gewählten Felder nil. Beim Aufruf von save werden diese Felder daher auch in der Datenbank mit NULL überschrieben! Mit reload können die fehlenden Felder nachgeladen werden. (Selbstverständlich bringt es nichts mit :select und reload zu arbeiten, wenn save auf JEDEN Datensatz angewendet werden soll).

belongs_to-Models laden mit :include

Angenommen beim BlogPost-Beispiel gibt es noch ein zugehöriges “Author”-Model, welches über belongs_to/has_many mit BlogPost verbunden ist und wir den Benutzernamen mit anzeigen wollen, dann würden wir bei der Ausgabe <blogpost-instanz-variable>.author.nickname aufrufen.

Da ActiveRecord nicht weiss, dass das Author-Model verwendet werden soll, wird es mit einer zweiten SQL-Abfrage in dem Moment geladen, wo auf das Feld nickname zugegriffen wird. Das ist insb. in Schleifen ungünstig, da n+1 SQL-Abfragen dazu notwendig sind (Eine Abfrage um die Liste der BlogPosts zu erstellen, dann n Abfragen um für jeden BlogPost den Author zu laden).

Per SQL kann mit der JOIN-Anweidungs der zugehörige Datensatz direkt (in einer Abfrage) geladen werden. ActiveRecord erzeugt Joins mit der :include-Option:

@posts = BlogPost.find(:all, :order => 'created_at', :limit => 100, :include => 'Author')

Mögliche Komplikationen: Im Beispiel wird nach der Spalte created_at sortiert, welche möglicherweise in beiden Models/Tabellen vorhanden ist. Man erhält dann eine ActiveRecord::StatementInvalid-Exception und folgende Fehlermeldung:

Mysql::Error: Column 'created_at' in order clause is ambiguous: SELECT ...

created_at gibt es in beiden Tabellen, daher ist es unklar nach welchem der Felder sortiert werden soll. Da der Inhalt von :order direkt an die SQL-Abfrage angehängt wird, kann man dort die Tabelle direkt mit angeben, z.B. so:

@posts = BlogPost.find(:all, :order => 'blog_posts.created_at', :limit => 100, :include => 'Author')