5
.
2
.
2019

App Entwicklung für Atlassian JIRA Server mit Active Objects

Dockerisierte Integration Tests mit Testdaten

Einleitung

Aktuell entwickle ich eine App für Atlassian JIRA Software, welche auch Data Center Compatible sein soll. Das bedeutet kurz gesagt den Zustand der Anwendung aus der Applikationsschicht in die Datenbankschicht zu verlagern, ClusterLocks einzusetzen und vieles mehr. Die Apps sollen dadurch in einer Cluster Umgebung (mehrere JIRA Instanzen) optimal skalieren. Damit man Data Center Compatible wird gibt Atlassian eine Checkliste heraus.

Dieser BlogPost gibt weiterführende Informationen zu Active Objects - dem Objekt Relationalen Mapper (ORM) von Atlassian - und setzt Grundlagen in diesem Gebiet voraus.

Am besten folgendes Tutorial durchgehen, dort werden die Basics von ActiveObjects erklärt:

ActiveObjects - Kurzeinleitung

Mit ActiveObjects gibt Atlassian eine Bibliothek heraus, die es einer App im OSGI Context zugriff auf die Datenbank ermöglicht.

Hier meine eigene Pro/Contra Liste zu ActiveObjects:

PRO:

  • Isolation - App sieht nur eigene Tabellen - Keine Tabellen von anderen Apps
  • ORM - Object Relationales Mapping ala JPA/Hibernate in reduzierter Form.
  • Echte Datenbanken - Unabhängig von der eingesetzten Datenbank lassen sich Abfragen definieren
  • Streaming API - gut für große Datensätze (fast batch queries - read only objects)
  • INNER und LEFT Joins - Left und Inner Joins bequem möglich

CONTRA:

  • Keine Subselects - Muss auf API Ebene bspw. durch Stream API gelöst werden
  • Kein Compound Unique Index - Es kann nur eine Column Unique sein. Unique Index über mehrere Spalten nicht unterstützt.
  • Keine Möglichkeit Joins zu beeinflussen - nur zwei Möglichkeiten LEFT, INNER
  • Komische DB spezifische Eigenheiten - bspw. manuelles Column-Quoting für PostgreSQL nötig

Unterstützte Datenbanken bei JIRA sind Stand heute:

  • MySQL
  • PostgreSQL
  • Microsoft SQL Server
  • Oracle

Obwohl von der spezifischen Datenbankengine abstrahiert wird, so muss man dennoch auf deren Eigenheiten achten. Hierzu gehört bspw. Column-Quoting bei PostgreSQL und das Case-Sensitive-Like-Query Problem auf das wir später eingehen.

Im Folgenden werde ich kurze isolierte Kapitel zu Best Practices bereitstellen. Dabei sind diese Best Practices teilweise von mir erdacht und nach bisherigem Kenntnisstand so zu empfehlen.

BestPractice #1: Keep Tablenames short (max. 30)

Tabellennamen dürfen maximal 30 Zeichen lang sein. Dabei muss man aber den Vendor-Prefix e.g. AO_AEF786 und die Translation mit Unterstrichen mitrechnen.

So wird aus ‘LogEvent’ bspw. ‘AO_AEF786_LOG_EVENT’ was bereits 19 Zeichen sind. Sind die Tabellennamen zu lang, fährt die Anwendung nicht hoch und wirft Exceptions.

BestPractice #2: Sub-Selects mit WHERE IN (…)

Wir haben in der Pro/Contra Liste bereits gesehen, dass es keine ‘nativen’ Sub-Selects gibt. Daher bedienen wir uns für wenig Daten den ‘WHERE IN’-Statements. Das native SQL Statement, welches wir uns wünschen ist folgendes.

github:b7f5098cc28a441c724a7eabccba8cbb

Dabei ist es leider nicht möglich einen Array/Liste von Werten an ActiveObjects zu übergeben, sondern man muss selbst die ‘Fragezeichen’ erzeugen, die in ihrer Anzahl zu den WHERE Parametern passen müssen.

Daher ist folgendes Pattern sehr nützlich:

github:1230a5401449f761f805a41e1dd2f94a

Es wird also sichergestellt, dass für jeden whereParam auch ein Fragezeichen erzeugt wird. Hardcodiert würde der select so aussehen:

github:3071095f93a261dd9e801d0adc9cc3f8

Ist nicht schön, aber funktioniert.

BestPractice #3: CaseInsensitive WHERE LIKE

Gerade im Data-Center Kontext macht es Sinn die Suche in die Datenbank zu verlagern, da man ansonsten den SuchIndex über alle Cluster-Nodes synchronisieren müsste. Auch ist bei JIRA Data-Center Installationen das Datenvolumen sehr hoch, was einen In-Memory-Index gefährlich groß machen könnte.

Daher wollen wir mit ‘SELECT WHERE FOO LIKE “%bar%”’ eine Suche in der Datenbank vornehmen. Vorbedingungen/Empfehlungen sind dabei:

  • @Index auf der Spalte
  • DatenTyp VARCHAR(255)

Das Problem ist dabei, dass die LIKE Queries bspw. bei PostgreSQL case-sensitive sind. Sprich eine Suche nach ‘Foo’ liefert andere Ergebnisse als ‘foo’.

Will man also eine case-insensitive Suche, dann muss man Lower-Case Shadow Spalten anlegen, mit lower-case Werten befüllen und auf diesen Spalten mit LIKE suchen. Die Entity mit Index und Shadow-Spalten sieht dabei bspw. so aus:

github:6b0fa1949a7c7873db9f6f4ac7700a46

Speichern und Suchen sieht so aus:

github:e0099e7f339351da011681919be2119e

Ist nicht schön, aber funktioniert.

BestPractice #4 - WHERE FOO IS NULL

Will man Datensätze selektieren, welche ein NULL Feld haben, dann geht das leider nicht auf die übliche weise, sondern man muss das wie folgt tun.

github:19a2d0fdc76b5125317472dd8122bd4e

BestPractice #5 - ORDER BY QUOTING

Trotz des Einsatzes eines ORM muss mann dennoch auf gewisse Datenbanktypen Rücksicht nehmen. Bei PostgreSQL muss man bspw. in den ORDER BY teilen einer Abfrage die Feldnamen manuell quoten.

github:cac6806a99c60612491faa0242541e1f

BestPractice #6 - String und Datentyp VARCHAR(255)

Will man auf seinen Textspalten einen Index legen so empfiehlt so wird man bei VARCHAR(255) enden. Man muss jedoch selbst die Strings vor dem persistieren kürzen, sonst fliegt eine Exception. Die stripToMax255Chars Methode wurde bereits in BestPractice#3 gezeigt.

Will man aber längere Strings persistieren, dann muss man die Unlimited-Annotation auf dem Setter verwenden, kann dann aber keinen Index auf dieses Feld legen.

github:6807ae44c8bdf8bf37d88b15736abb1d

BestPractice #7 - Lazy vs. Eager

Wie der Guide ‘Finding Entities’ schon sagt, ist find() generell Lazy. Wollen wir eager fetching, dann können wir die @Preload() Annotation auf dem Entity setzen. Man sollte sich die generierten SQL Statements ansehen und entsprechend optimieren.

Will man bspw. pagination nutzen und es reicht einem ReadOnlyEntityProxys zu erhalten, sollte man auf die Streaming-API setzen.

BestPractice #8 - Joins

Man kann mit ActiveObjects auf zwei Wegen einen Join erzeugen.

  • Variante 1: Left join via @OneToMany Annotationen.
  • Variante 2: Inner join via query.join() Abfrage.

Je nach Anwendungsfall mag es sinnvoll sein statt dem Left Join mal einen Inner Join zu verwenden. Ich habe bspw. in meiner App den Fall, dass ich alle Customfields haben will, die entsprechende Permissions haben. Hier ist ein Inner Join ideal, da ich wirklich nur die Customfields bekomme, die auch Permissions haben. Bei einem Left Join müsste ich in Memory noch die Customfields herausfiltern, die keine Permissions haben. Da ich die Pagination in die Datenbank verlagern will, ist das der Richtige Ansatz.

BestPractice #9 - Migration Tasks

Gerade zu diesem Thema steht wirklich viel Mist im Netz. Man muss einiges über den Aufbau von JIRA verstehen, bevor man hier eine sinnvolle Lösung bekommt.

Als Entwickler will ich sogenannte Migration Tasks laufen lassen, welche meine Datenbasis nach einem App-Update ändert. Bspw. Daten von Tabelle1 nach Tabelle2 schaufeln. In einer reinen Spring Data JPA Application würde ich evtl. einfach einen Service mittels @PostConstruct auf die Geschichte loslassen.

In JIRA ist aber das Problem, dass zum Zeitpunkt an dem ein PostConstruct ausgeführt wird die Persistenzschicht (ActiveObjects) noch nicht geladen ist. Es folgen NullPointerExceptions.

Daher ist mein Ansatz ein EventListener auf das ‘OnPluginEnabled Event’, denn sobald dieses Event getriggert wird, weiß ich, dass die Persistenzschicht bereit ist. Im Listener setze ich einen Cluster-Lock (nur für JIRA DataCenter) und starte die Migration Tasks.

Das sieht in etwa wie folgt aus.

github:b2b79ac278a632dae5b43b7c6765a6fd

BestPractice #10 - Dockerized Database Integration Tests

Wenn wir mit dem Atlassian SDK einfach nur ‘atlas-unit-test’ ausführen, so werden die ActiveObject Integration Tests gegen eine H2 InMemory-Datenbank ausgeführt. Wollen wir unsere Integration Tests gegen eine echte PostgreSQL oder MySQL Datenbank ausführen, so können wir das wie folgt.

github:d5071ae48d7e5fdabaf68f1ad1005722

Im obigen Beispiel starten wir mit Docker eine PostgreSQL Datenbank und geben dem ‘atlas-unit-test’ Befehl die entsprechenden Parameter mit, um gegen diese Datenbank zu testen.

Das komplette Setup sieht man auf GitHub:

Fazit

Unit Test macht wenig Sinn bei einem DataAccessService. Man will direkt einen Integration Test, der auf echten Testdaten gegen verschiedene kompatible Datenbanken testet. Der DataAccessService muss dabei die Integrität der Daten schützen und muss der einzige Weg sein, um auf die Datenbank zuzugreifen. Die höheren Schichten arbeiten nicht mit Active Objects sondern gegen die DataAccessService API. Der Einsatz von Lazy/Eager Fetching, Pagination und Streaming-API sind dabei wichtig für die Performance bei Atlassian JIRA DataCenter und sehr großen Datenbeständen.

Bernhard
Software Engineer - Java, JVM

Leidenschaft, Freundschaft, Ehrlichkeit, Neugier. Du fühlst Dich angesprochen? Dann brauchen wir genau Dich.

Bewirb dich jetzt!