~/home of geeks

JPA Collection Batch Updates

· 298 Wörter · 2 Minute(n) Lesedauer

hall full of ice figures, sci-fi

Manchmal hat man eine Menge von IDs, deren zugehörige Datensätze man aktualisieren möchte. Je nach Größe der Menge kann man Probleme bekommen, aber auch Lösungen finden.

Meine Methode erhält eine Collection von Primary Keys und soll bei allen zugehörigen Datensätzen ein Wert setzen: public void updateData(Collection<Integer> idsToUpdate) Die Collection konnte bis zu 10.000 Einträge enthalten.

Der erste Ansatz war es, jeden Datensatz als Objekt zu laden, ändern und speichern:

public void updateData(Collection<Integer> idsToUpdate) {
  final EntityManager entityManager = getEntityManager();
  for (final int id : idsToUpdate) {
    MyObject myObject = entityManager.find(MyObject.class, id);
    myObject.setMyField("newValue");
    entityManager.merge(myObject);
  }
}

Funktioniert wunderbar, ist aber auch sehr langsam (und damit auch Ressourcen fressend).

Also, dachte ich mir, kann ich das doch auch direkt auf der Datenbank machen, mit einem einzigen Update und einem IN-Statement:

public void updateData(Collection<Integer> idsToUpdate) {
  final EntityManager entityManager = getEntityManager();
  final Query query = entityManager.createQuery("UPDATE MyObject m SET m.myField='newValue' WHERE m.id IN :ids");
  query.setParameter("ids", ids);
  query.executeUpdate();
}

So schlank und so schnell! Aber auch mit einem Haken: Die zulässige Gesamtgröße für ein IN-Statement ist datenbankspezifisch und limitiert.

So darf man bei Oracle nicht mehr als 1000 Ausdrücke in einem IN-Statament haben. Bei MySQL ist die maximale Anzahl an Ausdrücken durch den Wert der Umgebungsvariable max_allowed_packet bestimmt. Dieser hat in einigen Initialeinstellungen den Wert 1 MB.

Also habe ich das Update in Batches a 100 Stück aufgeteilt:

public void updateData(Collection<Integer> idsToUpdate) {
  final EntityManager entityManager = getEntityManager();
  List<String> todoIds = new ArrayList<>(idsToUpdate);
  while (!todoIds.isEmpty()) {
    int amount = Math.min(100, todoIds.size());
    List<Integer> actualIds = todoIds.subList(0, amount);
    Query query = entityManager.createQuery("UPDATE MyObject m SET m.myField='newValue' WHERE m.id IN :ids");
    query.setParameter("ids", ids);
    query.executeUpdate();
    todoIds = todoIds.subList(amount, todoIds.size());
  }
}

Das klappte dann wunderbar, auch für 10.000 Einträge und war dabei auch noch sehr schnell. Natürlich kann man die Batchgröße auch noch etwas höher setzen.