mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
Add read only database configuration/builder option
When using DatabaseBuilder.readOnlyDatabase(true) then ebean will: - Set the DataSourceBuilder to use autoCommit=true and readOnly=true - Use the same DataSource instance for both dataSource and readOnlyDataSource This is to simplify the setup/configuration for creating a Database that will only have read-only use. Note that readOnly=true is a JDBC hint and for example H2 database effectively ignores that hint where as Postgres will enforce the read-only true nature.
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* The DataSource and read-only DataSource are expected to be the same
|
||||
* and use readOnly=true and autoCommit=true.
|
||||
*/
|
||||
boolean readOnlyDatabase();
|
||||
|
||||
/**
|
||||
* Return the DataSource.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<UTDetail> list = readDb.find(UTDetail.class).findList();
|
||||
assertThat(list).hasSize(2);
|
||||
assertThat(transaction.isReadOnly()).isTrue();
|
||||
transaction.commit(); // autoCommit=true but this is still allowed
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryInImplicitTransaction() {
|
||||
List<UTDetail> list = readDb.find(UTDetail.class).findList();
|
||||
assertThat(list).hasSize(2);
|
||||
}
|
||||
|
||||
// Requires .register(true).defaultDatabase(true) ... when running this test
|
||||
@Disabled
|
||||
@Test
|
||||
void queryInReadOnlyTransactional() {
|
||||
readOnlyTransactional();
|
||||
transactionalOnlyContainingQueries();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
void readOnlyTransactional() {
|
||||
List<UTDetail> list = readDb.find(UTDetail.class).findList();
|
||||
assertThat(list).hasSize(2);
|
||||
|
||||
SpiTransaction current = (SpiTransaction) Transaction.current();
|
||||
assertThat(current.isReadOnly()).isTrue();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
void transactionalOnlyContainingQueries() {
|
||||
List<UTDetail> list = readDb.find(UTDetail.class).findList();
|
||||
assertThat(list).hasSize(2);
|
||||
|
||||
SpiTransaction current = (SpiTransaction) Transaction.current();
|
||||
assertThat(current.isReadOnly()).isTrue();
|
||||
}
|
||||
|
||||
private static Database setupReadOnlyDatabase() {
|
||||
PostgresContainer.builder("15")
|
||||
.dbName("readonly_test")
|
||||
.build()
|
||||
.start();
|
||||
|
||||
var dataSourceBuilder = DataSourceBuilder.create()
|
||||
.username("readonly_test")
|
||||
.password("test")
|
||||
.url("jdbc:postgresql://localhost:6432/readonly_test");
|
||||
|
||||
var writeDb = databaseBuilder(dataSourceBuilder)
|
||||
.ddlGenerate(true)
|
||||
.ddlRun(true)
|
||||
.build();
|
||||
|
||||
writeDb.truncate(UTDetail.class);
|
||||
writeDb.save(new UTDetail("u0", 45, 3D));
|
||||
writeDb.save(new UTDetail("u1", 42, 5D));
|
||||
|
||||
return databaseBuilder(dataSourceBuilder)
|
||||
.readOnlyDatabase(true)
|
||||
// register + default required for the tests using @Transactional
|
||||
// .register(true).defaultDatabase(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DatabaseBuilder databaseBuilder(DataSourceBuilder dataSourceBuilder) {
|
||||
return Database.builder()
|
||||
.name("ro_test")
|
||||
.dataSourceBuilder(dataSourceBuilder)
|
||||
.ddlExtra(false)
|
||||
.defaultDatabase(false)
|
||||
.register(false)
|
||||
.addClass(UTDetail.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user