Vollständige Dependent-Einstellungen in Rails-Models testen

Ein selten, aber leider regelmäßig wiederkehrendes Problem sind Fremdschlüsselbeziehungen beim Löschen. In Rails muss auf Seite des Schlüsselziels definiert werden, ob ein Fremdschlüssel auf null gesetzt werden darf oder ob das ganze Model gelöscht werden muss. Vergisst man diese Konfiguration wirft die Datenbank beim Löschen einen Fehler. Um dies zu vermeiden möchte ich alle Fremdschlüssel, die auf eine Model-Tabelle zeigen, überprüfen, ob sie entsprechend konfiguriert sind. Hierfür verwende ich ein shared Example in Rspec.

Fremdschlüssel finden

Die Schlüssel für das beschriebene Model lassen sich in zumindest mit Postgres folgendermaßen auflisten:

def foreign_keys(to_table)
  ActiveRecord::Base.connection.tables.map do |table_name|
    ActiveRecord::Base.connection.foreign_keys(table_name).select{ |index| index.to_table == to_table }
  end.flatten
end

Konfigurierte Assoziationen finden

Die Assoziationen eines Models finden wir mit folgendem Ansatz. Wir ignorieren Assoziationen mit Scopes, da hier nicht ohne weiteres festgestellt werden kann, ob diese alle möglichen Verbindungen abdecken. Auch beschränken wir uns auf Has-many- und Has-one-Beziehungen, da diese zumindest aktuell die einzig relevanten sind. Sollte hier etwas fehlen ist dies auch nicht weiter tragisch, da der Test im Zweifel lieber zu viel als zu wenig bemängeln soll.

def safe_associations(target_class)
  associations =
    target_class.reflect_on_all_associations(:has_many) +
    target_class.reflect_on_all_associations(:has_one)
  associations.select{ |assoc| assoc.options[:dependent].in?(%i{destroy nullify}) && assoc.scope.nil? }
end

Shared Example für verdächtige Keys

Nun können die gültigen Konfigurationen von den Fremdschlüsseln abziehen. Nur wenn die Differenz leer ist, sind alle Schlüssel konfiguriert. Da wir bei Scopes etwas übervorsichtig sind, erlauben wir aber auch eine ignore-Option, um bestimmte Fremdschlüssel zu ignorieren:

shared_examples 'a dependent model' do  |options = {}|
  it 'has a dependent option for has_* associations' do
    foreign_keys = foreign_keys(described_class.table_name)
    associations = safe_associations(described_class)
    ignore = options[:ignore] || []

    suspicious_keys = 
      foreign_keys.map{ |key| "#{key.from_table}.#{key.column}" } -
      associations.map{ |assoc| "#{assoc.klass.table_name}.#{assoc.foreign_key}" } -
      ignore

    expect(suspicious_keys).to be_empty
  end

  # ...
end

Nun noch die Fehlermeldungen modifizieren und etwas Doku ergänzen und schon kann das Example in Model-Specs eingebunden werden:

describe BookWithChapters do
  it_behaves_like 'a dependent model'
end

Datenbanken im Home-Verzeichnis speichern

Mit PostgreSQL ist es sehr einfach einen Datenbank-Cluster im Home-Verzeichnis eines Nutzers anzulegen. Dies kann sehr hilfreich sein, wenn beispielsweise nur das Home-Verzeichnis verschlüsselt wird und die Datenbank nicht offen herumliegen soll.

Unter Ubuntu 14.10 installieren wir Postgre und betrachten das automatisch angelegte Standard-Cluster:

sudo apt-get install postgresql

pg_lsclusters
Ver Cluster Port Status Owner Data directory Log file
9.3 main 5432 online postgres /var/lib/postgresql/9.3/main /var/log/postgresql/postgresql-9.3-main.log

weiterlesen

RSpec und PostgreSQL: Insufficient Privilege

Bei einem rake spec erhielt ich den folgenden Fehler:

PG::InsufficientPrivilege: ERROR:  permission denied to create database
[...]

Es mag eine falsche Einstellung sein (ein einfaches rspec verursacht das Problem nicht), aber offensichtlich benötigt der Datenbanknutzer in meiner Konfiguration das Recht, neue Datenbanken zu erstellen. Auf meiner lokalen Maschine, wo die Tests laufen, kann ich das verantworten.

Postgres-Kommandozeile aufrufen:

sudo su - postgres
psql

Recht zum Erstellen der Datenbank an Nutzer vergeben:

ALTER USER "my-db-user" CREATEDB;

SQL-Datenbankänderungen testen

Manchmal dauert es ein wenig, bis man auf das Offensichtliche kommt. Ich habe hin und her überlegt, wie ich ein Update und Alter Table testen kann, ohne die Daten zu gefährden. Die Lösung ist elementarer Bestandteil einer jeden SQL-Datenbank: Eine Transaktion.

Transaktionen in PostgreSQL

begin;
alter table mytable rename column id to name;
\d mytable

weiterlesen

PostgreSQL-Byte-Array mit PHP in Datei schreiben

Will man Binärdaten, die in einer PostgreSQL-Tabelle als Typ bytea hinterlegt sind, direkt ausgeben oder in eine Datei schreiben, muss die Rückgabe zunächst demaskiert werden. Andernfalls wird sich euer Bildbetrachter weigern, eine gespeicherte Grafik zu öffnen. Grund dafür ist, dass die Daten bei der Auswahl mit SELECT als Oktalzahlen zurückgegeben werden, die mit einem Backslash beginnen. Die Funktion pg_unescape_bytea entfernt diese.

weiterlesen