Blog

DBIx::Class::DeploymentHandler und lange Indexnamen

07.09.2020 // Renée Bäcker

Wir nutzen in verschiedenen Projekte DBIx::Class als Abstraktionsschicht für die Datenbank. Das Schema der Datenbank entwickeln wir mit dem Tool MySQL-Workbench und die daraus resultierende Datei nutzen wir zur Code-Generierung wie in einem früheren Blogpost beschrieben.

Mit diesem generierten Code liefern wir auch die Änderung an der Datenbank aus. Dazu nutzen wir das Modul DBIx::Class::DeploymentHandler. Das funktioniert echt einfach:

use DBIx::Class::DeploymentHandler;
use Projekt::Schema; # DBIx::Class Schema

my $db = Projekt::Schema->connect(
    'DBI:Pg:dbname=projekt_db;host=unser.projekt.host',
    'db_user',
    'db_password',
);

my $deployer = DBIx::Class::DeploymentHandler->new({
    schema              => $db,
    script_directory    => '/path/to/db_upgrades',
    databases           => ['Pg'],
    force_overwrite     => 1,
    sql_translator_args => {
        quote_identifiers => 1,
    },
});

$deployer->prepare_install;
$deployer->install;

Wir instanziieren erst das Schema der Datenbank. Anschließend erzeugen wir ein Objekt um die Änderungen an der Datenbank auszurollen. Das Objekt benötigt noch den Pfad zu einem Verzeichnis in dem die Änderungsanweisungen gespeichert werden.

Bei prepare_install werden die YAML-Dateien in *db_upgrades *erstellt. In diesem YAML-Dateien ist eine Beschreibung der Datenbank: Welche Tabellen mit welchen Spalten gibt es. Weiterhin werden SQL-Dateien erstellt, die für die Datenbank passend die SQL-Befehle enthält.

Mit install werden dann die Änderungen in der Datenbank ausgerollt.

Und dabei sind wir auf ein Problem gestoßen, denn bei der Installation der Datenbank haben wir jede Menge Warnungen bekommen:

NOTICE:  identifier "common_internal_workphase_activity_idx_common_internal_workphase_id" will be truncated to "common_internal_workphase_activity_idx_common_internal_workphas"
NOTICE:  identifier "common_project_workphase_activity_idx_common_project_workphase_id" will be truncated to "common_project_workphase_activity_idx_common_project_workphase_"
NOTICE:  identifier "contract_remuneration_engineering_idx_contract_remuneration_engineering_facilitygroup_id" will be truncated to "contract_remuneration_engineering_idx_contract_remuneration_eng"
NOTICE:  identifier "contract_remuneration_facilitygroup_costgroup_idx_contract_remuneration_engineering_facilitygroup_id" will be truncated to "contract_remuneration_facilitygroup_costgroup_idx_contract_remu"
NOTICE:  identifier "contract_remuneration_engineering_costgroup_idx_contract_remuneration_engineering_id" will be truncated to "contract_remuneration_engineering_costgroup_idx_contract_remune"

Ursache ist, dass in PostgreSQL die Identifier nicht länger als 63 Zeichen sein. Sind sie es doch, werden sie gekürzt. Jetzt haben uns zwei Fragen beschäftigt:

  • Können wir längere Identifier erlauben?
  • Wo kommen die langen Namen eigentlich her?

Die erste Frage war relativ schnell beantwortet: Ja, geht. Aber dazu muss man PostgreSQL selbst kompilieren, da die maximale Länge im Quellcode hinterlegt ist (Immerhin, mit Closed Source hätte man nicht mal die Chance die Änderungen vorzunehmen und die Software neu zu bauen 😉 ). Mehr dazu gibt es in der Postgres-Doku.

Die zweite Frage war etwas schwieriger zu beantworten. Wir haben diese Identifier nirgends definiert. Also mussten wir nachvollziehen, wie DBIx::Class::DeploymentHandler arbeitet.

Da der fehlerhafte Befehl in der SQL-Datei erstmals auftaucht, musste es beim Erstellen der SQL-Befehle passieren. Das SQL kommt von dem Modul SQL::Translator, und der Index wird in der Methode add_index aus SQL::Translator::Schema::Table erstellt.

Letztendlich war die Lösung relativ einfach: Wir überschreiben einfach genau diese Methode:

use SQL::Translator::Schema::Table;
use Digest::MD5 qw(md5_hex);

{
  no warnings 'redefine';

  my %indexes;

  sub SQL::Translator::Schema::Table::add_index {
    my $self        = shift;
    my $index_class = 'SQL::Translator::Schema::Index';
    my $index;
 
    if ( UNIVERSAL::isa( $_[0], $index_class ) ) {
        $index = shift;
        $index->table( $self );
    }
    else {
        my %args = @_;

        $args{'table'} = $self;
        my $md5     = md5_hex( $args{$name} );
        $args{name} = 'idx__' . $args{table} . '__' . $md5;
        $index = $index_class->new( \%args ) or return
            $self->error( $index_class->error );
    }
    foreach my $ex_index ($self->get_indices) {
       return if ($ex_index->equals($index));
    }
    push @{ $self->_indices }, $index;
    return $index;
  }
}

Das ist das Schöne an Perl: Man kann Lösungen finden und anwenden ohne dass man den ursprünglichen Code anfasst. Hier nutzen wir die Möglichkeit, Subroutinen von jeder beliebigen Stelle aus für alle möglichen Namensräume zu definieren. Dazu muss nur der Vollqualifizierte Name der Subroutine angegeben werden.

Das einzige was wir beachten müssen ist, dass wir das Modul SQL::Translator::Schema::Table laden bevor wir die Subroutine definieren, denn sonst würde die Methode aus dem Modul unsere Subroutine wieder überschreiben.

Solche Änderungen sollten aber immer das letzte Mittel sein, da das Überschreiben von Subroutinen an ganz anderen Stellen als sie eigentlich definiert wurden, zu längeren Debugging-Sitzungen führen kann wenn mal Fehler auftreten.


Permalink: