Blog

Abbildung von MultiMaps mit Hibernate

Maarten Winkels

Aktualisiert Oktober 23, 2025
12 Minuten

Hibernate ist eine sehr vollständige ORM-Bibliothek. Eine der wenigen Auslassungen ist die Möglichkeit, Sammlungen von Sammlungen abzubilden. In diesem Blog wird dieses Versäumnis untersucht und eine Lösung für den speziellen Fall einer Map of Sets angeboten.

Sammlung von Sammlungen

Werfen wir zunächst einen Blick auf eine einfache Sammlungszuordnung. Hier hat eine Person eine Liste von Freunden.

public class Person {
  private Liste Freunde;
}
  ...

...

Dies führt zu den folgenden Tabellen:

Person
ID
1
Freund
IDPERSON_FKPOS
110
211

Die Tabelle Friend hat einen Fremdschlüssel zur Tabelle Person (PERSON_FK) und sie enthält auch eine Spalte mit dem Index in der Liste (POS). Dies ist eine sehr gebräuchliche Zuordnung (es gibt andere Möglichkeiten, dies im Datenbankschema zu modellieren, aber das ist für diese Diskussion nicht relevant). Nehmen wir an, wir möchten die Beziehung zwischen Person und Freund etwas spezifischer gestalten. Wir haben Gruppen von Freunden, die eine bestimmte gemeinsame Quelle haben, wie Verwandte, Schulkameraden oder irgendeine andere Art.

public class Person {
  private Karte<friendSource,Liste>  Freunde;
}

Die Sammlung hat sich von einer einfachen Sammlung in eine Sammlung von Sammlungen verwandelt. Die Beziehung hat sich nicht notwendigerweise von eins-zu-viele zu viele-zu-viele geändert. Ich habe mich dafür entschieden, die Beziehung in der ersten Situation als eins-zu-viele zu modellieren (es ist ziemlich seltsam zu denken, dass ein Freund nur Ihr Freund ist!), also können wir das auch so belassen. Es gäbe eine sehr einfache Änderung im Tabellenlayout, die dieser neuen Situation Rechnung tragen könnte:

Person
ID
1
Freund
IDPERSON_FKPOSQUELLE
110CLASS_MATE
211CLASS_MATE
310RELATIV

Nur die Tabelle Friend hat sich geändert. Die Spalte SOURCE bestimmt den Schlüssel für die Map und die Spalte POS bestimmt den Index in der Liste. Der eindeutige Schlüssel der Tabelle Friend hat sich jedoch geändert. Es gibt keine Möglichkeit, Hibernate für diese Situation zu konfigurieren. Das Mapping müsste sowohl einen Map-Schlüssel als auch ein Indexelement zulassen, aber das tut es nicht. Die einzige Möglichkeit für Hibernate, mit dieser Situation umzugehen, ist die Einführung einer neuen Entität, FriendGroup.

public class Person {
  private Kartengruppen;
}
public class FriendGroup {
  private Liste Freunde;
}
  ...


        <key column="person_fk"

    

... ...

Person
ID
1
FriendGroup
IDPERSON_FKQUELLE
11CLASS_MATE
21RELATIV
Freund
IDGRUPPE_FKPOS
110
211
320

Die neue Entität wird auf eine neue Tabelle abgebildet. Der Grund dafür ist, dass eine Entität eine Identität hat (PK-Spalte in der Datenbank und id-Feld im Code). Eine Sammlung hat keine Identität, gehört aber zu einer Entität. Die Elemente einer Sammlung haben einen Fremdschlüssel zum Primärschlüssel (Identität) des Eigentümers der Sammlung. Bei einer Sammlung von Sammlungen haben die Elemente in der endgültigen Sammlung keine Identität, auf die sie verweisen können, da die Sammlung nicht zu einer Entität gehört, sondern zu einer anderen Sammlung. Durch die Einführung einer neuen Entität, die im Grunde ein Identifikator mit einer Sammlung ist, kehren wir zu der einfachen Situation zurück. Das Gleiche gilt für Komponenten. Eine Komponente hat keine Identität und kann daher keine Sammlung enthalten, da es keine Identität gibt, auf die man verweisen könnte. Die Einführung des Konzepts der rekursiven Sammlungen (Sammlungen von Sammlungen) in Hibernate ist nicht ganz einfach. Es berührt einen großen Teil der Bibliothek. Es gibt einen Fall, in dem das Problem mit einfachen Mitteln gelöst werden kann, nämlich im Fall einer Map of Sets.

Karte der Sets

Nehmen wir an, wir haben Personen und Aufträge und eine Person hat viele Aufträge und jeder Auftrag hat einen bestimmten PaymentStatus.

public class Person {
  private Karte<paymentStatus,Setzen>  ordersByPaymentStatus;
}

Die Verwendung des obigen Codes wäre mit Hibernate immer noch ziemlich schwierig, also wenden wir uns an die Apache commons-collections Bibliothek für eine praktische Klasse

public class Person {
  private MultiMap ordersByPaymentStatus;
}

N.B.: Wir müssen die generischen Parameter der Schnittstelle weglassen. Die MultiMap erweitert die allgemeine Map-Schnittstelle, aber nicht die generische Map-Schnittstelle. Die put-Methode erlaubt einfache Werte, aber die get-Methode gibt eine Sammlung (Liste) dieser Werte zurück. Die MultiMap verwaltet eine Liste von Elementen (Werten) für jeden Schlüssel, aber wir kümmern uns nicht um die Reihenfolge in diesen Listen und interagieren mit ihnen wie mit einfachen java.util.Collections. Da sich die Implementierung der Sammlung, die wir verwenden möchten (MultiHashMap), von der normalen Map-Implementierung unterscheidet, die Hibernate verwendet (HashMap), müssen wir einen UserCollectionType erstellen. Dies ist eine der Schnittstellen, die Hibernate für Erweiterungen bereitstellt. Diese Schnittstelle beschreibt im Wesentlichen, wie Hibernate mit jeder Collection-Implementierung interagieren wird. Die Implementierung dieses UserCollectionType ist ziemlich einfach.

public class MultiMapType implements UserCollectionType {
  public boolean contains(Object collection, Object entity) {
  return ((MultiMap) collection).containsValue(entity);
  }
  public Iterator getElementsIterator(Object collection) {
  return ((MultiMap) collection).values().iterator();
  }
  public Object indexOf(Object collection, Object entity) {
  for (Iterator i = ((MultiMap) collection).entrySet().iterator(); i.hasNext();) {
  Map.Entry entry = (Map.Entry) i.next();
  Collection value = (Collection) entry.getValue();
  if (Wert.enthält(Entität)) {
  return entry.getKey();
  }
  }
  null zurückgeben;
  }
  public Object instantiate() {
  return new MultiHashMap();
  }
  public PersistentCollection instantiate(SessionImplementor session, CollectionPersister persister) throws HibernateException {
  return new PersistentMultiMap(session);
  }
  public PersistentCollection wrap(SessionImplementor session, Object collection) {
  return new PersistentMultiMap(session, (MultiMap) collection);
  }
  public Object replaceElements(Object original, Object target, CollectionPersister persister, Object owner, Map copyCache, SessionImplementor session) throws HibernateException {
  MultiMap Ergebnis = (MultiMap) Ziel;
  result.clear();
  Iterator iter = ((java.util.Map) original ).entrySet().iterator();
  while ( iter.hasNext() ) {
  java.util.Map.Entry me = (java.util.Map.Entry) iter.next();
  Object key = persister.getIndexType().replace( me.getKey(), null, session, owner, copyCache );
  Collection collection = (Collection) me.getValue();
  for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
  Object value = persister.getElementType().replace( iterator.next(), null, session, owner, copyCache );
  result.put(Schlüssel, Wert);
  }
  }
  Ergebnis zurückgeben;
  }
}

Die ersten beiden Methoden sind sehr einfach und werden wahrscheinlich bei jeder Implementierung einer Sammlung gleich sein. Beachten Sie, dass die Methode values() von MultiHashMap die Sammlung glättet und einen Iterator zurückgibt, der über alle Werte in den Listen iteriert. Damit eine MultiMap die Methode indexOf(...) implementieren kann, müssen wir die mit dem Schlüssel verknüpfte Liste daraufhin untersuchen, ob sie das angegebene Element enthält. Die Methoden instantiate(...) und wrap(...) sind miteinander verwandt. Die parameterlose Methode instantiate() muss eine Implementierung des nicht-persistenten Sammlungstyps zurückgeben, in unserem Fall MultiHashMap. Die zweite instantiate-Methode muss eine PersistentCollection zurückgeben. Dies ist eine spezialisierte Hibernate-Sammlung, die weiß, wie man Sammlungen dieses Typs persistent macht. Wir werden später noch genauer darauf zurückkommen. Die Methode wrap(...) wird von Hibernate verwendet, um eine nicht-persistente Sammlung in eine persistente Sammlung umzuwandeln. Der Unterschied zwischen den letzten beiden Methoden besteht darin, dass die gewrappte Sammlung durch die übergebene Sammlung gesichert und daher bereits initialisiert ist, während die instanziierte Sammlung zum Laden einer neuen Sammlung verwendet wird, die möglicherweise träge und daher noch nicht initialisiert ist. Die Methode replaceElements(...) kopiert den Inhalt des Originals in das Ziel, wobei die Typen für key und value zum Kopieren dieser Objekte verwendet werden. Diese Implementierung muss sowohl die Map als auch die Sammlungen, die Werte in der Map sind, iterieren, um jeden Schlüssel und Wert der Reihe nach zu kopieren. Das Persistieren einer MultiMap ist dem Persistieren einer normalen Map sehr ähnlich. Der einzige Unterschied besteht darin, dass die Werte in der MultiMap Sammlungen sind. Hibernate kann keine Sammlungen als Werte verarbeiten, daher müssen wir eine spezielle PersistentMultiMap implementieren. Die Quellen für beide hier beschriebenen Klassen finden Sie als Anhang zu diesem Blog.

public class PersistentMultiMap extends PersistentMap implements MultiMap {
{
  public PersistentMultiMap(SessionImplementor session, MultiMap map) {
  super(session, map);
  }
  public PersistentMultiMap(SessionImplementor session) {
  super(session);
  }

Die PersistentMultiMap erweitert die PersistentMap, da ihr Verhalten dem der Map sehr ähnlich ist. Sie implementiert auch die MultiMap-Schnittstelle, so dass sie MultiMap-Sammlungen in Entitätsobjekten ersetzen kann. Aufgrund dieser Schnittstelle müssen wir eine zusätzliche Methode implementieren:

  public Object remove(Object key, Object item) {
  Object old = isPutQueueEnabled() ? readElementByIndex(key) : UNKNOWN; 
  if (alt == UNKNOWN) {
  write();
  return ((MultiMap) map).remove(key, item);
  } sonst {
  queueOperation(new RemoveItem(key, item));
  alt zurückgeben;
  }
  }
  private class RemoveItem implements DelayedOperation {
  private final Object key;
  private final Object item;
  private RemoveItem(Object key, Object item) {
  this.key = key;
  this.item = item;
  }
  public Object getAddedInstance() {
  null zurückgeben;
  }
  public Object getOrphan() {
  Rückgabeartikel;
  }
  public void operate() {
  ((MultiMap) map).remove(key, item);
  }
  }

Die Methode remove(Object key, Object item) ist die einzige Ergänzung der MultiMap zur Map-Schnittstelle. Sie entfernt das Element aus der Sammlung, die durch den Schlüssel verschlüsselt ist. Die Implementierung ist größtenteils von einer der Sammlungsoperationen in den PersistentCollections übernommen. Der DelayedOperation-Mechanismus wird von Hibernate verwendet, um Operationen auf nicht vollständig initialisierte Sammlungen zu unterstützen. Eine der wichtigsten Änderungen am Verhalten der PersistentMap ist die Methode entries(...). Diese Methode wird von Hibernate verwendet, um alle Elemente (sowohl Schlüssel als auch Wert) zu iterieren. Im Falle einer MultiMap müssen wir einen Iterator bereitstellen, der alle Werte in den Sammlungen in der MultiMap zurückgibt, zusammen mit dem Schlüssel, an den die Sammlung als Map.Entry gebunden ist. Zu diesem Zweck führen wir eine neue Implementierung der Iterator-Schnittstelle ein.

  @Override
  public Iterator entries(CollectionPersister persister) {
  return new KeyValueCollectionIterator(super.entries(persister));
  }
  private final static class KeyValueCollectionIterator implements Iterator {
  private final Iterator parent;
  private Object key;
  private Iterator current;
  private KeyValueCollectionIterator(Iterator parent) {
  this.parent = parent;
  move();
  }
  public boolean hasNext() {
  return key != null;
  }
  public Object next() {
  if (Schlüssel == null) {
  throw new NoSuchElementException();
  } sonst {
  DefaultMapEntry result = new DefaultMapEntry(key, current.next());
  if (!current.hasNext()) {
  move();
  }
  Ergebnis zurückgeben;
  }
  }
  private void move() {
  while (this.parent.hasNext()) {
  Map.Entry entry = (Entry) this.parent.next();
  key = entry.getKey();
  current = ((Collection) entry.getValue()).iterator();
  if (current.hasNext()) {
  zurück;
  }
  }
  Schlüssel = Null;
  }
  public void remove() {
  throw new UnsupportedOperationException();
  }
  }

Der Rest der Klasse befasst sich mit Dirty Checking. Zur Durchführung von Dirty Checks und Lazy Updates speichert Hibernate einen Snapshot aller Entitäten und Sammlungen, die aus der Datenbank in den Speicher geladen werden. Zum Flush-Zeitpunkt wird der aktuelle Zustand der Objekte im Speicher mit dem Snapshot-Status verglichen und die Unterschiede werden in der Datenbank festgehalten. Der Snapshot wird mit der Methode getSnapshot(CollectionPersister persister) erstellt. Bei der Erstellung eines Snapshots müssen von allen Werten in der Sammlung tiefe Kopien erstellt werden. Der Snapshot ist eine weitere MultiMap.

  @Override
  public Serializable getSnapshot(CollectionPersister persister) throws HibernateException {
  EntityMode entityMode = getSession().getEntityMode();
  MultiHashMap clonedMap = new MultiHashMap(map.size());
  Iterator iter = map.entrySet().iterator();
  while (iter.hasNext()) {
  Map.Entry e = (Map.Entry) iter.next();
  Collection collection = (Collection) e.getValue();
  for (Iterator i = collection.iterator(); i.hasNext();) {
  final Object copy = persister.getElementType().deepCopy(i.next(), entityMode, persister.getFactory());
  clonedMap.put(e.getKey(), copy);
  }
  }
  return clonedMap;
  }

Der Snapshot wird in der Sitzung gehalten und kann über die Methode getSnapshot() abgerufen werden. Die Methoden equalsSnapshot(...), getDeletes(...), needsInserting(...) und needsUpdating(...) werden verwendet, um die Unterschiede zu prüfen. Die Methode equalsSnapshot(...) wird als Abkürzung verwendet, um festzustellen, ob eine weitere Prüfung auf Unreinheiten erforderlich ist. Die Methode getDeletes(...) muss einen Iterator über entweder die Schlüssel oder die Werte zurückgeben, die aus der Sammlung entfernt werden. Diese Werte werden nicht unbedingt aus der Datenbank entfernt, aber die Beziehung zwischen den beiden Entitäten (Eigentümer der Sammlung und Wert) wird entfernt. Beachten Sie, dass in dieser Implementierung auch Elemente zurückgegeben werden, die in der Map verschoben wurden. Eine Verschiebung wird von dieser Implementierung als Entfernen und Einfügen betrachtet (dies ist auch bei der Standardimplementierung der PersistentMap von Hibernate der Fall). Die Methoden needsInserting(...) und needsUpdating(...) prüfen den Snapshot für die Sammlung, die mit demselben Schlüssel wie der zu prüfende Eintrag verbunden ist. Die Implementierung beruht auf einer korrekt implementierten equals(...)- und hashCode()-Methode für den Entitätstyp (wie viele andere Hibernate-Codes auch).

  @Override
  public boolean equalsSnapshot(CollectionPersister persister) throws HibernateException {
  Map sn = (Map) getSnapshot();
  if (sn.size() != map.size())
  return false;
  Type elemType = persister.getElementType();
  for (Iterator i = sn.entrySet().iterator(); i.hasNext();) {
  Map.Entry entry = (Map.Entry) i.next();
  Map oldState = getCollectionAsIdentityMap((Collection) entry.getValue());
  Collection newState = (Collection) map.get(entry.getKey());
  for (Iterator iter = newState.iterator(); iter.hasNext();) {
  Object newValue = iter.next();
  Object oldValue = oldState.get(newValue);
  if (newValue != null && oldValue != null && elemType.isDirty(oldValue, newValue, getSession())) {
  return false;
  }
  }
  }
  true zurückgeben;
  }
  private Map getCollectionAsIdentityMap(Collection collection) {
  Map map = new HashMap();
  for (Iterator iter = collection.iterator(); iter.hasNext();) {
  Objekt element = iter.next();
  map.put(element, element);
  }
  return map;
  }
  @Override
  public Iterator getDeletes(CollectionPersister persister, boolean indexIsFormula) throws HibernateException {
  Set result = new HashSet();
  Map sn = (Map) getSnapshot();
  for (Iterator i = sn.entrySet().iterator(); i.hasNext();) {
  Map.Entry entry = (Entry) i.next();
  Collection oldState = (Collection) entry.getValue();
  Collection newState = (Collection) map.get(entry.getKey());
  for (Iterator j = oldState.iterator(); j.hasNext();) {
  Objekt element = j.next();
  if (!(newState.contains(element))) {
  result.add(element);
  }
  }
  }
  return result.iterator();
  }
  @Override
  public boolean needsInserting(Object entry, int i, Type elemType) throws HibernateException {
  Map.Entry e = (Entry) entry;
  Map sn = (Map) getSnapshot();
  Collection oldState = (Collection) sn.get(e.getKey());
  return oldState == null || !oldState.contains(e.getValue());
  }
  @Override
  public boolean needsUpdating(Object entry, int i, Type elemType) throws HibernateException {
  Map.Entry e = (Entry) entry;
  Map sn = (Map) getSnapshot();
  Collection collection = (Collection) sn.get(e.getKey());
  if (Sammlung == null) {
  return false;
  }
  for (Iterator iter = collection.iterator(); iter.hasNext();) {
  Object oldValue = iter.next();
  if (oldValue != null && oldValue.equals(e.getValue())) {
  return e.getValue() != null && elemType.isDirty(oldValue, e.getValue(), getSession());
  }
  }
  return false;
  }
}

Jetzt können Sie diesen Code einfach verwenden, indem Sie Ihrer Map in der Hibernate-Zuordnung ein Attribut vom Typ collection hinzufügen.

  
  ...



    

...

Jetzt wird die Map mit dem Datenbankstatus synchronisiert. Dieser Code funktioniert auch für Many-to-Many-Assoziationen oder Value Maps. Leider erzeugt die HBM2DDL-Funktion in Hibernate in diesen Fällen eine eindeutige Einschränkung, die verhindert, dass die Map mehrere Werte für denselben Schlüssel enthält. Wenn Sie HBM2DDL nicht verwenden, sondern Ihr Schema manuell schreiben, wird der Code für Sie funktionieren. Der Grund, warum Hibernate diese eindeutige Einschränkung für One-to-Many-Maps nicht generiert, liegt darin, dass sich die Fremdschlüsselspalte für eine One-to-Many normalerweise in der Spalte der untergeordneten Entität befindet. Da in der Datenbank möglicherweise Kinder ohne Elternteil vorhanden sind, könnte die eindeutige Einschränkung im Fall von mehreren Nullen Probleme bereiten.

Fazit

Hibernate kann verwendet werden, um MultiMaps zu persistieren. Dies erfordert etwas benutzerdefinierten Code und ein gutes Verständnis dafür, wie Hibernate erweitert werden kann. Die Erweiterung von Hibernate für die Arbeit mit anderen Arten von rekursiven Sammlungen könnte sich als wesentlich schwieriger erweisen.

Verfasst von

Maarten Winkels

Contact

Let’s discuss how we can support your journey.