[Munich-pm] Elche einfrieren - oder: Alle Wege führen nach ORM

Harald Jörg haj_58 at web.de
Mi Aug 2 11:31:04 PDT 2017


Renée schreibt:

> Am 02.08.2017 um 01:47 schrieb Harald Jörg:
>>
>> Das soll dann in etwa so aussehen:
>>
>>   package New::Class;
>>   use Moose;
>>   extends 'Old::Class';
>>   with 'My::MooseX::ORM';
>>
>>   # ...und dann später...
>>   my $object = New::Class->new(%params);
>>   my $id = $object->store;
>>
>>   # am nächsten Tag:
>>   my $object = New::Class->load_object($id);
>>   my $objects_ref = New::Class->load; # lädt die ganze Tabelle
>
> Ein Ansatz wäre, ein eigenes Attribut für Attribute zu schreiben:
>
>     package DBAttribute;
>
>     use Moose;
>     extends 'Moose::Meta::Attribute';
>
>     has is_column => (
>         is  => 'rw',
>         isa => 'Int',
>     );
>     no Moose;
>
>     1;

Die Eigenschaft "is_column" ist bei mir durch die Verwendung der Rolle
My::MooseX::ORM schon gesetzt, für alle Attribute der Klasse.  Die
Meta-Ebene hätte ich vorgesehen, wenn ich spezifische Angaben machen
wollte, *wie* ein Attribut zu speichern ist, oder ob z.B. für ein
Attribut ein Index anzulegen ist.  Das orientiert sich an
Moose::Cookbook::Meta::Labeled_AttributeTrait, weil das Erweitern von
Moose::Meta::Attribute als nicht mehr ganz so schick angesehen wird
("Subclassing metaclasses (as opposed to providing metaclass traits) is
strongly discouraged.") - ich habe das einfach so hingenommen.

Das läßt sich aber leider nicht durchgängig erreichen. Old::Class hat
Attribute, die selbst Objekte in "alten" Klassen sind, oder auch Objekte
von nicht-Moose-Klassen:

   has 'other'     => (isa => 'Old::Other::Class');
   has 'reference' => (isa => 'URI');

...und ich kann zwar die DB-Rolle in Old::Other::Class reinschrauben,
hätte dann aber immer noch keinen Platz, erweiterte Eigenschaften der
Attribute von Old::Other::Class abzulegen.
   

> In einer Rolle kannst Du dann folgendes machen:
>
>     package DBRole;
>
>     sub store {
>         my ($self, %param) = @_;
>         _check_table(); # check if table exists, add the table if it's
> missing
>         if ( $entry_exists ) {
>             $self->_update(%param);
>         }
>         else {
>             $self->_insert(%param);
>         }
>     }

Richtig, store ist bei mir eine Methode der Rolle My::MooseX::ORM.
store hat keine Parameter, denn das, was zu speichern ist, ist alles
über $self abgreifbar.  Du verwendest in sub insert {} weiter unten den
übergebenen Hash übrigens auch nicht :)

Allerdings: Das reicht so nicht!  Ein $object->store kann mehrere
Tabellenoperationen auslösen, wenn die Objektattribute Referenzen sind.
Die Rolle soll das Objekt mit allen Anhängseln speichern, genau wie die
"Object Graph storage engine" KiokuDB das macht, nur eben mit mehr
abgreifbaren Spalten.

>     sub load { ... }
>     sub load_objects { ... }

...und hier natürlich entsprechend: Um ein Objekt komplett aus der
Datenbank aufzubauen, müssen manchmal mehrere Tabellen gelesen werden.

>     sub insert {
>         my ($self, %param) = @_;
>
>         my $meta       = $self->meta;
>         my @attributes = $meta->get_attribute_list;
>         my @columns  = map{ $meta->get_attribute( $_ ) }@attributes;

Statt der letzten beiden Zeilen:

          my @columns = $meta->get_all_attributes();

...denn ich will ja auch die Attribute speichern, die die Klasse von
ihren Vorvätern ererbt hat, sonst f hlt was.

Explizit ausschließen muss ich die Attribute, die ich mir über die
Datenbank-Rolle einfange.  Das kriegt man über $attr->role_attribute und
$ra->associated_role->name hin.

>         my %columns;
>         for my $attribute ( @trackable ) {

Global symbol "@trackable" requires explicit package name
(did you forget to declare "my @trackable"?)  :)

Ich vermute mal, Dir schwebt hier ein Auswahlprozess über @columns vor?

>             if (
>                 $attribute->isa( 'DBAttribute' ) &&
>                 $attribute->is_column
>             ) {
>                 my $name = $attribute->name;
>                 $columns{$name} = $self->$attribute();
>             }
>         }
>
>         my $table = ...; # get class name from meta data
>         my $column_names = join ', ', keys %columns;
>         my $placeholders     = join ', ', ('?') x keys %columns;
>
>         my $SQL = "INSERT INTO $table ( $column_names ) values (
> $placeholders )";
        + my $sth = $dbh->prepare($SQL);
>         $sth->execute( values %columns );

Wenn der Wert eines Attributs ein ArrayRef ist, schreibst Du hiermit
'ARRAY(0x55b8ea21f790)' in die Datenbank... (siehe Bemerkung oben).

>     }

An dieser Stelle habe ich mit ähnlichem Code einen lustigen Fehler
gemacht: Bei mehrstelliger Anzahl von INSERTs mag man nicht jedes Mal
einen $dbh->prepare absetzen, denn eigentlich ist doch das SQL-Statement
eine Eigenschaft der Klasse (fast richtig), also immer das gleiche
(*FALSCH*).  Die Reihenfolge der key/value-Paare ist zufällig, d.h. in
der Regel bei jedem Objekt anders.  Das gibt dann bei jedem Testlauf
eine andere Fehlermeldung, weil's eben Zufall ist, welcher Placeholder
als erstes auf einen nicht kompatiblen Datentyp trifft.

Jedesmal den prepare abzusetzen hat auch seine ... Nebeneffekte. Es soll
Datenbanken geben, die unter Putz alle SQL-Strings cachen, damit der
nächste prepare mit dem gleichen String schneller geht.  Bei 16
Attributen gibt es 16! oder 2*10^13 verschiedene SQL-Statements, das
dauert dann ein wenig, bis der Cache mal einen Treffer vermelden kann.

> In der New::Class dann:
>
>   package New::Class;
>   use Moose;
>   extends 'Old::Class';
>   with 'My::MooseX::ORM';
>   with 'DBRole';
>
>   has id => (
>       is => 'ro',
>       isa => Int,
>       is_column => 1,
>   );

Siehe oben: Ich will die Eigenschaften der Attribute der Old::Class
nicht wiederholen.  Eine Id als eindeutige Identifikation des Objekts in
der Datenbank habe ich in die Rolle gesteckt: Als Attribute in
New::Class möchte ich nur Eigenschaften sehen, die als Hinweise für das
Speichern der Objekte gebraucht werden.

>   # ...und dann später...
>   my $object = New::Class->new(%params);
>   my $id = $object->store;
>
>   # am nächsten Tag:
>   my $object = New::Class->load_object($id);
>   my $objects_ref = New::Class->load; # lädt die ganze Tabelle
>
> Du musst Dir aber noch überlegen, wie Du mehr Spalteninformationen
> (z.B. Datentyp) etc. ablegen willst oder ob Du das auf Basis
> vorhandener Werte ermitteln willst...

Richtig!

Für mein aktuelles Problem komme ich schon sehr weit mit den
Moose-Basistypen.  Das liegt aber am Problem: Weil die Daten alle aus
XML-Dateien, Excel-Dateien und Webformularen kommen, sind sie alle erst
einmal Strings und die Objekte können aus Datenbank-Strings genauso
zusammengesetzt werden wie aus den Originaldateien.  Ein paar der
Strings werden für die Verarbeitung geparst, und erst bei denen beginnt
es, so richtig interessant zu werden.

...und es gibt noch viele interessante Baustellen, bei denen ich eine
Vorstellung habe, wie's geht, aber das braucht alles seine Zeit:

   * Datentypen wie 'Maybe[Str]': Wenn ich bei einer NULL in der
     Datenbank nicht unterscheiden kann zwischen "Attribut nicht
     angegeben" oder "Attribut explizit auf undef gesetzt", dann kann
     ich die Objekte nicht genauso aufbauen wie sie zum Zeitpunkt des
     store() waren.  Ist für mich aktuell nicht erforderlich, gehört
     aber dazu, weil sonst Prädikate vor und nach dem Speichern
     unterschiedliche Ergebnisse liefern können.

   * Beim load() aus der Datenbank kann man die Objekte im allgemeinen
     nicht mit Class->new(%params) aufbauen.  Da können mindestens
     init_arg, BUILDARG und BUILD in die Quere kommen.

   * Type Unions (isa => 'Str | App::Keyword') sind vermutlich auch für
     jedes andere Datenschema einigermaßen lästig.

   * Basisklassen als Attributtypen (isa => 'Base::Class'): Wenn man
     anhand solcher Informationen die Tabellen aus den Attributen der
     Klassen aufbaut, dann fehlen möglicherweise Spalten für Objekte,
     die auf Base::Class aufbauen und mehr Attribute aufweisen.  Anders
     als bei Type Unions merkt man das erst bei der Verarbeitung der
     Objekte.  Wie in Deinem Beispiel erfolgt also die Erzeugung der
     Tabellen erst beim store!

   * Gemischtwarenläden (isa => 'ArrayRef[Object]'), bei denen die
     einzelnen Array-Elemente bestenfalls eine gemeinsame Basisklasse,
     aber individuelle Attribute haben: Sowas fängt man sich ganz gern
     beim Parsen von XML-Dokumenten ein.

   * Wiederverwendung von Objekten: Wenn Attribute verschiedener Objekte
     das gleiche Objekt enthalten, dann soll es auch nach einem
     store/load-Zyklus wieder das gleiche Objekt sein.  Das ist nebenbei
     der Grund, woran die Verwendung von Data::Dumper+eval als
     Implementierung von store+load scheitert.

Wenn ich das so aufschreibe und betrachte: Vielleicht ist es auch
einfach eine Schnapsidee, die ich grade verfolge und es hat gute Gründe,
warum das keiner so macht...
-- 
Cheers,
haj


Mehr Informationen über die Mailingliste Munich-pm