Skip to content

Commit 20e599f

Browse files
Refactor ExportPipeline to improve code clarity and performance
- Added EntityGraph import for better JPA performance - Updated comments for clarity and consistency - Changed lastId handling to preserve exact ID types - Improved ZIP output handling to ensure data integrity
1 parent a5d3157 commit 20e599f

1 file changed

Lines changed: 44 additions & 20 deletions

File tree

  • extensions/saas/sources/migration/src/main/java/tools/dynamia/modules/saas/migration/pipeline

extensions/saas/sources/migration/src/main/java/tools/dynamia/modules/saas/migration/pipeline/ExportPipeline.java

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111
package tools.dynamia.modules.saas.migration.pipeline;
1212

13+
import jakarta.persistence.EntityGraph;
1314
import jakarta.persistence.EntityManager;
1415
import jakarta.persistence.EntityManagerFactory;
1516
import jakarta.persistence.metamodel.Attribute.PersistentAttributeType;
@@ -46,7 +47,6 @@
4647
import java.time.LocalDateTime;
4748
import java.util.ArrayList;
4849
import java.util.Collections;
49-
import java.util.LinkedHashMap;
5050
import java.util.List;
5151
import java.util.Map;
5252
import java.util.concurrent.ConcurrentHashMap;
@@ -100,17 +100,21 @@ public class ExportPipeline {
100100

101101
private static final LoggingService logger = LoggingService.get(ExportPipeline.class);
102102

103-
/** ZIP compression level — BEST_SPEED gives good ratio for JSON with minimal CPU overhead. */
103+
/**
104+
* ZIP compression level — BEST_SPEED gives good ratio for JSON with minimal CPU overhead.
105+
*/
104106
private static final int ZIP_LEVEL = Deflater.BEST_SPEED;
105107

106-
/** ZIP output buffer size. */
108+
/**
109+
* ZIP output buffer size.
110+
*/
107111
private static final int ZIP_BUFFER_SIZE = 256 * 1024;
108112

109-
/** Per-entity file write buffer size. */
113+
/**
114+
* Per-entity file write buffer size.
115+
*/
110116
private static final int ENTITY_BUFFER_SIZE = 64 * 1024;
111-
112-
/** Standard Jakarta Persistence fetch-graph hint key. */
113-
private static final String HINT_FETCH_GRAPH = "jakarta.persistence.fetchgraph";
117+
public static final String JAKARTA_PERSISTENCE_FETCHGRAPH = "jakarta.persistence.fetchgraph";
114118

115119
/**
116120
* Column definitions cached per entity class; built once on first export,
@@ -378,12 +382,14 @@ private void writeEmptyEntityFile(Path filePath, Class<?> entityClass) throws IO
378382
}
379383

380384
/**
381-
* Pages through all rows for {@code entityClass} using keyset pagination
382-
* ({@code id > lastId}) and writes each row to {@code gen}.
385+
* Pages through all rows for {@code entityClass} using keyset pagination and writes
386+
* each row to {@code gen}.
387+
*
388+
* <p>ID type-agnostic: {@code null} is used as the first-page sentinel so no
389+
* type-specific "zero" value is required. The JPA provider receives the actual
390+
* ID object (Long, UUID, String, …) on subsequent pages and handles coercion.
383391
*
384-
* <p>{@link Account} is skipped — its data lives in the manifest.
385-
* Each parallel task passes its own {@code localEm} so there is no
386-
* cross-thread EntityManager sharing.
392+
* <p>{@link Account} rows are skipped — account data lives in the manifest.
387393
*/
388394
private long writeEntityRows(JsonGenerator gen,
389395
Class<?> entityClass,
@@ -396,28 +402,40 @@ private long writeEntityRows(JsonGenerator gen,
396402
return 0;
397403
}
398404

405+
String simpleName = entityClass.getSimpleName();
399406
int chunkSize = resolveChunkSize(options);
400-
long lastId = 0;
407+
Object lastId = null; // null = first page; avoids assuming ID type
401408
long processed = 0;
402409

410+
EntityGraph<?> emptyGraph = localEm.createEntityGraph(entityClass); //to avoid errors with multiple eagers calls
411+
412+
403413
do {
404414
@SuppressWarnings("unchecked")
405-
List<Object> page = localEm.createQuery(
406-
"SELECT e FROM " + entityClass.getSimpleName() +
415+
List<Object> page = (lastId == null)
416+
? localEm.createQuery(
417+
"SELECT e FROM " + simpleName +
418+
" e WHERE e.accountId = :accountId ORDER BY e.id ASC")
419+
.setParameter("accountId", accountId)
420+
.setMaxResults(chunkSize)
421+
.setHint(JAKARTA_PERSISTENCE_FETCHGRAPH, emptyGraph)
422+
.getResultList()
423+
: localEm.createQuery(
424+
"SELECT e FROM " + simpleName +
407425
" e WHERE e.accountId = :accountId AND e.id > :lastId ORDER BY e.id ASC")
408426
.setParameter("accountId", accountId)
409427
.setParameter("lastId", lastId)
410428
.setMaxResults(chunkSize)
411-
.setHint(HINT_FETCH_GRAPH, localEm.createEntityGraph(entityClass))
429+
.setHint(JAKARTA_PERSISTENCE_FETCHGRAPH, emptyGraph)
412430
.getResultList();
413431

414432
for (Object entity : page) {
415433
if (token != null && token.isCancelled()) break;
416434
if (entity != null) {
417435
writeEntityRow(gen, entity, columns);
418436
Object idVal = JpaUtils.getJPAIdValue(entity);
419-
if (idVal instanceof Number n) {
420-
lastId = n.longValue();
437+
if (idVal != null) {
438+
lastId = idVal; // preserve exact type: Long, UUID, String, …
421439
}
422440
}
423441
processed++;
@@ -443,7 +461,12 @@ private long writeEntityRows(JsonGenerator gen,
443461
private void zipToOutput(Path tempDir, List<Class<?>> ordered, Long accountId,
444462
OutputStream output) throws IOException {
445463

446-
ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(output, ZIP_BUFFER_SIZE));
464+
// Keep reference to the buffer so we can flush it after finish().
465+
// ZipOutputStream.finish() writes the central directory into the BufferedOutputStream
466+
// buffer but does NOT flush it — without the explicit flush() below the last bytes
467+
// never reach `output` and the ZIP is corrupt.
468+
BufferedOutputStream buffered = new BufferedOutputStream(output, ZIP_BUFFER_SIZE);
469+
ZipOutputStream zipOut = new ZipOutputStream(buffered);
447470
zipOut.setLevel(ZIP_LEVEL);
448471

449472
addFileToZip(zipOut, tempDir.resolve(ExportConstants.MANIFEST_FILE), ExportConstants.MANIFEST_FILE);
@@ -456,7 +479,8 @@ private void zipToOutput(Path tempDir, List<Class<?>> ordered, Long accountId,
456479
}
457480
}
458481

459-
zipOut.finish(); // writes ZIP central directory; does not close the caller's stream
482+
zipOut.finish(); // writes ZIP central directory into buffered
483+
buffered.flush(); // pushes buffered bytes to the caller's output stream
460484
}
461485

462486
private static void addFileToZip(ZipOutputStream zipOut, Path file, String entryName)

0 commit comments

Comments
 (0)