DDD ID Generierung

Nein, da steht z.B mit UUID kannst du de MAC-Adresse des ID-generierenden Servers + der Erstellungszeit (in Nanosekunden) als ID verwenden. Es gibt aber auch Alternativen wie Twitter Snowflake und Boundary Flake, die Links sind am Schluss der Präsentation.

Das sind aber alles Formen einer weltweit eindeutigen ID, weshalb sie parallel generiert werden können. Auf die MAC-Adresse würde ich mich aber wahrscheinlich nicht verlassen, wenn die Knoten in einer virtualisierten Umgebung laufen.

Mal angenommen, es sei das oben genannte Interface gegeben:

public interface IdGenerator<T extends Serializable> {
    T nextId();
}

Dessen Implementierungen interessieren mich erst einmal gar nicht. Wie bekommt jetzt jede Entity am geschicktesten vor der Persistierung eine ID?

Ich habe auf dem Problem mittlerweile schon eine Zeit lang herumgedacht und eigentlich nur folgende Lösung gefunden:
[ol][li]Entities werden immer über Factories erzeugt[/li][li]in die Factories werden IdGeneratoren injiziert[/li][li]für jede Entity gibt es einen IdGenerator, der mit der [japi]Named[/japi]-Annotation benannt wird[/li][li]in die IdGeneratoren wird jeweils ein entityspezifisches Repository injiziert, welches eine Methode findLastId hat, damit kann der Generator initialisiert werden[/ol][/li]
Bei datenbankweit eindeutigen IDs (zu denen UUIDs (inkl. aller Geschmacksrichtungen wie den *flakes), wie auch datenbankweite sequenzen zählen) benötigt man nur die Schritte 1 und 2, weil es nur genau einen ID-Generator gibt.
3 und 4 werden dementsprechend nur bei entityspezifischen IDs benötigt.

Gibt es für entityspezifische IDs vielleicht einen eleganteren Weg, um alles zu verdrahten? Ist das Vorgehen an sich vielleicht schon verkompliziert?

*** Edit ***

Was mich dabei stört ist, dass eigentlich nur die Initialisierung der IdGeneratoren unterschiedlich ist. Wenn die Generatoren sich extern initialisieren ließen, indem ein Konstruktorargument den Initialisierungswert aufnimmt, bräuchte man nur angepasste IdGeneratoren in die EntityFactories zu injizieren.
Für dieses Problem fehlt mir eine (technische) Lösung.

Auf die MAC-Adresse würde ich mich aber wahrscheinlich nicht verlassen, wenn die Knoten in einer virtualisierten Umgebung laufen.

Warum nicht? Falls die Mac dich so sehr stört kannst du immer noch jedem Knoten per hand eine eigene ID-geben, die du der Entity ID voranstellst. Der Punkt ist, dass du mit der möglichen Länge einer UUID so viel Adress-Spielraum bei der Benennung deiner Entities hast, dass irgendwelche n-Inkremente komplett überflüssig sind. Du kannst deinen ID-Generatoren beispielsweise Namen geben, wodurch einer eine UUID: Heinz-328032582345i6u34869346803486340683 und ein andere eine UUID: Fritz-3428083240235093285092u3t09823509235 erstellt. Dann übergibst du per Konstruktor deinem ID-Generator eben Heinz oder Fritz.

Weil ich nicht weiß, wie die Virtualisierungslösungen die MAC-Adressen generieren. Auf einem einzelnen Virtualisierungsserver werden die Adressen sicherlich nicht doppelt vergeben, aber bei zwei unabhängig laufenden Instanzen kommt es eben auf den Generierungsalgorithmus an.

Die Umsetzung eines ID-Generators ist hierbei eigentlich das kleinste Problem. Da gilt es die Punkte
[ul][li]Vergabestrategie
[/li][li]Größe der ID
[/li][li]Performance im DBMS[/ul]
[/li]abzuwägen.

Ich habe derzeit aber folgende Schwierigkeiten, die ich hier gern diskutieren möchte:
[ul][li]Initialisierung und Injizierung der IdGeneratoren (nur bei per-Entity-Ids)
[/li][li]Prüfung, ob eine Entity bereits persistiert wurde oder nicht (geht nicht mehr via id != null - wie ginge das möglichst noninvasiv?)
[/li][*]Übergabe von neuen Entities über eine Webschnittstelle (DTO?)[/ul]

Wie wäre es mit einer UID wie z.b. die ObjectId von MongoDB?

ObjectId is a 12-byte type, constructed using:

[ul]
[li]a 4-byte value representing the seconds since the Unix epoch,[/li]> [li]a 3-byte machine identifier,[/li]> [li]a 2-byte process id, and[/li]> [li]a 3-byte counter, starting with a random value.[/li]> [/ul]

Wäre dann sogar Multiknoten sicher ^^

Wie gesagt, die Gestaltung der ID an sich halte ich mittlerweile für nicht mehr problematisch. Mir geht es vorerst nur noch um

[QUOTE=cmrudolph][…] folgende Schwierigkeiten, die ich hier gern diskutieren möchte:
[ul][li]Initialisierung und Injizierung der IdGeneratoren (nur bei per-Entity-Ids)
[/li][li]Prüfung, ob eine Entity bereits persistiert wurde oder nicht (geht nicht mehr via id != null - wie ginge das möglichst noninvasiv?)
[/li][li]Übergabe von neuen Entities über eine Webschnittstelle (DTO?)[/ul][/QUOTE]
[/li]
Mir ist klar, dass das technische Probleme sind. Daher vielleicht etwas zum verwendeten Stack. Ich nutze Java 8, Spring 4 und Spring Data JPA 1.6 mit Hibernate 4 (Datenbank ist derzeit eine MySQL 5.6).

Am besten wären natürlich Lösungen, die unabhängig von den verwendeten Technologien funktionieren (DI und JPA funktionieren ja weitestgehend unabhängig von der Implementierung).
Allerdings kann man wahrscheinlich gerade bei der Initialisierung der IdGeneratoren einige Spring-Features nutzen, die die Umsetzung erleichtern.
Die Prüfung, ob eine Entity bereits persistiert wurde, ist fast ausschließlich auf Spring Data bezogen, das Konzept dahinter allerdings nicht (z. B. ein Statusflag innerhalb der Entity).

*** Edit ***

Dafür habe ich jetzt eine Lösung mit einem Statusflag und JPA lifecycle events:

import javax.annotation.Nonnull;
import javax.persistence.*;
import java.io.Serializable;

import static com.google.common.base.Preconditions.checkNotNull;

@MappedSuperclass
public class AbstractEarlyIdPersistable<ID extends Serializable> implements Persistable<ID> {
    private static final long serialVersionUID = -5020502021329768695L;
    @Id
    private ID id;
    @Transient
    private boolean isNew = true;

    protected AbstractEarlyIdPersistable() {
    }

    public AbstractEarlyIdPersistable(@Nonnull ID id) {
        this.id = checkNotNull(id);
    }

    @Override
    public ID getId() {
        return id;
    }

    @Override
    public boolean isNew() {
        return isNew;
    }

    @PrePersist
    private void checkId() {
        if (id == null)
            throw new IllegalStateException(String.format("Entity of class %s should get persistet but has no id set.",
                    this.getClass().getCanonicalName()));
    }

    @PostPersist
    @PostLoad
    private void markPersisted() {
        isNew = false;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof AbstractEarlyIdPersistable)) return false;

        AbstractEarlyIdPersistable that = (AbstractEarlyIdPersistable) o;

        return id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }
}

Noninvasiv ist dabei relativ zu sehen, weil alle Entities von der Klasse erben müssen.

Hallo cmdrudolph,
ein paar Denkansöße zum persisted flag.

  • Das Feld genauso zu nennen, wie den accessor (isNew) finde ich unschön (letztlich aber nur Geschmacksfrage).
  • Denke darüber nach, die Semantik des flags umzudrehen. Anstatt isNew also ein isPersisted. Das hat zwei Vorteile. Erstens wird die Bedeutung des Flags klarer. Ohne das Hintergrundwissen, dass es hier um Persistierung geht, könnte man sich fragen, wann eine Entity denn “neu” ist. Zweitens sparst du dir die explizite Zuweisung von “true” bei der Initialisierung.
  • Der Nutzen des boolean-Flags ist ziemlich eingeschränkt. Denke darüber nach, hier ein Long/Integer zu verwenden. Ist dieser null, ist die Entity noch nicht persistiert. Ist dieser non-Null ist sie persistiert. Später könnte man mit einem solchen Feld (welches treffenderweise “version” zu nennen wäre) optimistic locking umsetzen, wenn man es denn braucht. Wenn es ein Version-Feld gibt, wird dessen Null/Not-Null-Zustand übrigens (zumindest von Hibernate) zur Erkennung genutzt, ob die Entity bereits persisitert ist. Du bist damit also out of the box näher an ORM-Frameworks dran als mit Deinen eigenimplementierten Hooks.

Das kam mir auch einen ganz kurzen Moment in den Sinn. Der Accessorname kommt allerdings von Spring Data (im Interface Persistable definiert). Dort wird diese Methode verwendet, um zu prüfen, ob em.merge oder em.persist aufgerufen werden muss.

Über Versionierung habe ich mir bisher noch gar keine Gedanken gemacht (abgesehen davon, dass das später wahrscheinlich mal mit envers passieren wird) - mir kam an dieser Stelle gar nicht die Idee, dass eine Versionierung die Verallgemeinerung des Flags ist. Die Idee gefällt mir aber recht gut.
Allerdings werden nicht alle Entities versioniert werden, daher werde ich für die versionierten noch eine zweite Superklasse erstellen.

Die Implementierung der isNew-Methode würde ich dann entsprechend austauschen, falls Spring Data JPA intern nicht noch ein separates Interface für versionierte Entities anbietet.

*** Edit ***

Achso, meine Implementierung der equals-Methode ist Murks, weil sie falsche Ergebnisse hervorrufen kann, wenn man unterschiedliche Klassen miteinander vergleicht. Der Equals-Vergleich muss also in der Unterklasse gemacht werden.

[QUOTE=cmrudolph]
Achso, meine Implementierung der equals-Methode ist Murks, weil sie falsche Ergebnisse hervorrufen kann, wenn man unterschiedliche Klassen miteinander vergleicht. Der Equals-Vergleich muss also in der Unterklasse gemacht werden.[/QUOTE]
wenn du aber nur überall auf gleiche Klasse prüfen willst, dann muss das ja nicht in mehreren Methoden geschehen,

mit den drei Standardzeilen

        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;

ginge es auch hier

Daran hatte ich auch schon gedacht. Das hat aber zur Folge, dass Proxies sich mit der Entityklasse nicht vergleichen lassen bzw. dass diese immer ungleich sind.
Bei der Verwendung von JPA sind Proxies ja fast zwangsläufig im Spiel (sofern man nicht mit Bytecodemanipulation arbeitet, was wieder Implementierungsspezifisch ist), weshalb mir ein o instanceof EntityClass sicherer erscheint.

Da lässt sich Class#isAssignableFrom(java.lang.Class) nutzen. Damit kannst Du equals -wie von SlaterB vorgeschlagen- in der Oberklasse implementieren und gleichzeitig großzügig (nicht nur Klassenidentität) bei der equals-Kompatibilität sein.

Meinst du so:

public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o.getClass().isAssignableFrom(this.getClass()) || this.getClass().isAssignableFrom(o.getClass()))) return false;

    AbstractEarlyIdPersistable that = (AbstractEarlyIdPersistable) o;
 
    return id.equals(that.id);
} 

Nicht ganz. Bei** instanceof EntityClass** testest Du ja auch nur die eine Richtung (ob Object o die gleiche oder eine Unterklasse ist). Gleiches machst Du mit isAssignablefrom auch. Also nur den einen Zweig Deiner or-Prüfung:
getClass().isAssignableFrom(o.getClass());
(Ich mag das this. nicht :twisted:)

Da funktioniert das ja auch. Bei isAssignableFrom wird ansonsten der equals-Contract verletzt:

Beispiel:
Sei x eine Instanz von MyEntity und y ein dynamisch generierter Proxy vom Typ MyEntityProxy. MyEntityProxy ist Unterklasse von MyEntity.
Daraus folgt MyEntity.class.isAssignableFrom(MyEntityProxy.class) == true aber MyEntityProxy.class.isAssignableFrom(MyEntity.class) == false.
Daher kann zwar x.equals(y) == true gelten, aber es gilt immer y.equals(x) == false. Die Symmetrieeigenschaft ist verletzt.

Ich habe das nicht 100%ig durchdacht, daher habe ich mein letztes Posting als Frage formuliert.

Dein Einwand mit der Symmetrie ist korrekt. DAS habe ICH nicht bis zu Ende gedacht. Deine Lösungsidee funktioniert aber nicht, weil
(o.getClass().isAssignableFrom(this.getClass()) || this.getClass().isAssignableFrom(o.getClass()))
auch true zurück gibt, wenn der Typ von o eine Superklasse von MyEntity ist (z.B. Object). Der Fall müsste also auch noch ausgeschlossen werden. Damit wird der Code dann aber so hässlich, dass ich inzwischen denke, es ist besser, wenn Du equals in den jeweiligen Klassen ausprogrammierst und dort dann instanceof mit konstanter Klasse prüfst…

nur die bestehende Zeile
if (!(o instanceof AbstractEarlyIdPersistable)) return false;
zu belassen, also eine Zeile mehr statt equals in potentiell 50 Unterklassen, halte ich nicht für häßlich,

falls einem dass zu langsam ist (oder zumindest vorkommt), zuviel Arbeit für equals-Methode, ok,
falls es reale Probleme gibt, vielleicht auch eigene Vererbung zwischen Entity-Klassen?, ok

aber wenn es wirklich so konsequent nach der Id geht und die Chance besteht hier mit einer Methode alle oder viele Klassen abzudecken,
dann sollte dies versucht werden


und in so einer zentralen Methode einer zentralen Klasse wären dann doch noch mehr Code, ruhig auch gleich 20 Zeilen extra für (vermeinliche) Performance, auch ok,
erstmal doch der Class-Vergleich, nur wenn unterschiedliche Klassen dann instanceof & Co. usw.,

[QUOTE=nillehammer]Deine Lösungsidee funktioniert aber nicht, weil

(o.getClass().isAssignableFrom(this.getClass()) || this.getClass().isAssignableFrom(o.getClass()))

auch true zurück gibt, wenn der Typ von o eine Superklasse von MyEntity ist (z.B. Object).[/QUOTE]
Stimmt, den Fall habe ich übersehen. Allerdings gibt equals dann nicht true zurück, sondern es sollte eine ClassCastException fliegen, weil o ja kein AbstractEarlyIdPersistable ist.

Ich finde, dass @SlaterB Recht hat. Es ist nur die eine Zeile vor dem Cast. Der Vorteil, dass man nicht vergessen kann, die Methode zu überschreiben, wiegt schwerer als der zusätzliche Check. Spezialfälle wie tiefere Vererbunghierarchien, bei denen entitypezifische IDs vergeben werden, können dann immer noch separat behandelt werden.

Dann ist das vorerst meine equals-Methode:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o.getClass().isAssignableFrom(getClass()) || getClass().isAssignableFrom(o.getClass()))) return false;

    if (!(o instanceof AbstractEarlyIdPersistable)) return false;
    AbstractEarlyIdPersistable that = (AbstractEarlyIdPersistable) o;

    return id.equals(that.id);
}

Soo schlimm finde ich das gar nicht.

Edit: in #14 habe ich übrigens das abstract bei der Klassendeklaration vergessen.

Da habt Ihr beide Recht. In Kombination sieht das garnicht so schlecht aus. Ich würd nur vielleicht zuerst die instanceof-Prüfung machen oder sie sogar in die ||-Kaskade als ersten Zweig aufnehmen.

Und nur der Vollständigkeit halber:

Ich bezog mich in meinem letzten Post nur auf die Prüfung, die ich hingeschrieben habe, weil das der entscheidende Teil war.

Klar. Ich weiß auch nicht, was ich da in dem Moment gelesen hatte :wink:


Mit dem Gesamtproblem bin ich jetzt an einem Punkt angekommen, an dem für jede Entity drei Klassen erstellt werden müssen (weniger geht glaube ich auch gar nicht mehr).

Das sieht dann so aus:

Wie man sieht, ist das einmal der IdGenerator-Zweig, bestehend aus dem IdGenerator-Interface:

@FunctionalInterface
public interface IdGenerator {
    int nextId();
}

dessen Implementierung LocalIdGenerator:

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import java.util.concurrent.atomic.AtomicInteger;

@ThreadSafe
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class LocalIdGenerator implements IdGenerator {
    private final AtomicInteger currentId;

    public LocalIdGenerator(@Nullable Integer initialId) {
        currentId = new AtomicInteger(initialId == null ? 0 : initialId);
    }

    @Override
    public int nextId() {
        return currentId.incrementAndGet();
    }
}

und einer weiteren Abstraktionsschicht, um die Implementierung des IdGenerators später problemlos austauschen zu können:

import org.springframework.stereotype.Component;

import javax.annotation.Nullable;

@Component
public class IdGeneratorFactory {
    public IdGenerator createIdGenerator(@Nullable Integer initialId) {
        return new LocalIdGenerator(initialId);
    }
}

Für den Datenbankzugriff gibt es die Repositories:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface MyEntityRepository extends JpaRepository<MyEntity, Integer> {
    @Query("select max(e.id) from MyEntity e")
    Integer getLastId();
}

Und schlussendlich gibt es noch die Entity-Hierarchie, die oben mit AbstractEarlyIdPersistable beginnt:

import org.springframework.data.domain.Persistable;

import javax.annotation.Nonnull;
import javax.persistence.*;
import java.io.Serializable;

import static com.google.common.base.Preconditions.checkNotNull;

@MappedSuperclass
public abstract class AbstractEarlyIdPersistable<ID extends Serializable> implements Persistable<ID> {
    private static final long serialVersionUID = -5020502021329768695L;
    @Id
    private ID id;
    @Transient
    private boolean persisted = false;

    protected AbstractEarlyIdPersistable() {
    }

    public AbstractEarlyIdPersistable(@Nonnull ID id) {
        this.id = checkNotNull(id);
    }

    @Override
    public ID getId() {
        return id;
    }

    @Override
    public boolean isNew() {
        return !persisted;
    }

    @PrePersist
    private void checkId() {
        if (id == null)
            throw new IllegalStateException(String.format("Entity of class %s should get persistet but has no id set.",
                    this.getClass().getCanonicalName()));
    }

    @PostPersist
    @PostLoad
    private void markPersisted() {
        persisted = true;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof AbstractEarlyIdPersistable) ||
                !(o.getClass().isAssignableFrom(getClass()) || getClass().isAssignableFrom(o.getClass())))
            return false;

        AbstractEarlyIdPersistable that = (AbstractEarlyIdPersistable) o;

        return id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }
}

und dann mit der Entity weiter geht:

@Entity
public class MyEntity extends AbstractEarlyIdPersistable<Integer> {
    private static final long serialVersionUID = -60470266393164538L;
    private String field1;
    private String field2;

    protected MyEntity() {
    }

    public MyEntity(int id,
                    @Nonnull String field1,
                    @Nonnull String field2) {
        super(id);
        this.field1 = checkNotNull(field1);
        this.field2 = checkNotNull(field2);
    }

    public String getField1() {
        return field1;
    }

    public String getField2() {
        return field2;
    }
}

In der Factory laufen die Fäden zusammen:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import xxx.domain.support.IdGenerator;
import xxx.domain.support.IdGeneratorFactory;

import javax.annotation.Nonnull;

@Component
public class MyEntityFactory {
    private final IdGenerator idGenerator;

    @Autowired
    public MyEntityFactory(MyEntityRepository repository, IdGeneratorFactory idGeneratorFactory) {
        idGenerator = idGeneratorFactory.createIdGenerator(repository.getLastId());
    }

    public MyEntity createEntity(@Nonnull String param1, @Nonnull String param2) {
        return new MyEntity(idGenerator.nextId(), param1, param2);
    }
}

Die schwierigste Entscheidung war dabei, die Factory sowohl das Repository als auch die IdGeneratorFactory kennen zu lassen, weil ich sie eigentlich gern vom Repository unabhängig hätte. Ich habe bisher aber noch keinen Weg gefunden (ohne pro Entity noch konkrete IdGeneratoren einzuführen, die dann wiederum das zugehörige Repository kennten), diese Abhängigkeit dort heraus zu halten.

Das ist alles recht Springlastig, das Konzept ist aber problemlos auf andere DI-Frameworks übertragbar. Die Repositories müsste man dann natürlich auch von Hand ausprogrammieren.

Wenn jemand zu den genannten Problemen noch Tipps oder Verbesserungsvorschläge hat, würde ich mich freuen.

Ich habe bis hierher interessiert mitgelesen, und muss jetzt einfach mal fragen, welchen Sinn das alles hat. Ist ja reichlich kompliziert geworden :slight_smile:

(vorab: ich hab wirklich keine Ahnung von DDD)

Muss mich einfach wundern als alter Mann: wozu braucht man künstliche Primärschlüssel für Entities BEVOR diese persistiert werden?

Die Identität einer Entity hängt bei Domänengetriebener Entwicklung einzig und allein von der … Identität der Entity ab. Das bedeutet logischerweise, dass in einer equals-Methode einzig und allein diese ID als Vergleichswert zulässig ist. Das wiederum hat zur Folge, dass die ID bereits vor der Persistierung bekannt sein muss.

Doch nur dann, wenn man die Entity vor der Persistierung in ein .equals steckt. Aber wann und warum sollte man das machen, wozu braucht man in der DDD eine Entity in der Schwangerschaft…Kann es sein, dass der herkömmliche Gang der Dinge

  1. Irgendwoher kommen ein Schwung Daten
  2. Ein oder mehrere Entities erzeugen mit „LEEREM“ Primärschlüssel
  3. In einer Transaktion alle in die Datenbank, dabei bekommt jede (von mir aus von der Datenbank per Sequenz, Autoincrement, …) eine ID verpasst
  4. Fertig

Wie oft - und warum - gibt es bei der DDD überhaupt die Notwendigkeit, mit Entities [die noch nicht persistiert wurden, aber irgendwann werden] großartige Berechnungen oder Aktivitäten durchzuführen? Gerade in hochgradig verteilten System ist ein vom RDBMS generierter Primärschlüssel doch Gold wert und recht einfach zu benutzen…