From 83cb935e4cb7e0bd4bb2dfbd72199e689518e45e Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Fri, 10 Jul 2026 19:37:55 -0700 Subject: [PATCH] Enable database result streaming --- src/main/java/org/traccar/config/Keys.java | 11 +++++++++- .../org/traccar/storage/DatabaseStorage.java | 2 +- .../org/traccar/storage/QueryBuilder.java | 21 +++++++++++++++++-- .../org/traccar/storage/QueryBuilderTest.java | 6 +++--- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/traccar/config/Keys.java b/src/main/java/org/traccar/config/Keys.java index d2fe05e20..a7ed0e865 100644 --- a/src/main/java/org/traccar/config/Keys.java +++ b/src/main/java/org/traccar/config/Keys.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 - 2025 Anton Tananaev (anton@traccar.org) + * Copyright 2019 - 2026 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -579,6 +579,15 @@ public final class Keys { List.of(KeyType.CONFIG), 20); + /** + * Number of rows fetched per round trip for streamed queries (position history and exports). On PostgreSQL this + * enables a server-side cursor so results are not fully buffered in memory. + */ + public static final ConfigKey DATABASE_STREAM_FETCH_SIZE = new IntegerConfigKey( + "database.streamFetchSize", + List.of(KeyType.CONFIG), + 1000); + /** * SQL query to check connection status. Default value is 'SELECT 1'. For Oracle database you can use * 'SELECT 1 FROM DUAL'. diff --git a/src/main/java/org/traccar/storage/DatabaseStorage.java b/src/main/java/org/traccar/storage/DatabaseStorage.java index b08d413ec..381060dbd 100644 --- a/src/main/java/org/traccar/storage/DatabaseStorage.java +++ b/src/main/java/org/traccar/storage/DatabaseStorage.java @@ -86,7 +86,7 @@ public class DatabaseStorage extends Storage { for (int index = 0; index < values.size(); index++) { builder.setValue(index, values.get(index)); } - Stream stream = builder.executeQueryStreamed(clazz); + Stream stream = builder.executeQueryStreamed(clazz, databaseType); builder = null; return stream; } catch (SQLException e) { diff --git a/src/main/java/org/traccar/storage/QueryBuilder.java b/src/main/java/org/traccar/storage/QueryBuilder.java index 15006e2d0..c23eff783 100644 --- a/src/main/java/org/traccar/storage/QueryBuilder.java +++ b/src/main/java/org/traccar/storage/QueryBuilder.java @@ -59,6 +59,8 @@ public final class QueryBuilder implements AutoCloseable { private final String query; private final boolean returnGeneratedKeys; + private boolean streamingTransaction; + private QueryBuilder( Config config, DataSource dataSource, ObjectMapper objectMapper, String query, boolean returnGeneratedKeys) throws SQLException { @@ -234,11 +236,18 @@ public final class QueryBuilder implements AutoCloseable { } } - public Stream executeQueryStreamed(Class clazz) throws SQLException { + public Stream executeQueryStreamed(Class clazz, String databaseType) throws SQLException { ResultSet resultSet = null; try { logQuery(); + connection.setAutoCommit(false); + streamingTransaction = true; + statement.setFetchSize(switch (databaseType) { + case "MySQL", "MariaDB" -> Integer.MIN_VALUE; + default -> config.getInteger(Keys.DATABASE_STREAM_FETCH_SIZE); + }); + resultSet = statement.executeQuery(); ResultSetMetaData resultMetaData = resultSet.getMetaData(); @@ -311,7 +320,15 @@ public final class QueryBuilder implements AutoCloseable { try { statement.close(); } finally { - connection.close(); + try { + if (streamingTransaction) { + streamingTransaction = false; + connection.rollback(); + connection.setAutoCommit(true); + } + } finally { + connection.close(); + } } } diff --git a/src/test/java/org/traccar/storage/QueryBuilderTest.java b/src/test/java/org/traccar/storage/QueryBuilderTest.java index e01935560..4ea136ff6 100644 --- a/src/test/java/org/traccar/storage/QueryBuilderTest.java +++ b/src/test/java/org/traccar/storage/QueryBuilderTest.java @@ -96,7 +96,7 @@ public class QueryBuilderTest { try (QueryBuilder query = QueryBuilder.create(config, dataSource, objectMapper, "SELECT * FROM test_entity"); - Stream stream = query.executeQueryStreamed(TestEntity.class)) { + Stream stream = query.executeQueryStreamed(TestEntity.class, "H2")) { List results = stream.toList(); assertEquals(1, results.size()); TestEntity entity = results.get(0); @@ -132,7 +132,7 @@ public class QueryBuilderTest { try (QueryBuilder query = QueryBuilder.create(config, dataSource, objectMapper, "SELECT * FROM test_entity"); - Stream stream = query.executeQueryStreamed(TestEntity.class)) { + Stream stream = query.executeQueryStreamed(TestEntity.class, "H2")) { List results = stream.toList(); assertEquals(1, results.size()); TestEntity loaded = results.get(0); @@ -161,7 +161,7 @@ public class QueryBuilderTest { try (QueryBuilder query = QueryBuilder.create(config, dataSource, objectMapper, "SELECT * FROM test_entity ORDER BY count"); - Stream stream = query.executeQueryStreamed(TestEntity.class)) { + Stream stream = query.executeQueryStreamed(TestEntity.class, "H2")) { List results = stream.toList(); assertEquals(3, results.size()); assertEquals("row0", results.get(0).getName());