2022-05-10 Generic Row Mapper

Bereits letzte Woche über haben wir uns eine neue Lektüre vorgenommen. Wir genießen die Morgen bei Sonnenschein mit einem Kapitel aus dem pragmatischen Programmierer.

Für unsere DataModelEngine haben wir einen GenericRowMapper implementiert. Bisher haben die RowMapper aus JDBI gute Arbeit geleistet. Nun haben wir neue Anforderungen, die scheinbar nicht mehr erfüllt werden können. Wir benötigen nun die Möglichkeit komplexere Joins abzusetzen und dabei unter anderem eine Tabelle mehrfach zu joinen, da es sich je nach Fall um eine andere Relation handeln kann.

Im Kern nutzt unser RowMapper die Funktionen aus JDBI. Beispielsweise kann auf das Ergebnis eines Selects typesafe zugegriffen werden. Ein Int-Wert ist ein Int-Wert und kein Wert vom Typ String. Allerdings muss der Aufrufer der JDBI-Funktionen (also unser RowMapper) wissen, ob er GetString(ColumnId) oder GetTimestamp(ColumnId) aufrufen muss. Das Mappen von Rows unterteilt sich in zwei Phasen. Es gibt eine Analyse- und eine Mapping-Phase.

Die Analyse-Phase besteht aus drei Schritten:

  1. EntityCache initialisieren
    Da im Falle von Joins mehrere Spalten zu einer Entität gehören können, bestimmen wir für jede Spalte einen Entity Cache. Für jede Spalte, die sich auf die selbe Entität bezieht, verwenden wir den selben Cache. Beim ersten Zugriff auf den Cache wird eine Instanz der Entität erzeugt.
    Der Zugriff auf diesen Cache passiert mittels ColumnId (1,2,3,4, etc.).
  2. ColumnValueMapper initialisieren
    Für jede Spalte wird ein ValueMapper instanziiert. Diesem wird eine Funktion zugewiesen, die bei Ausführung bspw. JDBIs resultSet.GetInt() oder resultSet.GetString() aufruft. Außerdem kennt der ValueMapper die Spalte, für die er zuständig ist. D.h. in der Mapping-Phase werden die einzelnen Mapper aufgerufen, die Zeilendaten werden übergeben und der Mapper kann zielsicher auf der jeweiligen Spalte seine ihm übergebene Funktion zum Mappen anwenden. Wodurch die Ausführung sehr performant ist.
    Auch dieser Zugriff auf diesen Cache erfolgt mittels ColumnId (1,2,3,4, etc.).
  3. KMutableProperty bestimmen
    Als letztes benötigen wir die Properties, da die PropertySetter aufgerufen werden müssen, um einen Property Wert zu setzen. Auch diese cachen wir für einen schnellen Zugriff.

Am Ende sieht die Initialisierung wie folgt aus:

Initialisierung Caches

Am Ende mussten wir unseren EntityCache, ColumnValueMapper und KMutableProperty nur noch mit den gelesen Daten füttern. Auf der erzeugten Entität, für das jeweilige Attribut, wird der gemappte Wert gesetzt und wir hatten unser gewünschtes Ergebnis.

Interessant ist die Vorgehensweise für uns, da wir das Cachen von Funktionen für einen späteren und performanten Aufruf so noch nicht verwendet haben.

2022-05-03 Service Provider goes Maven Central

Es ist so weit. Unsere erste Library, der Service Provider, geht auf Maven Central live!
Außerdem ist eine Menge Zeit in die Weiterbildung in Generics geflossen. Um typesafe auf eine Sammlung von Klassen zugreifen zu können, ist unsere Core-Library um eine GenericStoreFactory reicher geworden, mit dessen Hilfe bis zu 20 Type-Parameter übergeben werden können.
Nebenbei haben wir uns ein paar Powershell-Skripte angelegt, mit denen wir unsere Publishing-Repositories leichter verwalten können. Zum Anlegen eines neuen Projektes, aus dem später eine Library entstehen soll, waren einige manuelle Schritte notwendig. Anstatt diese zu dokumentieren, haben wir ein Template-Projekt angelegt und für eine einfachere Verwaltung ein Skript angelegt, welches uns das Template-File aus IntelliJ in unser Publishing-Repository pusht. Im Zuge der Veröffentlichung auf Maven Central haben wir unsere Authentifizierungsinformationen in die gradle.properties-Datei unseres jeweiligen Users umgezogen und mussten uns einen Ordner suchen, in dem wir den private Key zum Signieren unserer Dateien ablegen. Da die Pfade zu unseren Usern unterschiedlich sind, schreibt ein weiteres unserer Skripte, nachdem es die Dateien gedownloaded hat, die Dateipfade um. Wir ersparen uns so eine Menge manueller Schritte.

2022-04-26 Code Review Service Provider

Nachdem wir die letzten Wochen Ostern genossen und an kleineren Dingen gearbeitet haben, soll der Service Provider in den kommenden Woche live gehen. Dazu haben wir uns die letzten zwei Tage genommen und ein großes Code Review des Services Providers durchgeführt. Neben kleineren Anpassungen haben wir die Doku vervollständigt und vereinheitlicht, die Prüfung zirkulärer Beziehung beim Instanziieren von Services mit abhängigen Services im Constructor gerefactored und Unittests um fehlende Prüfungen ergänzt.

Im Großen und Ganzen sind wir sehr zufrieden. Einige Unittests müssen noch überarbeitet werden und nächste Woche wollten wir den Service Provider in Maven publishen.

Unser Service Provider Interface sieht jetzt wie folgt aus:

ServiceProvider Interface Definition

Für den vereinfachten Aufruf mit Generic Types gibt es Extension Functions, die äquivalent zu den Funktionen des Interfaces sind:

ServiceProvider Extension Functions

2022-04-05 Let‘s INNER JOIN Maven

So..nachdem wir im Schnee beim Boarden wieder etwas Energie getankt haben, ist unser Projekt diese Woche wieder ein paar Schritte vorangeschritten. Zum ersten Mal haben wir uns an die Relationen gewagt und die OneToOne-Relation implementiert. Somit können wir nun zwei Entitäten miteinander joinen.

Hier ist ein Beispiel unserer bisherigen Implementierung. Eine Person hat eine DriversLicense, die mit der Person mittels „oneToOne<DriversLicense>“ verknüpft wird:

class Person : Entity() {
    companion object : DataAccessObject<Person>()
    var firstname by string()
    var lastname by string()
    var driversLicense by oneToOne<DriversLicense>()
}

class DriversLicense : Entity() {
    companion object : DataAccessObject<DriversLicense>()
    var description by string().nullable()
}

Das zugehörige generierte Sql ist das folgende:

SELECT 
  a.id aid, a.firstname afirstname, 
  a.lastname alastname, 
  a.createdAt acreatedAt, 
  a.changedAt achangedAt, 
  b.id bid, b.description bdescription, 
  b.createdAt bcreatedAt, 
  b.changedAt bchangedAt 
FROM persons a 
INNER JOIN drivers_licenses b 
ON a.drivers_licenses_id = b.id

Außerdem haben wir nach viel Herumprobieren und einer Menge Refactoring in unserem Deployment unser erstes Testprojekt im Maven Snapshot-Repository veröffentlicht. Maven ist sehr strickt, was das Veröffentlichen angeht und es müssen gerade beim ersten Mal einige Schritte durchlaufen werden, bis man überhaupt einen Account mit eigenem Namespace erstellt hat, mit dem man etwas veröffentlichen darf. Außerdem mussten Keys generiert, als Zertifikat online zur Verfügung gestellt und Files damit signiert werden . Das POM-File benötigt zusätzliche Informationen, zusätzliche Dateien wie Javadocs müssen generiert und mit der Library mitgeliefert werden und und und. Lange Rede..mit dem Service Provider wird bald unsere erste Library auf Maven zu finden sein.

2022-03-08 Vorbereitung Release Service Provider

Allmählich geht uns die Luft in unserer Anstellung aus. Die Arbeit macht immer weniger Spaß, Projekte ändern sich, Vorgesetzte ändern sich, das Team soll möglicherweise aufgeteilt werden, wir setzen wieder auf ältere Technologien auf. Andererseits sind wir mit Team Slimster nicht so vorangekommen, wie es erforderlich wäre, um damit selbstständig zu werden. Viel Zeit haben wir in unsere eigene Ausbildung und eigene kleine Frameworks gesteckt, die wir nutzen wollen, bei denen es uns Spaß gemacht hat, sie zu entwickeln. Nun haben wir überlegt, ob es nicht sinnvoll wäre diese Tools anderen Entwicklern zur Verfügung zu stellen, sich Feedback einzuholen, Steine ins Rollen zu bringen und unser Wissen in den Themen Entwicklung, UX, Scrum etc. zu nutzen und es anderen entgeltlich zur Verfügung zu stellen.

Beginnen werden wir damit unseren Service Provider zu publishen. Dafür haben wir noch etwas Zeit in die Entwicklung kleinerer Verbesserungen gesteckt und die Testabdeckung wieder auf 100% gebracht. Beispielsweise können nun Services instanziiert werden, die von anderen Services abhängig sind. Gemeint sind Services mit Parametern in der Constructor-Schnittstelle. Als nächsten werden wir eine Nutzerdokumentation nach unseren Vorstellungen schreiben und diese in einem Wiki zur Verfügung stellen. Dann schauen wir was passiert.

Auf der anderen Seite haben wir an unserem Persistenz-Framework entwickelt. Hier gab es einige Erweiterungen zur Deklaration von DataObjects. Außerdem haben wir zwei weitere Operationen für unsere Where-Condition eingeführt.

Neuerungen zur DataObject Deklaration:

@TableName("essay")
class Book : Entity() {
    companion object : DataAccessObject<Book>()

    @Length(40)
    var isbn by string()
    var title by string()
    var description by string().nullable()
    var completed by boolean()
}
  1. Die Annotation „TableName“ kann nun verwendet werden, um einen alternativen Namen für die Ablage der DataObjects (bspw. eine Datenbanktabelle) in der Datenquelle (bspw. Datenbank) angeben zu können.
  2. Wir haben eine Annotation „Length“ eingeführt, um die Länge eines Properties, wie es in der Datenquelle modelliert werden soll, zu bestimmen.
  3. Properties können jetzt nullable sein.
  4. Uns ist aufgefallen, wir unterstützen gar nicht den Basistyp Boolean und haben diesen nachgezogen.

Neue Operationen:

Book.find { where { id.between(1, 10) } }
Book.find { where { id.within(1, 5, 6, 7) } }
  1. In einer Where-Condition kann nun mit between gefiltert werden. Da between die zwei Parameter von und bis hat, kann die Funktion nicht als Infix deklariert werden. D.h. between muss zwangsläufig, anders als unsere anderen Operation wie eq, gt oder not, mit der Punktannotation aufgerufen werden.
  2. Des Weiteren kann nun die Operation in verwendet werden. Auch hier gilt die Punktannotation, da mehrere Werte als Varargs übergeben werden können. Da das Keyword in bereits tief in Kotlins Sprachgebrauch verwoben ist (und nicht verwendet werden darf), haben wir uns stattdessen für within als Namen für die Operation entschieden.

Das waren die wichtigsten Themen in dieser Woche.

2022-03-01 Diese Woche im Programm:

Backend-Framework Entwicklung. Wir sind immer noch dabei. Unser Persistenz-Framework nimmt immer mehr Gestalt an und wir haben schon so viel dabei gelernt. Wir lieben die Möglichkeit typesafe auf unsere Entitäten zuzugreifen, was in ABAP nicht denkbar war. Diese Woche haben wir die Möglichkeit unserer Where-Conditions fertiggestellt. D.h. wir können typesafe Bedingungen im Code formulieren und das Ergebnis von irgendeiner Persistenz (derzeit MySQL) anfordern. Vor Kurzem gab es für das Absetzen von Conditions noch mehrere Möglichkeit. Uns war es zu wüst und letztendlich gibt es nur noch eine.

In sämtlichen Fällen gibt es eine Funktion „where“, die eine Condition zurückliefert. Hier gibt es 3 Möglichkeiten:

1. die einfache Bedingung. Über die Attribute des DataObjects können die Operatoren eq, gt, ge, lt, le, not, like aufgerufen und der Vergleichswert übergeben werden.

val trees = Tree.find { where { type eq TreeType.LEAF } }

val trees = Tree.find { where { name like "%in%" } }

2. die kombinierte Bedingung. In diesem Fall eine einfache OR Verknüpfung. Bedingungen können durch das Aneinanderreihen oder durch Verwendung von varargs geschachtelt werden. Der zweite Fall wird benötigt, um Bedingungen durch Klammern von einander zu trennen.

Tree.find {
    where {
        (type eq TreeType.LEAF).or(type eq TreeType.NEEDLE)
    }
}

Tree.find {
    where {
        or(type eq TreeType.LEAF, type eq TreeType.NEEDLE)
    }
}

3. die literal Bedingung. Der Verwender kann jede Art von Request absetzen. Platzhalter werden durch Bindings ersetzt. Diese Variante ist nicht typesafe, sondern dient für Requests, die derzeit nicht durch das Framework unterstützt werden.

Tree.find {
    where {
        literal("type IN ?")
            .bindList(listOf(TreeType.LEAF, TreeType.PALM))
    }
}

Außerdem haben wir die zwei Operatoren „is null“ und „is not null“ eingeführt. So können null-Werte aus der Datenbank gelesen werden.

Tree.count { where { changedAt.isNull() } }

Unsere max und min Funktionen haben wir ebenfalls erweitert. Sie liefern nun auch ein Element vom selektierten Feldtyp zurück. Bspw. liefert max( createdAt) nun einen Timestamp. Auch sie verwenden nun standardisiert das Conditions-Konzept, um das Ergebnis einzuschränken.

Tree.max { createdAt }.nanos

Tree.max({ createdAt }) { where { type eq TreeType.LEAF } }

Des Weiteren haben wir an unserem DataObjectCache gearbeitet. Er verhindert, dass Instanzen des selben Schlüssels mehrfach geladen werden. Wird der Cache verwendet und an unterschiedlichen Stellen auf dasselbe Objekt zugegriffen und verändert, greifen alle Verwender konsistent auf das selbe Objekt zu.

Weiterhin wurde der dbQueryExecutor um eine Logger-Funktion erweitert. Bei Verwendung des Loggers wird jeder ausgeführte Query auf der Konsole ausgegeben.

2022-02-23 Frischer Wind von außen?

Seit Langem gibt es diese Woche etwas Besonderes zu berichten. Über einen Bekannten haben wir von einem Unternehmer erfahren, der Startups unterstützt, in diese investiert, selbst welche gegründet hat und relativ erfolgreich mit seiner Arbeit ist.

Kurzerhand hat er uns am Montag zu sich eingeladen, um mit uns über unser Projekt und wie wir damit live gehen können gesprochen. Nach einer kurzen Kennenlernrunde haben wir über unsere Idee gesprochen. Er kommt eher aus der Wirtschaftsecke und wollte wissen, wie wir damit Geld verdienen wollen. Nachdem wir in unserem Backend Framework derzeit etwas versunken sind, konnten wir ihm diese Frage gar nicht komplett beantworten. Er selbst hatte sogar vorgeschlagen Klinken zu putzen, um Werbung zu machen. Meinte aber auch wir müssten kurz- bzw. mittelfristig einen Teil der Entwicklung auslagern, um profitabel zu werden. Das würde allerdings bedeuten, wir würden unser Hobby, das der Grund für unser Projekt ist, an den Nagel hängen und nur noch Konzepte für ausländische Entwickler erstellen. Die Idee fand er okay, aber war sich nicht sicher, ob es schon einige Mitbewerber am Markt gibt. Die hatten wir zwar in der Vergangenheit versucht auszuloten, aber auf die von ihm genannten sind wir nicht gekommen. D.h. unsere Aufgabe ist es noch einmal herauszufinden, welche Konkurrenten es gibt, zumindest einen lauffähigen Prototyp zu erstellen, um mit der App live zu gehen.

An sich ein sehr interessanter Termin. Besonders in der Hinsicht neuen Wind von außen zu bekommen. Über den Tellerrand hinauszuschauen. Uns ist bewusst geworden, wie lange wir schon an der Team Slimster App arbeiten. Ein großer Teil der Zeit floss in Forschung und Ausbildung. Für uns ein wichtiger Teil, den wir auf keinen Fall missen wollen. Andererseits lechzen wir danach eine App live gehen zu sehen, zumindest einen Euro damit zu verdienen.

2022-02-15 Dies und das

Diese Woche in Unterbesetzung aber trotzdem fleißig. Die DataAccessObject-Schnittstelle hat ein neues, vereinheitlichtes Format, bei dem jetzt hoffentlich klar ist, was an Eingrenzungsmöglichkeiten, wie beispielsweise where-Bedingungen, und Funktionalitäten, wie z.B. Sortierung, zur Verfügung stehen. Methoden wie „count“, „delete“ und „select“ erwarten nun immer ein Where, welches mittels WhereBuilder zusammengesammelt wird. „find“ und „min“/“max“ können ebenfalls über eigene Builder versorgt werden, inkludieren aber ebenfalls einen WhereBuilder. Somit ist der Zugriff vereinheitlicht und wir werden uns nächste Woche nochmal anschauen, ob es wirklich eine Vereinfachung ist.

Ich habe mich auch einem Unittest gewidmet, der schon länger fehlgeschlagen ist, ich aber nicht herausgefunden habe, weshalb Mockk beim gemockten Objekt nicht das korrekte Ergebnis zurückgeliefert hat. Letztendlich ist mir aufgefallen, dass die Methode, die ich mocken wollte, einen varargs-Parameter enthält. Obwohl ein einzelner Wert valide ist, muss hier ein Array übergeben werden:

//Konstruktor:
Reflections(prefix: String, vararg scanners: Scanner)

//Es muss mit Array gemockt werden:
mockkConstructor(Reflections::class)
every {
	constructedWith<Reflections>(
		EqMatcher(packagePath),
		OfTypeMatcher<Array<Scanner>>(Array<Scanner>::class)
	).getSubTypesOf(IService::class.java)
} returns resultSet

Nebenbei bin ich noch über eine Performance-Verbesserung für die Dependency-Beschaffung gestolpert. Da die Repositories in der Reihenfolge, in der sie im gradle-File definiert werden, nach den Dependencies befragt werden, gab es bei uns 151 Aufrufe, die mit „Resource missing“ zurück kamen. Wir hatten bei uns unser Github-Repository vor Maven eingebunden. Dies haben wir mittels

./gradlew build –refresh-dependencies –info

ermittelt. Somit kann die beste Reihenfolge, je nach eingebundenen Repositores, gefunden werden. Jetzt sind wir lediglich bei 8 extra Aufrufen, dadaurch, dass wir auf folgende Reihenfolge umgestellt haben:

repositories {
        mavenCentral()
        google()
        //unser Repository
        RepositoryManager(rootProject).addConsumeRepository(this)
    }

2022-02-08 Bergauf aus der Messy Middle

Es fühlt sich diese Woche langsam wieder so an, als ob wir wieder etwas mehr Fahrt gewinnen. Das ORM-Framework nimmt immer mehr Gestalt an, mit unserem Deployment-Setup lässt sich uns unsere neue Core-Library einbinden und bald können wir uns dem eigentlichen Problem vom Juni widmen, mit dem alles begann, den Relationen.

Einen Nachmittag haben wir uns Zeit genommen uns zu überlegen, wie wir unsere DataAccessObject-Schnittstelle für alle Funktionsaufrufe vereinheitlichen können. Nach längerem Herumprobieren sind wir bisher zu folgendem Ergebnis gekommen:

Tree.find {
    fields { id, name }
    where {
        or(
            and(
                id eq 12,
                name eq "Tanne"
            ),
            id eq 24
        )
    }
    asc { name }
    desc { id }
    limit(123, 10)
}

Die meisten Deklarationen waren bereits aus früheren Implementierung vorhanden. Die längste Zeit ist in die Where-Bedingung geflossen. Hier sind wir uns auch noch nicht hunderprozentig sicher und probieren noch weitere Schreibweisen aus.

Zum ersten Mal sind auch Integrationstests entstanden, in denen wir in Transaktionen Funktionen auf den DataObjects aufrufen und somit nun einen durchgängigen Test durch die meisten Schichten haben. Lediglich die DataSource haben wir gemockt. Hier gab es noch ein kleines Problem. Die DataSource ist auf dem DataAccessObject zu finden , welches das CompanionObject in unseren DataObjects ist. Daher war sie für die Tests nur statisch verfügbar und konnte nicht für jeden Test separat gemockt werden. Um dieses Problem zu umgehen, haben wir einen DataSourceInjector implementiert, der eine DataSource ins DataAccessObject injecten kann. Da dieses Prinzip nur für Testzwecke genutzt werden soll, haben wir durch einen InjectorVerifier sichergestellt, dass nur injected werden kann, wenn ein gültiges Passwort mitgegeben wird. Dieses haben wir nur in unserer Testumgebung hinterlegt und prüfen im Produktivcoding gegen den gehashten Wert.

Letzte Woche waren wir noch auf der Suche nach einer Möglichkeit, transitive Dependencies, die in unserer Core-Library verwendet werden, auch in den verwendenden Libraries der Core-Library nutzbar zu machen. Dies ist uns mittels der Einbindung der Dependencies via „api“ statt „implementation“ gelungen. Wir sind also an einigen Stellen ein Stückchen weiter gekommen.

2022-02-01 DProperties, Dummyobjekte, Deployment

Attribute Properties werden zu DProperties

Refactoring, Refactoring, Refactoring. AttributeProperties werden zu DProperties. Unsere DataObjects basieren auf der Basisklasse DataObject, die Funktionen bereitstellt, um Delegates für Strings, Int, Float und alle anderen unterstützten Typen zu erzeugen. Die Delegates basieren auf unserer Klasse AttributeProperty. Im Framework greifen wir aber auch häufig auf die Type-Properties der Reflection-Klassen zu, die ebenfalls Properties heißen. Unsere AttributeProperties haben wir an vielen Stellen einer Variablen „property“ zugewiesen. Um diesem Mischmasch und der Unlesbarkeit Einhalt zu gebieten heißen alle AttributeProperties nun DProperties (für Delegate Properties) und die Type-Properties sind KProperties (da sie auf der Klasse KMutableProperty basieren).

Dummyobjekte statt Dummyobjekte 😛

Für Abfragen wie Tree.find{ id = 24 } haben wir bisher konkrete Instanzen von Datenobjekte erzeugt, um einen typesafen Zugriff auf die Attribute in unserer Selector-Funktion { id = 24 } zu ermöglichen. Auch wenn wir dazu nur ein DummyObjekt verwenden, das nur einmal erzeugt und immer wieder verwendet wird, wird eine konkrete Instanz des DataObjects erzeugt. Es ist unschön und kann früher oder später zu Problemen führen.
Um das Problem zu lösen, werden nun richtige Dummyobjekte in Form von Mocks erzeugt. Das Mock sieht aus wie das DataObject, aber es ist eine Instanz einer komplett anderen Klasse. Dafür nutzen wir Objenesis. Ein kleines Framework, das auch von mockk eingesetzt wird.

Reload works

Unser Reload funktioniert nun. Der Reload eines einzelnen DataObjects funktionierte bereits. Nur beim Massenreload hatten wir Probleme, da die Reihenfolge der zu reloadenden Objekte nicht mit dem Ergebnis der Datenbank übereinstimmte. Wurden nun die Properties der DataObjects geupdated, hat das DataObject 1 möglicherweise die Attribute vom DataObject 5 bekommen.
Um das Problem zu lösen, sortieren wir die zu reloadenden DataObjects, bevor wir die Daten des Reloads von der Datenbank auslesen.

Core Library

Jetzt geht es nochmal ans Deployment. Wir würden gerne eine Core-Library anlegen, die wir in alle Projekte einbinden, um gewisse Funktionalitäten wie z.B. zentrale Extensions auf Strings, Booleans etc. oder den ServiceProvider und zukünftige Dependencies, die zentral verfügbar sein sollen, anzubieten. Dazu muss das Deployment angepasst werden, weil wir bis jetzt keine transitiven Dependencies in dem Projekt, welches die Dependency einbindet, zur Verfügung stellen können. Hier bedarf es wieder etwas mehr Forschungsarbeit.