Widersprüchliche Dependencies

Wenn man größere Applikationen entwickelt kommt man irgendwann einmal an den Punkt, wo verschiedene Libraries, die man gerne verwenden möchte, ihrerseits von anderen Libraries abhängen. Da kommt dann auch schonmal dieselbe Library mehrfach vor, nur leider mit unterschiedlichen Versionen. Man probiert dann immer gerne, ob es nicht eine Version gibt, mit der beide funktionieren und manchmal hat man Glück, bis dann eine dritte kommt. Im Prinzip ist es möglich, auch so etwas zu lösen, aber es ist schon konzeptionell nicht so einfach und in der Praxis auch etwas mühsam, wenn es überhaupt gelingt. Zum Glück ist es in vielen Fällen möglich, einfach die neueste der verlangten Versionen zu nehmen, aber manchmal geht das nicht, sei es, weil die Version wirklich überprüft wird und eine bestimmte verlangt wird oder sei es, weil diese beiden Versionen wirklich an einer Stelle inkompatibel sind, die dummerweise noch verwendet wird.

In Java gibt es dafür einen Ansatz, eigene ClassLoader zu verwenden. Hier ein kleines Beispiel, in dem eine Klasse X drei Objekte aus verschiedene Klassen Y instanziiert. Diese Klassen Y sind alle im selben Package, aber in verschiedenen Jars.

1. X.java

import java.net.URL;
import java.net.URLClassLoader;

public class X {
    public static void main(String args[]) {
        try {
            ClassLoader c0 = Thread.currentThread().getContextClassLoader();

            URL cpa = new URL("file:///home/bk1/src/class-loader/A/A.jar");
            URL cpb = new URL("file:///home/bk1/src/class-loader/B/B.jar");
            URL cpc = new URL("file:///home/bk1/src/class-loader/C/C.jar");
        
            ClassLoader ca = new URLClassLoader(new URL[]{ cpa }, c0);
            ClassLoader cb = new URLClassLoader(new URL[]{ cpb }, c0);        
            ClassLoader cc = new URLClassLoader(new URL[]{ cpc }, c0);
            
            Object a = ca.loadClass("Y").newInstance();
            System.out.println("a=" + a + " a.class=" + a.getClass());
            Object b = cb.loadClass("Y").newInstance();
            System.out.println("b=" + b + " b.class=" + b.getClass());
            Object c = cc.loadClass("Y").newInstance();
            System.out.println("c=" + c + " c.class=" + c.getClass());
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

2. A/Y.java

public class Y {
    public String toString() {
        return "Feuer";
    }
}

Dies landed kompiliert in A.jar

3. B/Y.java

public class Y {
    public String toString() {
        return "Wasser";
    }
}

Dies landed kompiliert in B.jar

4. C/Y.java

public class Y {
    public String toString() {
        return "Erde";
    }
}

Dies landed kompiliert in C.jar

Und das ist die Ausgabe:

a=Feuer a.class=class Y
b=Wasser b.class=class Y
c=Erde c.class=class Y

Natürlich sollte man dies bevörzugt Frameworks überlassen, statt es selber zu programmieren. Aber grundsätzlich ist das Problem durchaus lösbar.

Share Button

IT Sky Consulting GmbH web page in English

The web page of IT Sky Consulting GmbH is now also available English.

Share Button

RISC und CISC

Vor 20 Jahren gab es einen starken Trend, Mikroprossoren mit RISC-Technologie (reduced instruction set computer), zu bauen. Jeder größere Hersteller wollte so etwas bauen, Sun mit Sparc, Silicon Graphics mit MIPS, HP mit PA-Risc, IBM schon sehr früh mit RS6000, was später, als man auf die Idee kam, das zu vermarkten, als PowerPC rebranded wurde, DEC mit Alpha u.s.w. Zu einer Zeit, als man zumindest in Gedanken noch für möglich hielt Assembler zu programmieren (auch wenn man es kaum noch tat), tat das noch richtig weh. Und Software war doch sehr CPU-abhängig, weil plattformunabhägige Sprache, die es damals selbstverständlich schon lange vor Java gab, einfach wegen des Overheads der Interpretation für viele Zwecke zu langsam waren. So behalf man sich mit C-Programmen mit wahren Orgien an ifdfefs und konnte die mit etwas Glück für die jeweilige Plattform aus CPU und einem UNIX-Derivat kompilieren. Der Wechsel der CPU-Architektur eines Herstellers war damals eine große Sache, z.B. bei Sun von Motorola 680×0 zu Sparc. Und die Assemblerprogrammierung der RISC-CPUs war ein Albtraum, an den sich auch erfahrene Assemblerprogrammierer kaum herangewagt haben. Zum Glück gab es damals schon sehr gut optimierende Compiler für C und Fortran und so konnte man das Thema einem ganz kleinen Personenkreis für die Entwicklung von kleinen Teilen des Betriebssytemkerns und kleinen hochperformanten Teilen großer Libraries überlassen.

Eigentlich sollte RISC ermöglichen, mit derselben Menge an Silizium mehr Rechenleistung zu erzielen, insgesamt also Geld und vielleicht sogar Strom zu sparen. Vor allem wollte man für die richtig coole Server-Applikation, die leider immer etwas zu ressourchenhungrig für reale Hardware war, endlich die richtige Maschine kaufen können. RISC war der richtige Weg, denn so hat man einen Optmierungsschritt beim Compiler, der alles optimal auf die Maschinensprache abbildet, die optimiert dafür ist, schnell zu laufen. Ich wüsste nicht, was daran falsch ist, wenn auch diese Theorie vorübergehend nicht zum Zuge kam. Intel konnte das Problem mit so viel Geld bewerfen, dass sie trotz der ungünstigeren CISC-Architektur immer noch schneller waren. Natürlich wurde dann intern RISC benutzt und das irgendwie transparent zur Laufzeit übersetzt, statt zur Compilezeit, wie es eigentlich besser wäre. Tatsache ist aber, dass die CISC-CPUs von Intel, AMD und Co. letztlich den RISC-CPUs überlegen waren und so haben sie sich weitgehend durchgesetzt.

Dabei hat sich die CPU-Abhängigkeit inzwischen stark abgemildert. Man braucht heute kaum noch Assembler. Die Plattformen haben sich zumindest auf OS-Ebene zwischen den Unix-Varianten angeglichen, so dass C-Programme leichter überall kompilierbar sind als vor 20 Jahren, mit cygwin sogar oft unter MS-Windows, wenn man darauf Wert legt. Applikationsentwicklung findet heute tatsächlich weitgehend mit Programmiersprachen wie Java, C#, Scala, F#, Ruby, Perl, Python u.ä. statt, die plattformunabhängig sind, mittels Mono sogar F# und C#. Und ein Wechsel der CPU-Architektur für eine Hardware-Hersteller ist heute keine große Sache mehr, wie man beim Wechsel eines Herstellers von PowerPC zur Intel-Architektur sehen konnte. Man kann sogar mit Linux dasselbe Betriebssystem auf einer unglaublichen Vielfalt von Hardware haben. Die große Mehrheit der schnellsten Supercomputer, die allermeisten neu vekrauften Smartphones und alle möglichen CPU-Architekturen laufen mit demselben Betriebssystemkern, kompiliert für die jeweilige Hardware. Sogar Microsoft scheint langsam fähig zu sein, verschiedene CPU-Architekturen gleichzeitig zu unterstützen, was lange Zeit außerhalb der Fähigkeiten dieser Firma zu liegen schien, wie man an NT für Alpha sehen konnte, was ohne große finanzielle Zuwendungen seitens DEC nicht aufrechterhalten werden konnte.

Aber nun, in einer Zeit, in der die CPU-Architektur eigentlich keine Rolle mehr spielen sollte, scheint alles auf Intel zu setzen, zum Glück nicht nur auf Intel, sondern auch auf einige konkurrierende Anbieter derselben Architektur wie z.B. AMD. Ein genauerer Blick zeigt aber, das RISC nicht tot ist, sonder klammheimlich als ARM-CPU seinen Siegeszug feiert. Mobiltelefone habe häufig ARM-CPUs, wobei das aus den oben genannten Gründen heute fast niemanden interessiert, weil die Apps ja darauf laufen. Tablet-Computer und Netbooks und Laptops sieht man auch vermehrt mit ARM-CPUs. Der Vorteil der RISC-Architektur manifestiert sich dort nicht in höherer Rechenleistung, sondern in niedrigerem Stromverbrauch.

Ist die Zeit reif und CISC wird in den nächsten zehn Jahren wirklich durch RISC verdrängt?

Oder bleibt RISC in der Nische der portablen stromsparendenen Geräte stark, während CISC auf Server und leistungsfähigen Arbeitsplatzrechnern dominierend bleibt? Wir werden es sehen. Ich denke, dass früher oder später der Vorteil der RISC-Architektur an Relevanz auf leistungsfähigen Servern und Arbeitsplatzrechnern gewinnen wird, weil die Möglichkeiten der Leistungssteigerung von CPUs durch mehr elektronischen Elementen pro Quadratmeter Chipfläche und pro Chip an physikalische Grenzen stoßen werden. Dann die bestmögliche Hardwarearchitektur mit guten Compilern zu kombinieren scheint ein vielversprechender Ansatz.

Die weniger technisch interessierten Nutzer wird diese Entwicklung aber kaum tangieren, denn wie Mobiltelefone mit Android werden Arbeitsplatzrechner mit welcher CPU-Architektur auch immer funktionieren und die Applikationen ausführen, die man dort installiert. Ob das nun plattformunabhängig ist, ob man bei kompilierten Programmen ein Binärformat hat, das mehrere CPUs unterstützt, indem einfach mehrere Varianten enthalten sind oder ob der Installer das richtige installiert, interessiert die meisten Benutzer nicht, solange es funktioniert.

Share Button

Databases and Immutable Objects

Deutsch

A common approach in functional programming and even a good practice in object oriented programming is to prefer having objects immutable.

Especially in applications with multithreading this is extremely helpful, but in principal it helps having the information flow under control and avoiding unexpected side effects. I do not want to postulate this as a dogma, since there are actually legitimate uses of mutable objects, but it is a good idea to keep in mind what mutability means and implies where it is used and to question if it is really worth it in the particular case.

It is important to differentiate between really immutable objects and those that are handed out via a wrapper or a second interface in an immutable way while still being accessible for changes by those parts of the code that control the mutable interface.

Example:

import java.util.*;

public class A {
    List l = new ArrayList();
    
    public void addStr(String s) {
        l.add(s);
    }

    public List getList() {
        return Collections.unmodifiableList(l);
    }
}

Even though the list provided by getList() is by itself immutable, it can still be changed by the addStr(..)-method of A.

The whole story gets more interesting when involving databases.

Database contents are in some way mapped to objects. If this mapping is done via automatisms like OR-mappers or Active-Record or if it is done explicitely by using mechanisms like JDBC or DBI is secondary for the moment. These objects can be based on specifically written code for the particular usage or generic DB-content-objects or just plain old collections, which should be considered as an implementation detail for the moment. Important is that such objects or data structures expressing DB content exist in the software. For simplicity’s sake they will be referred to as objects throughout this text.

Now DB content can change. This can happen due to activities of the software with which we are dealing right now. But there can also be accesses to the database by other software or by an DB administrator which can cause changes in the database content while the software is running. This already results in some complications, because the objects processed by the software are already outdated while being processed. This would be acceptable if there were no caching mechanisms for database content on software and framework level, leaving the caching to the database software. Other than framework and software caches the caches of the database itself can be truly transaction conform. Off course it is good having the DB in terms of network connectivity not too far away from the application, maybe even on the same machine, depending on the access patterns. If the network connectivity is poor, this approach ends up having the cache on the wrong side of the network connection.

If the objects within the software have a life time that is so short that it can be avoided that they are out of sync with the database contents they are representing, things should be fine. This can be achieved by using the transaction isolation of „serializable“ or „phantom-read“ and by discarding all these objects before the end of the transaction. Within the transaction these objects that have been read during the same transaction are guaranteed to be up to date, as a result of the concept of transaction isolation used here. If certain OR-mapping patterns needed to map collections to database contents are not involved it is possible that „repeatable-read“ is already enough to guarantee this. As long as objects are only read from the database (SELECT or READ or FIND) immutable objects work just fine.

Even deleting (DELETE) can be imagined, but in this case it is important to ensure that the corresponding object is not in use in other parts of the software even though the database content represented by it have already been deleted. When ensuring that objects representing database content do not live longer than the corresponding transaction this should be possible to deal with. Also creating new objects (INSERT or CREATE) and the corresponding database content should be possible.

What is most problematic is changing of data (UPDATE). Common OR mapper do this by reading the object, changing it and then saving it, hoping that the framework will figure out the differences and run the appropriate update, possibly multiple updates in conjunction with inserts and deletes. This becomes difficult with immutable objects, because that blocks the typical approach just described. Ways can be found to accomplish this anyway, but they tend to get this done at the expense of the elegance of the framework. Even more important it becomes relevant to deal with the situation that objects expire even within the transaction and need to be replaced by new objects representing the changed database contents.

The provocative question is if updates are at all needed. Off course they are needed, since update is one of the four basic database operations. But it is worth taking a closer look in which cases updates really make sense and in which cases other approaches are attractive alternatives. A classic is an application that is dealing with some kind of bookings and some kind of subjects to which accounts are attached, typically this can be persons or companies or other entities, but we can go more abstract than that. For simplicity we can assume persons as an example. This can be a banking software with accounts, account owners and whatever is needed on top of that. Or a billing system of a phone company that calculates, creates and manages invoices to customers based on their phone usage and subscriptions. It is commonly seen that there is such a booking table to which records can be added, but never deleted or changed. That would falsify the book keeping. For canceling a booking, a cancel-entry can be added which is actually a booking in the reverse direction marked as cancel for the original booking. Maybe there is an archiving process that moves old data to a data warehouse, thus actually performing a delete in the booking table and replacing the old bookings by some summary entry, but that is really a detail not relevant for this article as long as this archiving process is done in a good way not compromising the regular usage of the software and database, maybe by actually taking the system down for maintenance during this archiving process once a year. Usually some kind of balance is needed quite often. In principal this can easily be calculated any time by just adding up the bookings of that particular account. This is in line with normalization requirements. Unfortunately this will slow down software to an extent that it is not useful for any practical purposes, but in theory this approach is extremely beautiful. So a more efficient way of finding balances is needed. Possibly each booking could contain the resulting balance reducing the task to looking for the newest booking of a particular account. That can be dangerous if two bookings generated simultaneously refer to the same previous balance. With transaction isolation of „serializable“ that would not happen, but this might slow down the database quite a bit because it reduces the performance of access to the largest table and thus worsening the bottle neck that might already exist there. It could be solved on database level for example with triggers or materialized views so that each insert to the booking table influences the balance automatically. This should work find for all kinds of accesses to the system by any software as long as it is constrained from doing any delete and update to the booking table. Actually granting only INSERT and SELECT rights to the booking table for the user under which the software is running is a very good idea. It could be seen as a disadvantage that part of the business logic is moved to the database. Maybe this is acceptable by just understanding the balance being the sum of the bookings of the account as some kind of high level constraint which is actively enforced by the database. Another approach would be to provide a functionality in the software that inserts a booking or more likely a set of bookings comprising a transaction and that always keeps the balance updated. This can go wrong if simultaneous bookings for the same account can happen, meaning that a transaction for a second booking start while the transaction of a first booking of the same account has not completed. This seems to be simple to avoid by just providing some queue for each account and working through the queue, but since the transactions usually include bookings on several different account this can become quite messy to implement while still avoiding deadlocks. It must be quite interesting to build such a system in a way that it will work fine even under high load. In any case objects containing the balance of an account tend to age extremely fast so they have to be handled with care as short lived objects within a transaction or even shorter, if that account is touched by bookings to that account in the same tranaction.

On the other hand the table with persons and addresses is commonly kept up to date using updates whenever data changes. People marry, change their phone number or move to another address. But is this really the best approach? In reality moving to another address could be known in advance and maybe it is not a good requirement that this change of address has to be entered on the exact date when the person actually moves. Or who wants to spend his wedding day updating all kind of name and address fields in all kinds of web applications? Ok, this can be cheated on because it is usually not a big deal if the change is done a little bit too late. But for some purposes exact data are really desirable. And then again, is this really the right approach? Sometimes it is required to be able to answer questions like name and address of the customer while booking 23571113a was being performed. Maybe it is a good idea to store address changes as some kind of bookings as well, with a valid-since field. For finding a customer’s current address it is sufficient to look for the newest entry for that particular customer that is not in the future. With this approach new entries need to be created for these changes and they can even be prepared in advance. Even deleting a customer can be accomplished by added a „deleted“-entry with a particular valid-since timestamp to that table.

The beatiful part is that now certain data in the database can be considered immutable since no updates can occur to them. So dealing with them as immutable in the software becomes a valable approach and actually also a must. These objects can even be kept in the system longer than the duration of a transaction. It remains important to be careful and to understand the OR-mapping since complex objects represeting collections can even change due to inserts. An example would be an account object containing the account and all its bookings. Customer address needs to be dealt with in conjunction with the timestamp that was used for acquiring it. So it is a good practice to freeze the „now“-timestamp at the beginning of a large operation and to consistently use it throughout that operation to have a consistent view of the data. If this timestamp gets too old, it might be a good idea to refresh it and to reread all data attached to the timestamp. Other than for bookings the times of address changes usually need not be accurate to the micro second as long as consistency is guaranteed and no weird mixture of old and new addresses ever occurs.

Share Button

Datenbanken und unveränderliche Objekte

English

Ein beliebter Ansatz in der funktionalen Programmierung, aber auch teilweise in der objektorientierten Programmierung ist es, Objekte nach Möglichkeit unveränderlich (engl. „immutable“) zu machen.

Speziell für Applikationen mit Multithreading ist das sehr nützlich, aber grundsätzlich erleichtert es auch im Griff zu haben, wie der Informationsfluss ist und unerwartete Seiteneffekte zu verhindern. Ich will das hier nicht als Dogma postulieren, denn es gibt durchaus legitime Verwendungen von veränderlichen Objekten, aber man sollte sich auf jeden Fall bewusst sein, was das bedeutet und ob sich die Veränderbarkeit (engl. „mutability“) in dem Fall überhaupt lohnt.

Unterscheiden muss man noch die echt unveränderlichen Objekte und diejenigen, die nur durch einen Wrapper oder ein zweites Interface unveränderlich weitergegeben werden, obwohl es noch für einen Teil des Codes Zugriff auf ein Interface gibt, das Veränderungen zulässt. Der typische Fall sind zum Beispiel Collections in Java, die in einem Objekt leben und mit Methoden dieses Objekts verändert werden können, aber die nur als immutable gewrappt herausgegeben werden.

Beispiel:

import java.util.*;

public class A {
    List l = new ArrayList();
    
    public void addStr(String s) {
        l.add(s);
    }

    public List getList() {
        return Collections.unmodifiableList(l);
    }
}

Obwohl also die mit getList() herausgegebenen Liste selbst immutable ist, kann sie mittels add aus A noch verändert werden.

Interessant wird es aber nun, wenn man eine Datenbank ins Spiel bringt.
Datenbank-Inhalte werden in irgendeiner Weise auf Objekte abgebildet. Ob das nun mit Automatismen passiert oder explizit mit Mechanismen wie JDBC, DBI o.ä. und ob es spezifische Objekte oder generische universelle DB-Content-Objekte oder Collections sind, spielt keine so große Rolle, es gibt solche Objekte oder Datenstrukturen.

Nun können sich Datenbankinhalte ändern. Das kann durch die Software, auf die wir uns hier beziehen, selbst passieren, aber auch von außen. Allein das ist schon eine gewisse Komplikation, wenn man es genau anschaut. Die Objekte, mit denen sich die Software befasst, sind dann nämlich schon veraltet. Das kann so akzeptabel sein, wenn man darauf verzichtet, auf Software- oder Frameworkebene ein Caching von solchen Objekten aufzubauen und das Caching allein der Datenbank überlässt, die das im Gegensatz zu Frameworks oder anderer Software, die die Datenbank nur nutzt, problemlos transaktionskonform kann. Schön ist es dann natürlich wieder, wenn die DB netzwerkmäßig nicht zu weit von der Applikation weg ist, sonst ist der Cache leider auf der falschen Seite der Netzwerkverbindung…

Wenn also in der Software die Objekte so kurzlebig sind, dass man ausschließen kann, dass sie veralten, dann kann man sagen, dass sie hinreichend mit dem Stand in der Datenbank übereinstimmen. Mit Transaktionsisolation „serializable“ oder „phantom-read“ und dann innerhalb von einer Transaktion sind diese Objekte, die man in derselben Transaktion gelesen hat, garantiert noch aktuell. Wenn nicht bestimmte OR-mapping-Muster für Collections vorkommen, ist das schon bei „repeatable-read“ der Fall. Man lebt also gut mit „immutable“-Objekten, solange diese nur aus der Datenbank gelesen (SELECT oder READ) werden. Auch das Löschen (DELETE) kann man sich noch vorstellen, aber in diesem Fall muss man schon darauf achten, dass das betreffende Objekt nicht noch an zu vielen Stellen der Software in Gebrauch ist, obwohl es in der Datenbank schon gelöscht ist. Wenn man aber sowieso sicherstellt, dass diese Objekte, die Datenbankinhalte darstellen, nicht länger leben als die entsprechenden Transaktionen, dann sollte auch das handhabbar sein. Auch das Erzeugen neuer Objekte und das Anlegen der entsprechenden Daten in der Datenbank (INSERT oder CREATE) ist möglich.

Schwierig wird es aber mit dem Ändern von Daten in der Datenbank (UPDATE). Bei gängigen OR-Mappings funktioniert das so, dass man das entsprechende vorher gelesen Objekt ändert und dann speichert, in der Hoffnung, dass das Framework die Unterschiede herausfindet und ein entsprechendes Update, eventuell auch eine Kombination aus Updates, Inserts und Deletes initiiert. Das wird mit unveränderbaren (immutable) Objekten schwierig. Man kann auch da Wege finden, aber das geht dann leicht auf Kosten der Eleganz des Frameworks und man hat sich auf jeden Fall damit auseinanderzusetzen, dass die Objekte jetzt auch innerhalb der Transaktion veralten und durch solche mit den aktualisierten Inhalten ersetzt werden müssen.

Die provokante Frage ist, ob man das Update überhaupt braucht. Natürlich braucht man das, aber auch hier kann man genauer schauen, welches die Fälle sind, wo das wirklich so ist und wann andere Ansätze besser sind.
Ein klassisches Beispiel ist eine Software, die irgendwelche Buchungen und irgendwelche Personen verwaltet. Das kann eine Bankensoftware sein, wo man Konten hat. Oder ein Billingsystem einer Telefongesellschaft, wo man die Rechnungen für Kunden generiert und verwaltet. Man hat dort oft so eine Buchungstabelle, in der grundsätzlich nur Datensätze eingefügt werden und niemals Daten gelöscht oder verändert werden, wenn wir einmal Fragen der Archivierung sehr alter Daten ausklammern. Dazu hätte man gerne noch einen Kontostand. Den kann man strenggenommen immer berechnen, indem man einfach alle Buchungen für das Konto aufaddiert. Leider ist die Software dann nachher langsam und für keinerlei praktische Zwecke brauchbar, aber theoretisch sehr schön. Man braucht also schon einen effizienten Mechanismus, um Kontostände zu ermitteln. Vielleicht kann jede Buchung den aus ihr resultierenden Kontostand beinhalten und man muss nur nach der neuesten Buchung suchen. Das ist gefährlich, weil zwei etwa gleichzeitig erzeugte Buchungen sich auf denselben Vorgängerkontostand beziehen. Mit Transaction-Isolation „serializable“ wäre das nicht passiert, aber das bremst natürlich die Datenbank schon sehr aus, ausgerechnet bei der größten Tabelle, in der die ganze Arbeit läuft und wo es sowieso schon den Performance-Flaschenhals gibt. Man kann auch auf Datenbankebene eine Lösung implementieren, etwa mit Triggern oder Materialized Views, wo jedes Insert in der Buchungstabelle ein Update des entsprechenden Kontostands bewirkt. Das hat den Vorteil, dass beliebige DB-Zugriffe von beliebiger Software richtig verarbeitet werden, solange man in der betreffenden Buchungstabelle Delete- und Update-Zugriffe unterbindet. Der Nachteil ist aber, dass die Businesslogik sich jetzt auf die Datenbank und die Applikation verteilt. Das kann man akzeptieren, wenn man das als eine Art abstrakten Constraint ansieht, der von der Datenbank „aktiv“ eingehalten wird. Oder man kann es vermeiden, indem man auf Applikationsebene eine entsprechend Funktionalität zum Einfügen einer Buchung einführt, die den Kontostand anpasst. Wiederum wird das zu falschen Ergebnissen führen, wenn man erlaubt, dass eine zweite Buchung für dasselbe Konto eingefügt wird, während schon eine Transaktion für das Einfügen einer Buchung für das Konto läuft. Man sieht also, dass die richtige Lösung dieser Buchungstabelle nicht einfach ist. Wenn dann noch hinzukommt, dass die Buchungen wiederum zu Transaktionen gruppiert sind, also z.B. die Gutschrift auf einem Konto in derselben Transaktion wie die Abbuchung von einem anderen Konto laufen soll, dann wird es schon interessant, wie man das System so baut, dass es absolut zuverlässig und korrekt ist, auch unter Last, und doch auch performant ist. Und in der Applikation muss man nun mit Konto-Objekten, die den Kontostand enthalten, sehr vorsichtig sein, weil dieser Kontostand veraltet, sobald eine Buchung dazukommt.

Die Tabelle oder die Tabellen mit den Personen, Adressen werden gerne mit Updates aktualisiert, wenn sich Daten ändern. Leute heiraten, ändern die Telefonnummer oder ziehen um. Aber ist das wirklich der richtige Weg? In Wirklichkeit weiß man von dem Umzug schon etwas vorher und hat vielleicht am Umzugstag selbst keine Zeit, das zu erfassen. Und in Wirklichkeit muss das System auch Fragen beantworten können, wie den Namen und die Adresse des Kunden zu der Zeit, als Buchung 23571113a stattfand. Vielleicht kann man Adressänderungen auch als „Buchungen“ speichern, mit einem Gültigkeitsbeginn. Dann wird die Adresse für einen Kunden ermittelt, indem man den Adresseintrag für die Kundennummer sucht, der das größtmögliche Gültigkeitsdatum hat, das gerade noch nicht in der Zukunft liegt. Mit dem Ansatz muss man in dieser Tabelle vielleicht tatsächlich nur neue Datensätze einfügen, wenn sich etwas geändert hat. Und wenn ein Kunde gelöscht wird, dann fügt man einen Datensatz ein, der beinhaltet, dass der Kunde ab einem bestimmten Datum „gelöscht“ ist, aber man kann noch bei Bedarf Daten liefern.

Das Schöne ist nun aber, dass Daten, die in der Datenbank „immutable“ sind, also keine Updates erhalten dürfen, in der Software auch problemlos immutable sein können (und sollten) und dass man sie auch langlebiger als eine Transaktion machen kann. Vorsicht ist aber immer geboten, weil komplexe Objekte in der Applikation sich auch allein durch Inserts in einer Datenbanktabelle ändern würden, wenn etwa eine Collection enthalten ist. Man muss also das OR-Mapping genau kennen, egal wie faszinierend diese Automatismen auch sein mögen. Im Fall von einer Kundenadresse muss man die gültige Adresse immer in Verbindung mit einem Timestamp behandeln, der beim ermitteln der gültigen Adresse eingesetzt wurde. Wenn dieser Timestamp veraltet ist, muss man auch die Adresse neu abfragen, wobei es im Gegensatz zu den Buchungen die Adressänderungen selten auf die Mikrosekunde des Umzugszeitpunkts ankommt, solange man die Konsistenz wahrt und nicht in der Woche des Umzugs wilde Mischungen aus der alten und der neuen Adresse verwendet.

Share Button

Mehrfache Interfaces mit inneren Klassen

Die im Beitrag über Multidispatch angedeutete Möglichkeit, mit inneren Klassen oder Wrappern anderen „equals()“ und „hashCode()“-Methoden anzubieten, sollte vielleicht etwas genauer gezeigt werden. Als Beispiel dient hier einmal Java, aber die Thematik ist auch in Scala, Ruby, Perl, C# und anderen Sprachen vorhanden, weil es gängig ist, dass die Hashtabellen gerne mit einer einer bestimmten Kombination aus hashCode() und eqals() zusammenarbeiten. Das zwingt dazu, diese beiden Methoden nach der gängigsten Verwendung in Hashtabellen zu definieren.

Die Idee lässt sich aber auch anwenden, um verschiedene Interfaces mit derselben Klasse zu implementieren.

Das Prinzip sieht so aus:


public interface A {
    public int fa(int x);
}

public interface B {
    public int fb(int x);
}

public class X implements A {

    ....
    public int fa(int x) {
        ....
    }
    private class Y implements B {
        public int fb(int x) {
             return fa(x) % 999;
        }
    }
    public B getAsB() {
        return new Y()
    }
}

Man kann also die Implementierung von B in der inneren Klasse Y vorsehen und dabei auf alle Methoden und Attribute der umgebenden Klasse zugreifen. Die Referenz auf die umgebende Klasse ist bei allen Instanzen der (nicht-statischen) inneren Klasse implizit dabei.

Hier ein Beispiel für verschiedene hashCode() und equals-Methoden:


public interface StringPair {
    public String getLhs();
    public String getRhs();
}

public class StringPairImpl implements StringPair {
    private final String lhs;
    private final String rhs;

    public StringPairImpl(String lhs, String rhs) {
        assert lhs != null;
        assert rhs != null;
        this.lhs = lhs;
        this.rhs = rhs;
    }

    public String getLhs() {
        return lhs;
    }

    public String getRhs() {
        return rhs;
    }

    public int hashCode() {
        return lhs.hashCode() * 91 + rhs.hashCode();
    }

    public boolean equals(Object obj) {
         if (this == obj) {
             return true;
         } else if (! obj instanceof StringPairImpl) {
             return false;
         } else {
             StringPairImpl other = (StringPairImpl) obj;
             return this.lhs.equals(other.lhs) && this.rhs.equals(other.rhs);
         }
    }

    private class IgnoreCase implements StringPair {

        public String getLhs() {
            return lhs;
        }

        public String getRhs() {
            return rhs;
        }

        public int hashCode() {
            return lhs.toUpperCase().hashCode() * 91 + rhs.toUpperCase().hashCode();
        }

        public boolean equals(Object obj) {
             if (this == obj) {
                 return true;
             } else if (! obj instanceof StringPairImpl) {
                 return false;
             } else {
                 StringPairImpl other = (StringPairImpl) obj;
                 return this.lhs.toUpperCase().equals(other.lhs.toUpperCase())
                     && this.rhs.toUpperCase().equals(other.rhs.toUpperCase());
             }
        }
    }

    public StringPair getWithCaseIgnored() {
        return new IgnoreCase();
    }
}

Die innere Klasse bietet also eine Implementierung desselben Interfaces, aber hashCode() und equals() ignorieren diesmal Groß- und Kleinschreibung.

Eine andere Möglichkeit wäre es bei Collections, eine immutable-Variante über eine innere Klasse anzubieten und eine getImmutable()-Methode einzubauen. Das ist aber bekanntlich nicht der Weg, den die JDK-library gewählt hat.

Was man aber dabei auch sieht: Leider muss man sehr viel „unnötigen“ Code schreiben, der eigentlich offensichtlich ist, aber eben doch nicht fehlen darf. Das erschwert sowohl das Schreiben als auch das Lesen des Codes, vor allem aber die Wartbarkeit. So wird die an sich gute Idee der inneren Klassen teilweise wieder verwässert.

Share Button

Snapshot too old – Behandlung langlaufender SELECTs

Wer größere Datenbank-Applikationen entwickelt, wird sich mit dem Problem auseinandersetzen müssen, was bei langlaufenden Abfragen eigentlich passiert.

Man hat also ein „SELECT“ am laufen, dass mehrere Sekunden oder sogar Minuten dauert, vielleicht sogar eine Stunde. Das kann durchaus sinnvoll sein, aber es lohnt sich natürlich, dieses mit gutem Wissen über die Datenbank-Software genau anzuschauen und zu optimieren.

Trotz aller Optimierungen muss man aber prinzipiell davon ausgehen, dass sich die Daten während des Lesezugriffs ändern. Es finden dauernd Schreibzugriffe statt, die natürlich erst nach Abschluss der Transaktion mit einem „COMMIT“ wirklich sichtbar werden. Nun können aber während des Lesezugriffs jede Menge Transaktionen stattfinden. Würde man diese berücksichtigen, müsste der Lesezugriff dauernd von vorne anfangen oder er würde inkonsistente Daten zurückliefern, die von verschiedenen Ständen der Datenbank stammen.

Die Lösung ist, den Stand der Datenbank vom Beginn des Lesezugriffs quasi einzufrieren und für die Dauer des Lesezugriffs zur Verfügung zu stellen. Zugriffe, die später beginnen, bekommen einen anderen Stand zu sehen, auch wenn sie früher fertig werden. Im Fall von Oracle-Datenbanken werden hierfür sogenannte Snapshots verwendet. Es werden also ähnliche Mechanismen wie für Transaktionen verwendet, um mehrere Stände der Datenbank parallel zu speichern.

Das bedeutet aber, dass bei einer sehr aktiven Datenbank immer größere Unterschiede zwischen den beiden Ständen entstehen. Bei vielen Transaktionen oder vielen parallelen langlaufenden Lesezugriffen sind sogar oft viel mehr als zwei Stände. Dafür gibt es Bereiche in der Datenbank, sogenannte Rollback-Segmente. Diese muss man definieren und bereitstellen und wenn sie zu klein sind, laufen sie irgendwann über. Dann kommt es im Fall von Oracle zu dem Fehler „ORA-01555: Snapshot too old“ und der lange Lesezugriff scheitert. Wenn man Pech hat nach ein paar Stunden. Wenn es ein Zugriff durch eine Software ist, wird es interessant, ob die Fehlerbehandlung für diesen Fall funktioniert oder ob es eine obskure „Exception“ in der Log-Datei gibt, die niemand bemerkt.

Mehr dazu findet man unter anderem hier:

Grundsätzlich muss diese Fragestellung bei allen ernsthaften transaktionalen Datenbanken gelöst werden und es ist sicher interessant, wie das im Detail aussieht. Vielleicht ist bei anderen Datenbanken so etwas wie eine „Lese-Transaktion“ („read transaction“) die Antwort. Als Isolationslevel muss man mindestens „repeatable read“ oder „serializable“ wählen. Wenn man mit niedrigeren Isolationsleveln arbeitet, muss man genau wissen, was man tut, da es sonst überraschende Fehler gibt.

Bei PostgreSQL muss man das mit
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
oder
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
explizit einstellen, sonst wird „READ COMMITTED“ gewählt.

Bei mariaDB und mysql hängt es von der DB-Engine ab. InnoDB verwendet als Default „REPEATABLE READ“, DB2 scheint eher auf „READ COMMITTED“ zu setzen, MS-SQL-Server auch auf READ COMMITTED.

Höhere Isolation-Level bedingen natürlich auch das Risiko von Verklemmungen („Deadlocks“), was erkannt werden sollte und zum Rollback einer der beiden Transaktionen führen sollte.

Es lohnt sich also, bei anspruchsvollen DB-Applikationen genau zu schauen, wie man mit diesen Transaktion-Isolationen umgehen will und ob man mit den Default-Eintstellungen der verwendeten Datenbank-Software zufrieden ist.

Vor einigen Jahren habe ich übrigens feststellen müssen, dass man diese niedrigeren Isolationslevel bei Oracle wohl einstellen konnte, dass die Datenbank oder der JDBC-Treiber sich dann aber fehlerhaft verhalten haben. Man sollte also auch herausfinden, ob die entsprechende Eintstellung von der verwendeten Datenbanksoftware unterstützt wird und zwar nicht nur auf dem Papier, sondern real und absolut zuverlässig. Eine unzuverlässige transaktionale Datenbank kann man billig durch einen Zufallszahlengenerator ersetzen.

Share Button