groovy, dsl und templates

erstellt am 17.11.2009 | 2 kommentare

themen: groovy, mda/mdd, dsl, template

groovy-dsl-tutorial

im artikel dsl mit groovy gaben wir anhand eines kleinen beispiels eine einführung in groovy-dsl. wir haben eine tabelle mit ihren spalten definiert und dann ein create-table-statement daraus generiert.

da es eine groovy-dsl einführung war, haben wir ein wichtiges prinzip der softwareentwicklung, nämlich die trennung von model und view, vernachlässigt. (zum erzeugen des create-statements hatten wir die create-funktionen im Table- und ColumnModel.) das möchten wir in diesem artikel nachholen.

unser beispiel:

angenommen sie entwickeln eine datenbankanwendung mit hunderten von tabellen und eine anforderung ist, dass die anwendung mit unterschiedlichen datenbanken läuft. mit unserer bisherigen lösung müssten wir für jeden datenbankdialekt eine eigene create-funktion implementieren. da wir unser model aber nicht mit noch mehr code überfrachten wollen, wählen wir einen anderen weg: groovy-temples. (anmerkung: wir gehen der einfachheit halber davon aus, dass es eine reine sql-anwendung ist und kein orm-framework verwendet wird)

groovy besitzt ein template framework, und mit der SimpleTemplateEngine wird es uns leicht gemacht, vorlagen für die create-statements je datenbank-typ zu erstellen und damit die create-statements zu generieren.

vorbereitung

wir verwenden die groovy-programme aus unserem vorigen artikel (dsl mit groovy).
als erstes bereinigen wir unsere model-klassen und entfernen die create-funktionen.
zur besseren übersicht hier noch einmal die beiden klassen:
class TableModel {
   /**
    * Name der Tabelle
    */
   String name
   /**
    * Liste der Columns
    */
   ArrayList<ColumnModel> columns = new ArrayList<ColumnModel>()
}

class ColumnModel {
    /**
    * Tabelle, zu der diese Column gehört
    */
    TableModel table
    /**
    * name der column
    */
    String name
    /**
    * datentyp
    */
    String datatype
   /**
    * not null
    */
   boolean notnull
}

   

unsere templates

unser erstes template soll ein create-table-statement für eine MySql-datenbank sein, und so aussehen:
create table test (
id int not null,
name text
)
engine = InnoDB;
   

einem groovy-template werden daten übergeben, die innerhalb des templates verwendet werden können.
in unserem fall übergeben wir ein TableModel-objekt mit dem namen "table". (code siehe weiter unten).

im template selbst können wir groovy-code ausführen:
- ${xxx}: mit dieser syntax wird der wert, den xxx liefert, im template ausgegeben
- <% xxx %>: hiermit wird xxx als groovy-code ins template eingebettet
zum besseren verständnis verwenden wir in unserem ersten template beide varianten.

unser template ist folgendermaßen strukturiert:

01 create table ${table.name} (
02 <% table.columns.eachWithIndex{ col, i -> %>
03 ${col.name} ${col.datatype} <% if (col.notnull) { %>not null<% } %>
04 <% if (i+1 < table.columns.size()) { %>,<% } %>
05 <% } %>
06 )
07 engine = InnoDB;
nun die erklärung zu den einzelnen zeilen:
01: "create table" ist ein fixer text, mit "${table.name}" wird das "name"-property aus unserem table-objekt ausgegeben
02: hier wird mittels der "<% xxx %>"-syntax groovy-code eingebettet: es wird über alle columns im table-objekt iteriert. (mit index)
03: dieser teil wird für jede column ausgeführt und gibt column.name, column.type und "not null" wenn column.notnull == true
04: dieser teil dient nur dazu, um nach jeder außer der letzten column ein komma (",") auszugeben
05: das hier ist das ende der "table.columns.eachWithIndex { col, i -> " anweisung. d.h. der each-loop ist hier beendet
06 und 07: geben nur mehr einen fixen text aus.

das template-code wurde nur zwecks erklärungen in mehrere zeilen aufgeteilt. würden wir das template wie hier verwenden kämen zu viele zeilenumbrüche zustande, da alle zeilenumbrüche auserhalb von groovy-code ausgegeben werden.

daher zusammenfassend unser endgültiges MySql-template (gespeichert in der datei MySql.tpl):

create table ${table.name} (<% table.columns.eachWithIndex{ col, i -> %>
${col.name} ${col.datatype} <% if (col.notnull) { %>not null<% } %><% if (i+1 < table.columns.size()) { %>,<% } %><% } %>
)
engine = InnoDB;
analog können wir auch für Oracle ein template schreiben (Oracle.tpl):
create table ${table.name} (<% table.columns.eachWithIndex{ col, i -> %>
${col.name} ${col.datatype=="text"?"clob":col.datatype} <% if (col.notnull) { %>not null<% } %><% if (i+1 < table.columns.size()) { %>,<% } %><% } %>
);
hier ist folgender abschnitt erklärungsbedürftig:
${col.datatype=="text"?"clob":col.datatype}
da es in Oracle den datentyp "text" nicht gibt, wird statt "text" "clob" (character-large-object) als datentyp ausgegeben.
(anmerkung: in einer realen anwendung würde man den datentyp nicht als text sondern als enum definieren und jeweils den der datenbank entsprechenden datentyp ermitteln.)

SimpleTemplateEngine

wir haben unsere templates definiert, nun wollen wir sie auch verwenden:

dazu bietet uns groovy mit der SimpleTemplateEngine die einfachste möglichkeit.
hier die nötigen schritte:

import groovy.text.SimpleTemplateEngine

def engine = new SimpleTemplateEngine()

erzeugen einer engine-instanz
def template = engine.createTemplate(template)
erzeugen einer template-instanz. template kann dabei ein String, ein File, eine URL oder ein beliebiger Reader sein
template = template.build(model)
erzeugen anhand der im model übergebenen daten (als Map) das ergebnis
def result = template.toString()
liest das ergebnis als String aus

anmerkung: der einfachheit halber kann man die createTempate() und build() - schritte zusammenfassen:

def template = engine.createTemplate(template).build(model)
   

für unser beispiel ergibt sich damit:

import groovy.text.SimpleTemplateEngine

def builder = new TableBuilder();

def table = builder.table("test") {
   column "id", datatype: "int", notnull: true
   column "name", datatype: "text"
}

// Ausgabe
def engine = new SimpleTemplateEngine()

// Ausgabe für MySql
def template = engine.createTemplate(new File('MySql.tpl')).
make([table: table])
println "MySql-Statement:"
println template.toString()

// Ausgabe für Oracle
template = engine.createTemplate(new File('Oracle.tpl')).
make([table: table])
println "Oracle-Statement:"
println template.toString()

zuerst erstellen wir mit der dsl vom vorigen artikel eine tabellen-definition, die in der variablen "table" gespeichert wid.
danach erzeugen wir, wie oben beschrieben eine template-engine, und verwenden einmal das MySql-template und danach das Oracle-template.
als model übergeben wir der build-funktion die Map [table: table] (1. "table" ist die variable, wie sie im template verwendet wird, 2. "table" ist unsere lokale table-variable, die wir am anfang erzeugt haben.)
anstatt das ergebnis mit println an die konsole auszugeben könnten wir natürlich das ergebnis in .sql-dateien schreiben und es dann in der datenbank ausführen.

hier das erzielte ergebnis:

MySql-Statement:
create table test (
id int not null,
name text
)
engine = InnoDB;

Oracle-Statement:
create table test (
id int not null,
name clob
);

create table test (
id int not null,
name text
);

die source-files zu diesem artikel können sie hier downloaden.

danke für ihre aufmerksamkeit,
wir feuen uns über kommentare und anregungen.


kommentar abgeben

kommentare

  • Georgy:
    Die einfachste (aber auch unsicherste) Variante ist:

    1. Datei (z.B. "test.tab") mit der Tabellendefinition wie vorgeschlagen erstellen

    2. Test.groovy so abwandeln:

    builder = new TableBuilder(); // darf nicht mehr mit def definiert werden!

    def tabledef = (new File('test.tab')).text // liest die Tabellendefinition

    def table = evaluate("builder.$tabledef") // führt den Build-Prozess aus

    // danach weiter mit der Ausgabe

    erstellt von erich konicek, 01.04.2010 11:20 (vor 5 monat)

  • Wie kann man das Beispiel so umprogrammieren, dass die Informationen aus einer Datei gelesen werden. Das heißt, ich möchte einfach eine Datei haben, wo Folgendes steht:

    table("test") {
    column "id", datatype: "int", notnull: true
    column "name", datatype: "text"
    }

    Die möchte ich dann in meinem Script laden und daraus die create-Anweisung generieren.

    erstellt von Georgy, 31.03.2010 16:57 (vor 5 monat)

rss feed für kommentare dieser seite