Freitag, 24. Februar 2012

Ein paar Takte zu Uniqueness-Validatoren in ActiveRecord und DataMapper

Sowohl ActiveRecord als auch DataMapper (1.2) haben eine kleine fiese Angewohnheit:
Sie überprüfen bei jedem Speichern sämtliche "uniqueness"-Constraints des jeweiligen Modells.

Ein kleines Beispiel. Gegeben sei ein Modell der folgenden Form:
class Foo < ActiveRecord::Base
  validates_uniqueness_of :bar
end
Dann zeigt uns die Konsole folgendes:
irb> o = Foo.first
=> #<Foo id: 1, bar: "some_value">
irb> ActiveRecord::Base.logger = Logger.new(STDOUT) && nil
=> nil
irb> o.save
  Foo Exists (20.8ms)  SELECT 1 FROM `foos` WHERE (`foos`.`bar` = BINARY 'some_value' AND `foo`.`id` != 1) LIMIT 1
=> true
Das Feld `bar` wird also auf Eindeutigkeit überprüft, obwohl es überhaupt nicht geändert wurde. Das mag in einer Welt ohne Datenbank-Constraints wie bei ActiveRecord sogar irgendwie noch sinnvoll sein, bei DataMapper (was Datenbank-Constraints unterstützt), erscheint mir dies vollständig unsinnig.

Abhilfe

ActiveRecord
Jedes ActiveRecord-Objekt stellt Methoden bereit, um zu überprüfen, ob ein Attribut geändert wurde oder nicht. Eine solche Methode hieße für ein Feld mit dem Namen `foo` beispielsweise `foo_changed?`. Verwendet man dies, so sähe unser obiges Beispiel wie folgt aus:
class Foo < ActiveRecord::Base
  validates_uniqueness_of :bar, :if => :bar_changed?
end
DataMapper
Bei DataMapper gibt es ebenfalls die Möglichkeit eine Validierung mit einer `:if`-Option nur unter bestimmten Voraussetzungen auszuführen. Allerdings gibt es hier keine `_changed?`-Methoden für jedes Feld, sondern "nur" eine Methode, die Auskunft über alle geänderten Methoden gibt. Diese Methode heißt `dirty_attributes` und gibt einen Hash der Form `<#DataMapper::Property...> => dirty_value` zurück. Nutzt man dies aus, so könnte man folgendes schreiben:
class Foo
  include DataMapper::Resource
  property :bar, String
  validates_uniqueness_of(:bar, :if => lambda{|r| r.dirty_attributes.has_key?(Foo.properties[:bar])})
end
Dies ist natürlich vergleichsweise umständlich und ermöglicht nicht den Einsatz der praktischeren `:unique`-Option in der `property` selbst. Ich habe mir deshalb für diese Zwecke die DataMapper::Resource.property-Methode wie folgt überschrieben:
module MyModelExtension
  def property(name, type, options = {})
    options = options.dup
    unique = options.delete(:unique) # Don't pass the :unique option to DM directly
        
    property = super(name, type, options)

    if unique
      opts = {:if => lambda{|res| res.dirty_attributes.has_key?(property)}}
      opts.merge!(:scope => unique) if unique.is_a?(Array)
      self.validates_uniqueness_of(name, opts)
    end

    return property
  end

  DataMapper::Model.append_extensions self
end
Damit sollte dann auch wieder die `:unqiue`-Option funktionieren:
class Foo
  include DataMapper::Resource
  property :bar, String, :unqiue => true
end

Dienstag, 12. April 2011

ActiveRecord Migrations in Rails 3.0 Engines

Im aktuellen Rails 3.0 fehlen aus meiner Sicht noch einige recht wichtige Features der Rails-Engines:
  • Datenbank-Migrationen aus Eninges
  • Tasks zum Kopieren oder Verlinken von Assets (/public Verzeichnis in der Engine)
  • Die Möglichkeit eine Engine in den Routen der Applikation zu "mounten" 
All diese Punkte werden zwar mit dem Rails 3.1 Release voraussichtlich behoben sein, aber gerade die ersten beiden Punkte sind auch unter Rails 3.0 relativ einfach selbst zu adressieren. So genügt zum Einbetten und Bereitstellen eigener ActiveRecord-Migrationen ein einfacher rake-task (kopiert aus dem Rails core und leicht modifiziert):
    task :migrate => :environment do
      ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
      ActiveRecord::Migrator.migrate(MeinEngineNamespace::Engine.find_root_with_flag("db").join('db/migrate'), ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
      Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
    end
Der einzige Unterschied zu dem original-task im Rails core ist das MeinEngineNamespace::Engine.find_root_with_flag("db"). Dort steht im "original" ein einfaches Rails.root.
Ist der obige Task irgendwo in der Engine eingebaut (möglichst auch in einem aussagekräftigen Namespace), so können damit aus einer Applikation alle Migrations-Scripte im db/migration Verzeichnis der Engine "rauf" und auch wieder "runter" migriert werden (analog zum "gewohnten" db:migrate Task).

Donnerstag, 7. April 2011

Benutzerdefinierte HTTP Verben wie 'PURGE' mit Net::HTTP

Betreibt man eine Webanwendung beispielsweise hinter einem vorgelagerten Reverse-Proxy, so kann unter Umständen eine aktive Invalidierung der Cache-Inhalte des Proxys durch die Anwendung selbst erforderlich werden. Das bedeutet, dass die Anwendung aktiv den Reverse-Proxy anweist bestimmte URLs zu "vergessen". Proxys wie beispielsweise Squid bieten die Möglichkeit dies per HTTP mit dem nicht weiter standardisierten Verb "PURGE" zu erledigen. Bei anderen Proxys wie Varnish ist ein PURGE per HTTP zwar nicht vordefiniert aber relativ leicht zu konfigurieren.

Doch wie soll man einen solchen Request aus der Web-Anwendung absetzen? Im Ruby-Umfeld bietet Net::HTTP dafür die Möglichkeit eigene HTTP-Verben zu definieren. Dazu muss man an geeigneter Stelle die folgende Klasse definieren:
require 'net/http'

class Net::HTTP::Purge < Net::HTTPRequest
  METHOD = 'PURGE'
  REQUEST_HAS_BODY = false
  RESPONSE_HAS_BODY = true
end
Dies funktioniert natürlich äquivalent auch mit anderen Verben. Der Request selbst kann dann wie folgt abgesetzt werden:
  http = Net::HTTP.new("mein.proxy.de", "80")
  request = Net::HTTP::Purge.new("/url", "")
  http.request(request)