@JdbcTypeCode(SqlTypes.JSON) mit eigener Java-Klasse

Wir nutzen eigentlich bei allen Services bzw. allen Hibernate-Entities immer die @Version-Annotation für Optimistic Locking.

Siehe dazu auch:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#locking-optimistic-mapping

Bei einem speziellen Service sind uns in letzter Zeit aber immer wieder Datensätze mit sehr hohen Werten bei der Version aufgefallen, die eigentlich nicht durch reguläre Updates (durch die Business Logik) erklärbar waren.

Die erste Vermutung ging in die Richtung eines Concurrency-Problems, allerdings konnte das auch relativ schnell ausgeschlossen werden.

Glücklicherweise konnten wir das Problem durch Experimentieren mit unterschiedlichen Testfällen relativ schnell auch lokal reproduzieren.

Im Speziellen haben wir festgestellt, dass hier vor einem Select-Query zu einzelnen Entities Updates durchgeführt wurden, obwohl diese eigentlich gar nicht geändert wurden.

Siehe dazu auch:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#flushing-auto

Eine weitere Analyse ergab, dass das Problem bei einer von uns definierten Java-Klasse lag, die in der Datenbank auf eine JSON-Column gemappt wurde.

Hier ein vereinfachtes Beispiel dazu:

    @Entity(name = "MyPerson")
    public static class MyPerson {

        @Id
        @GeneratedValue
        Long id;

        String name;

        @JdbcTypeCode(SqlTypes.JSON)
        @Column(name = "my_json")
        MyJson myJson;

        @Version
        int version;

        public MyPerson() {
        }

        public MyPerson(String name) {
            this.name = name;
        }
    }

    public static class MyJson {
        String text = "fubar";
    }

Sieht im Grunde eigentlich in Ordnung aus. Wenn man damit aber einen ganz einfachen Test ausführt …

    @Test
    void savePerson(SessionFactoryScope scope) {
        scope.inTransaction(session -> {
            MyPerson person = new MyPerson("name");
            person.myJson = new MyJson();
            session.persist(person);
        });
    }

… sieht man, dass hier nach dem Insert ein zusätzliches Update durchgeführt wird:

Hibernate: insert into MyPerson (my_json,name,version,id) values (?,?,?,?)
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?

Aber woran liegt das?

Das Problem ist schlicht und einfach, dass vergessen wurde in der Klasse MyJson die equals()- und hashCode()-Methoden zu überschreiben. Erweitert man das vorhergehende Beispiel wie folgt um eine equals()-Methode wird auch das Update-Query nicht mehr getriggert:

    public static class MyJson {

        String text = "fubar";

        public boolean equals(Object other) {
            return other instanceof MyJson myJson && text.equals(myJson.text);
        }
    }

Das Überschreiben der equals()-Methode garantiert die korrekte Funktionsweise des dirty checking Mechanismus von Hibernate, welcher sicherstellt, dass Änderungen in den Java-Entities nach Änderungen auch zur Datenbank synchronisiert werden.

Bei JSONs ist es ein wenig komplizierter, weil ein Objekt nach Serialisierung und anschließender Deserialisierung nicht mehr dem Original entspricht und damit auch Hibernate glaubt, dass sich an dem Objekt etwas geändert hat (obwohl sich die beiden Objekte nur durch die Object Identity, nicht aber im Inhalt unterscheiden). Aus diesem Grund ist es empfehlenswert bei eigenen Klassen, die als JSON in der Datenbank gespeichert werden sollen, darauf zu achten, dass auch die equals()- und hashCode()-Methode entsprechend korrekt sind.

Es kann aber noch schlimmer werden…
Hat man nämlich eine Batch-Verarbeitung implementiert, bei der man mehrere Objekte eines Typs zuvor selektiert hat, somit im Persistence Context hält und in einer Schleife zu jedem Objekt ein weiteres Select-Query ausführt, wird vor jedem Select für jede Entity ein Update-Query ausgeführt. Somit können also schnell n² Updates entstehen, selbst wenn man nichts ändern wollte.

Hier ein vereinfachtes Beispiel:

    @Test
    void saveThreePersons_batchProcessing(SessionFactoryScope scope) {
        scope.inTransaction(session -> {

            MyPerson first = new MyPerson("name");
            first.myJson = new MyJson();
            session.persist(first);

            MyPerson second = new MyPerson("name");
            second.myJson = new MyJson();
            session.persist(second);

            MyPerson third = new MyPerson("name");
            third.myJson = new MyJson();
            session.persist(third);

            List<MyPerson> persons = session.createQuery("select p from MyPerson p", MyPerson.class).getResultList();

            for (MyPerson person : persons) {

                session.createQuery("select p from MyPerson p where p.id=:id", MyPerson.class).setParameter("id", person.id).getSingleResult();

            }
        });

    }

In diesem Beispiel wird eine Vielzahl von Updates ausgeführt:

Hibernate: insert into MyPerson (my_json,name,version,id) values (?,?,?,?)
Hibernate: insert into MyPerson (my_json,name,version,id) values (?,?,?,?)
Hibernate: insert into MyPerson (my_json,name,version,id) values (?,?,?,?)
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: select mp1_0.id,mp1_0.my_json,mp1_0.name,mp1_0.version from MyPerson mp1_0
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: select mp1_0.id,mp1_0.my_json,mp1_0.name,mp1_0.version from MyPerson mp1_0 where mp1_0.id=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: select mp1_0.id,mp1_0.my_json,mp1_0.name,mp1_0.version from MyPerson mp1_0 where mp1_0.id=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: select mp1_0.id,mp1_0.my_json,mp1_0.name,mp1_0.version from MyPerson mp1_0 where mp1_0.id=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?
Hibernate: update MyPerson set my_json=?,name=?,version=? where id=? and version=?

Im übrigen kann man in solchen Situationen auch den FlushMode einzelner Queries von AutoCommit auf Manual ändern.

Siehe dazu auch:
https://thorben-janssen.com/flushmode-in-jpa-and-hibernate/#how-to-configure-the-flushmode

Das sollte aber auch nur dann genutzt werden, wenn man sich wirklich sicher ist, dass sich die Entities zuvor wirklich nicht geändert haben, die man hier selektieren will.

Link zum vollständigen Test:
https://github.com/peter1123581321/hibernate-test-case-templates/blob/json-version-bug/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java

HHH-18898 fix of a NPE when using an embeddable with a specified JavaType

In https://hibernate.atlassian.net/browse/HHH-18898 wurde ein Bug bei der Verwendung eines Embeddables eingemeldet.

Hier ein einfaches Beispiel, bei der die EntityEmbedNative die EmbedNative einbindet.

    @Embeddable
    public static class EmbedNative {

        @Column(name = "DATUM")
        @JavaType(LocalDateJavaType.class)
        LocalDate value;
    }

    @Entity(name = "EntityEmbedNative")
    public static class EntityEmbedNative {

        @Id
        @Column(name = "id")
        long id;

        @Embedded
        EmbedNative embedNative;
    }

Link zur Embeddable-Annotation:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#embeddables

Wenn man folgende Select-Statements ausführt, sieht man dass bei zweien eine NullPointerException auftritt:

"select z from EntityEmbedNative z where embedNative.value=:datum",
"select z from EntityEmbedNative z where :datum=embedNative.value",
"select z from EntityEmbedNative z where embedNative=:datum", // this query failed with the bug
"select z from EntityEmbedNative z where :datum=embedNative",
"select z from EntityEmbedNative z where embedNative.value in (:datum)",
"select z from EntityEmbedNative z where embedNative in (:datum)" // failed as well

Anzumerken ist, dass das dritte, vierte und sechste Query aber ohnehin nur funktionieren kann, wenn das Embeddable-Objekt nur eine Feld hat (was im oberen Beispiel gegeben ist).

Interessanterweise wird die NullPointerException aber nur geworfen, wenn das Argument auf der rechten Seite der Expression steht, nicht aber wenn es auf der linken Seite steht wie es im vierten Query auch der Fall ist.

Hier geht’s zum vollständigen Reproducer-Test:
https://github.com/peter1123581321/hibernate-test-case-templates/blob/HHH-18898/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/HHH18898Test.java

Der Bug konnte durch zwei Null-Checks in der Klasse AbstractSqlAstTranslator schnell gefixt werden.

Hier der Pull-Request:
https://github.com/hibernate/hibernate-orm/pull/10235

Der Bug wurde ursprünglich nicht für native Datentypen eingemeldet sondern in Kombination mit der @JavaType-Annotation. Außerdem ist mir während dem Bugfixing auch aufgefallen, dass das Problem auch bei Listen mit dem IN-Operator auftritt. Somit wurde der Bug eigentlich für 4 unterschiedliche Szenarien gefixt. 🙂

HHH-8535 Generating an ID with TableGenerator causes a loop if next_val is NULL

Diesmal ein Bug der bereits vor über 10 Jahren eingemeldet wurde:
https://hibernate.atlassian.net/browse/HHH-8535

Dabei geht’s um eine @TableGenerator-Annotation mit der eine table-based Sequence definiert wurde. Der Bug beschreibt eine Endlosschleife, der auftritt, wenn der next_val-Wert versehentlich (warum oder wie auch immer) auf NULL gesetzt wurde.

Den Test zum Reproduzieren des Bugs findet man hier:
https://github.com/peter1123581321/hibernate-test-case-templates/blob/HHH-8535/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java

Der Bug wurde im TableGenerator gefixt indem ggf. eine HibernateException geworfen wird.

Link zum Bugfix:
https://github.com/hibernate/hibernate-orm/pull/10288/commits/e5ed84e9fe6c73d240062a4e91abed59dbb4093e

Link zu @TableGenerator-Annotation in der Hibernate-Doku:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#identifiers-generators-table

HHH-13721 Getting duplicated entities with @OneToMany and @ElementCollection

In https://hibernate.atlassian.net/browse/HHH-13721 wurde ein Bug im Zusammenhang mit der @ElementCollection-Annotation beschrieben.

Das Jira-Ticket war zwar noch offen, der Bug konnte allerdings nicht mehr reproduziert werden. Nachdem aber im Code keine Referenz auf das Jira-Ticket zu finden war, wurde mit einem Pull-Request zumindest ein neuer Test hinzugefügt und das Jira-Ticket dann auch geschlossen:
https://github.com/hibernate/hibernate-orm/pull/10283/files

Die @ElementCollection-Annotation ist jedenfalls nicht uninteressant, da man damit eine one-to-many Beziehung abbilden kann, bei der die Child-Elemente zwar in einer eigenen Tabelle gespeichert werden, aber selbst keine Entities sind. Somit sind sie auch an die Parent-Entity gebunden und werden zusammen mit ihr persistiert oder gelöscht. Als Beispiel findet man oft eine Person-Entity der ein oder mehrere Telefonnumern zugeordnet sind.

Link zur Java-Doc der @ElementCollection-Annotation:
https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/elementcollection

Link zur Hibernate-Doc zur @ElementCollection-Annotation:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#collections-elemental

HHH-18891 fix of an AssertionError when using a NotFound annotation

In https://hibernate.atlassian.net/browse/HHH-18891 wurde ein Bug bezüglich der @NotFound-Annotation eingemeldet.

Prinzipiell kann diese Notation mit einer @ManyToOne– oder @OneToOne-Annotation verwendet werden. Normalerweise hat man in so einem Fall einen Foreign-Key auf die Parent-Tabelle. Wenn dieser jedoch fehlt, können in der Column der Child-Entity aber auch IDs gespeichert werden, die in der Parent-Tabelle eventuell gar nicht exisitieren. Nachdem bei einer @ManyToOne– oder @OneToOne-Annotation die Parent-Entity (eager) mitgeladen wird, wird per default eine Exception geworfen. Mit einer @NotFound(IGNORE)-Annotation kann die Exception aber auch verhindert werden.

@ManyToOne, @OneToOne und @NotFound-Annotation in der Hibernate-Doku:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#associations-many-to-one
https://docs.hibernate.org/orm/7.2/userguide/html_single/#associations-one-to-many
https://docs.hibernate.org/orm/7.2/userguide/html_single/#associations-not-found

In einer speziellen Konstellation tritt bei der Annotation allerdings ein Assertion-Error auf:

java.lang.AssertionError
	at org.hibernate.sql.results.graph.Initializer.getResolvedInstance(Initializer.java:67)
	at org.hibernate.sql.results.graph.embeddable.internal.EmbeddableAssembler.assemble(EmbeddableAssembler.java:40)

Den Testcase zum Reproduzieren des Bugs findet man hier:
https://github.com/peter1123581321/hibernate-test-case-templates/blob/HHH-18891/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/HHH18891Test.java

Konkret tritt der Fehler aber in Kombination mit einer @JoinFormula-Annotation auf:

    @Entity(name = "DocumentIgnore")
    public static class DocumentIgnore {

        @Id
        @GeneratedValue
        Long id;

        @ManyToOne
        @NotFound(action = NotFoundAction.IGNORE)
        @JoinColumnOrFormula(column = @JoinColumn(name = "owner", referencedColumnName = "id", insertable = false,
                updatable = false))
        @JoinColumnOrFormula(formula = @JoinFormula(value = "'fubar'", referencedColumnName = "name"))
        Person owner;
    }

Link zur @JoinFormula-Annotation:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#associations-JoinFormula

Der entsprechende Bugfix war im EmbeddableAssembler dann relativ schnell umgesetzt.

Link zum Pull-Request:
https://github.com/hibernate/hibernate-orm/pull/10208 

HHH-18813: Fix of generated Insert-Query in CteUpdateHandler

In https://hibernate.atlassian.net/browse/HHH-18813 wurde ein Bug beschrieben der auftritt, wenn man zwei Tables mit InheritanceType.Joined verwendet.

Links zu Inheritance bzw. Joined table in der Hibernate Doku:
https://docs.hibernate.org/orm/7.2/userguide/html_single/#entity-inheritance
https://docs.hibernate.org/orm/7.2/userguide/html_single/#entity-inheritance-joined-table

Außerdem wurde bei einer der Tables die SecondaryTable-Annotation verwendet um einzelne Columns in einer separaten Table auszulagern.

Javadoc zu den Inheritance- und SecondaryTable-Annotations:
https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/secondarytable
https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/inheritance

Das Setup sieht dann wie folgt eigentlich recht einfach aus. Es gibt eine Base-Entity mit einer Id und eine weitere Entity, welche die Base-Entity extended und nur noch zwei Attribute enthält. Allerdings wird eines der beiden Attribute in einer weiteren Tabelle gespeichert.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class SecondaryTableEntityBase {

    private Long id;

    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@SecondaryTable(name = "test")
public class SecondaryTableEntitySub extends SecondaryTableEntityBase {

    private Long b;

    private Long c;

    @Column
    public Long getB() {
        return b;
    }

    public void setB(Long b) {
        this.b = b;
    }

    @Column(table = "test")
    public Long getC() {
        return c;
    }

    public void setC(Long c) {
        this.c = c;
    }
}

Die daraus resultierenden Create-Statements sehen wie folgt aus:

Hibernate: 
    create table SecondaryTableEntityBase (
        id bigint not null,
        primary key (id)
    )
Hibernate: 
    create table SecondaryTableEntitySub (
        b bigint,
        id bigint not null,
        primary key (id)
    )
Hibernate: 
    create table test (
        c bigint,
        id bigint not null,
        primary key (id)
    )

Nun gibt es aber das Problem aus dem eigentlichen Bug-Report, das folgendes Update-Statement fehlschlägt:

update SecondaryTableEntitySub e set e.b=:b, e.c=:c

Nämlich mit der Fehlermeldung:

Caused by: org.postgresql.util.PSQLException: ERROR: column "b" of relation "test" does not exist

Eh klar, es liegt ja auch nur die Column c in der Tabelle test, nicht aber b.

Reproduziert werden kann das mit diesem Testcase:
https://github.com/peter1123581321/hibernate-test-case-templates/blob/HHH-18813/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java

Allerdings tritt der Fehler nicht mit H2 oder MySQL auf, aber zumindest mit Postgres.

Den Pull-Request zum Bugfix findet man dann hier:
https://github.com/hibernate/hibernate-orm/pull/10173

Der Fehler passierte hier letztlich im CteUpdateHandler und konnte sehr leicht gefixt werden, weil hier an einer Stelle nur zwei Listen verwechselt wurden:
https://github.com/hibernate/hibernate-orm/pull/10173/commits/2279fd10a43ef75bc6acc6181a55c3fe93986d2e#diff-7f87d8e3b206813710d227a659e8d3fe39cccc825b75a7ee9483c4fb0411a037

Somit tritt dieses Problem nun auch nicht mehr auf. 🙂

ATCo2: Development Update #3

In den letzten Wochen gab es wieder vier große Updates:

  • Erstens enthält ATCo2 jetzt 30 Steam-Errungenschaften.
  • Zweitens wurden Probleme mit Bildschirmauflösungen und Seitenverhältnissen behoben. Das Spiel wird ab sofort immer im 16:9-Format angezeigt. Kleinere Bildschirmauflösungen werden nun ebenfalls unterstützt.
  • Drittens wurden neue Versionen für Linux und macOS hinzugefügt.
  • Viertens gibt es jetzt auch eine richtig coole Hintergrundmusik.

Weitere tolle Verbesserungen:

  • Die Levelkarten sind jetzt farbig.
  • Der Zeitfaktor kann mit den +/- Tasten auf dem Nummernblock erhöht/verringert werden.
  • Ein neuer Zähler oben zeigt nun die Anzahl aller jemals gelandeten Flugzeuge an.
  • Bei einer Landung auf dem Zielflughafen wird nun der CO₂-Ausstoß pro Person für diesen Flug angezeigt.
  • Ein weiterer Zähler oben zeigt nun die Gesamtmenge des CO₂-Ausstoßes pro Person für alle jemals gelandeten Flugzeuge an.
  • Ein neuer Infotext erklärt, warum ein Level verloren wurde (CO₂-Emissionen vs. Flugzeugabsturz/Verlust).

Ich hoffe die Änderungen gefallen euch 🙂

ATCo2: Development Update #2

Heute gibt es wieder ein neues Update von ATCo2 auf Steam.

Hier eine Übersicht der wichtigsten Änderungen:

  • Diverse Screens überarbeitet (inkl. neuer Hintergründe)
  • Szenenübergänge (Ein-/Ausblenden) hinzugefügt
  • Neue Texte zu den Klimaauswirkungen der Fliegerei in den Level-Start-Screens
  • Zufälligere Auswahl des Startflughafens
  • Größe der Flugzeuge und der Infoboxen angepasst
  • Größe der Zielflughafen-Texte/-Punkte angepasst
  • Unterscheidung zwischen wide-body- und narrow-body Flugzeugen
  • Überarbeitete obere und untere Leiste in der Levelansicht
  • Levels sind jetzt nach Anzahl der Zielflughäfen sortiert
  • Anzahl der Flugzeuge abhängig vom Level
  • Leveldauer individuell
  • Standardgeschwindigkeit der Flugzeuge abhängig vom Level
  • Maximaler CO₂-Fußabdruck abhängig vom Level
  • CO₂-Strafe hinzugefügt, wenn ein Flugzeug vom Kurs zum Zielflughafen abweicht
  • CO₂-Strafe abhängig vom Level
  • 7 neue Levels, insgesamt jetzt 20 Level
  • Levelparameter optimiert
  • Windsystem implementiert
  • Mehr Verkehr in mehreren Leveln
  • Nach Ablauf der Levelzeit müssen alle verbleibenden Flugzeuge zu ihren Zielen geleitet werden
  • Diverse Fehlerbehebungen

Das Spiel sollte jetzt noch mehr Spaß machen.
Ich hoffe es gefällt euch! 🙂

ATCo2: Development Update #1

In den letzten zwei Wochen gab es viele Fortschritte und nun auch ein neues Update auf Steam, das eigentlich erst für Ende September geplant war.

Das neueste Update beinhaltet einige grundlegende Gameplay-Verbesserungen:

  • Flugzeuge, die vom Kurs auf ihren Zielflughafen abweichen, werden rot markiert. Neben dem Flugzeug erscheint ein neuer Warntext, der euch über die Situation informiert.
  • Flugzeuge, die auf Kollisionskurs sind oder sich in unmittelbarer Nähe befinden, werden ebenfalls rot markiert. Auch hier erscheint neben dem Flugzeug ein neuer Warntext, der euch über die Situation informiert.
  • Wir haben eine kleine Animation (Absturz-/Explosionssymbol) hinzugefügt, die bei einer Kollision zweier Flugzeuge angezeigt wird.
  • Spieler werden mit kurzer Textmeldung (die nach wenigen Sekunden automatisch verschwindet) benachrichtigt, wenn ein Flugzeug aus dem Bild verschwindet.
  • Spieler werden mit kurzer Textmeldung (die nach wenigen Sekunden automatisch verschwindet) benachrichtigt, wenn ein Flugzeug seinen Zielflughafen erreicht hat.
  • Im ersten Level gibt es eine kurze Einweisung in die Flugzeugsteuerung und Informationen zum Spielziel.

Außerdem wurde mit der Arbeit an 10 neuen Levels begonnen, die noch während der Early-Access-Phase zur Verfügung gestellt werden. Die neuen Levels decken folgende Regionen ab:
USA Ost, USA West, Mittelamerika, Südamerika, Südafrika, Südostasien, Portugal, Westeuropa, Großbritannien, Japan/Südkorea

Ich hoffe es gefällt euch! 🙂

ATCo2: Release Announcement

Mein erstes Spiel wird am 1. September 2022 um 18 Uhr MESZ als Early-Access-Titel auf Steam erscheinen.

Hier geht’s zur Store-Page des Spiels:
https://store.steampowered.com/app/2108400/ATCo2/

ATCo2 ist ein unterhaltsames Spiel für Luftfahrt-Fans, die mehr über die Nachhaltigkeitsauswirkungen des Fliegens erfahren möchten.

Ihr schlüpft in die Rolle eines Fluglotsen an verschiedenen Orten der Welt.
Euer Ziel ist es, alle Flugzeuge ohne unnötige Umwege zu ihren Zielflughäfen zu lotsen. Um das nächste Level zu erreichen, müsst ihr die gesamten CO₂-Emissionen stets im Blick behalten.

Bleibt gespannt auf weitere Ankündigungen!