diff --git a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java index a9a856bed..bbe3afdc9 100644 --- a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java +++ b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java @@ -1053,6 +1053,14 @@ public interface DatabaseBuilder { @Deprecated DatabaseBuilder setSkipDataSourceCheck(boolean skipDataSourceCheck); + /** + * Set to true if this database is used in a read only way. + *
+ * The DataSource and read-only DataSource are expected to be the same + * and use readOnly=true and autoCommit=true. + */ + DatabaseBuilder readOnlyDatabase(boolean readOnlyDatabase); + /** * Set a DataSource. */ @@ -2607,6 +2615,14 @@ public interface DatabaseBuilder { */ boolean skipDataSourceCheck(); + /** + * Return true if this database is used in a read only way. + *
+ * The DataSource and read-only DataSource are expected to be the same
+ * and use readOnly=true and autoCommit=true.
+ */
+ boolean readOnlyDatabase();
+
/**
* Return the DataSource.
*/
diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
index 30d72e55e..eb5e25b14 100644
--- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
+++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java
@@ -304,6 +304,8 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
private boolean skipDataSourceCheck;
+ private boolean readOnlyDatabase;
+
/**
* The data source (if programmatically provided).
*/
@@ -1343,6 +1345,17 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
return this;
}
+ @Override
+ public DatabaseBuilder readOnlyDatabase(boolean readOnlyDatabase) {
+ this.readOnlyDatabase = readOnlyDatabase;
+ return this;
+ }
+
+ @Override
+ public boolean readOnlyDatabase() {
+ return readOnlyDatabase;
+ }
+
@Override
public DataSource getDataSource() {
return dataSource;
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InitDataSource.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InitDataSource.java
index c5fead0da..2c994a0c3 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InitDataSource.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InitDataSource.java
@@ -38,15 +38,19 @@ final class InitDataSource {
* Initialise the "main" read write DataSource from configuration.
*/
private DataSource initDataSource() {
- return createFromConfig(config.getDataSourceConfig(), false);
+ return createFromConfig(config.getDataSourceConfig(), config.readOnlyDatabase(), false);
}
/**
* Initialise the "read only" DataSource from configuration.
*/
private DataSource initReadOnlyDataSource() {
+ if (config.readOnlyDatabase()) {
+ // using the same DataSource instance
+ return config.getDataSource();
+ }
var roConfig = readOnlyConfig();
- return roConfig == null ? null : createFromConfig(roConfig, true);
+ return roConfig == null ? null : createFromConfig(roConfig, false, true);
}
DataSourceBuilder.Settings readOnlyConfig() {
@@ -76,20 +80,21 @@ final class InitDataSource {
return url != null && !"none".equalsIgnoreCase(url) && !url.trim().isEmpty();
}
- private DataSource createFromConfig(DataSourceBuilder.Settings dsConfig, boolean readOnly) {
+ private DataSource createFromConfig(DataSourceBuilder.Settings dsConfig, boolean readOnlyDB, boolean readOnly) {
if (dsConfig == null) {
throw new PersistenceException("No DataSourceBuilder defined for " + config.getName());
}
- if (dsConfig.isOffline()) {
- if (config.getDatabasePlatformName() == null) {
- throw new PersistenceException("You MUST specify a DatabasePlatformName on DatabaseConfig when offline");
- }
+ if (dsConfig.isOffline() && config.getDatabasePlatformName() == null) {
+ throw new PersistenceException("You MUST specify a DatabasePlatformName on DatabaseConfig when offline");
}
attachAlert(dsConfig);
attachListener(dsConfig);
- if (readOnly) {
+ if (readOnlyDB) {
+ dsConfig.autoCommit(true);
+ dsConfig.readOnly(true);
+ } else if (readOnly) {
// setup to use AutoCommit such that we skip explicit commit
var mainSettings = config.getDataSourceConfig();
dsConfig.autoCommit(true);
diff --git a/ebean-test/src/test/java/org/tests/readonly/PostgresReadOnlyDatabaseTest.java b/ebean-test/src/test/java/org/tests/readonly/PostgresReadOnlyDatabaseTest.java
new file mode 100644
index 000000000..c33a97738
--- /dev/null
+++ b/ebean-test/src/test/java/org/tests/readonly/PostgresReadOnlyDatabaseTest.java
@@ -0,0 +1,119 @@
+package org.tests.readonly;
+
+import io.ebean.Database;
+import io.ebean.DatabaseBuilder;
+import io.ebean.Transaction;
+import io.ebean.annotation.Transactional;
+import io.ebean.datasource.DataSourceBuilder;
+import io.ebean.test.containers.PostgresContainer;
+import io.ebeaninternal.api.SpiTransaction;
+import jakarta.persistence.PersistenceException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.tests.model.basic.UTDetail;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class PostgresReadOnlyDatabaseTest {
+
+ private static Database readDb;
+
+ @BeforeAll
+ static void beforeAll() {
+ readDb = setupReadOnlyDatabase();
+ }
+
+ @Test
+ void insertPreventedInReadOnlyTransaction() {
+ assertThatThrownBy(() -> {
+ try (Transaction transaction = readDb.beginTransaction()) {
+ UTDetail u0 = new UTDetail("u0", 45, 3D);
+ readDb.save(u0);
+ }
+ }).isInstanceOf(PersistenceException.class)
+ .hasMessageContaining("ERROR: cannot execute INSERT in a read-only transaction");
+ }
+
+ @Test
+ void queryInExplicitTransaction() {
+ try (Transaction transaction = readDb.beginTransaction()) {
+ List