Skip to content

Commit 82aad7c

Browse files
committed
#141 - Add support for schema initialization.
We now provide DatabasePopulator and ScriptUtils to run SQL scripts using R2DBC Connections to initialize and clean up databases.
1 parent a21b403 commit 82aad7c

33 files changed

+2086
-1
lines changed

src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ protected Mono<Void> doCleanupAfterCompletion(TransactionSynchronizationManager
421421
* Prepare the transactional {@link Connection} right after transaction begin.
422422
* <p>
423423
* The default implementation executes a "SET TRANSACTION READ ONLY" statement if the {@link #setEnforceReadOnly
424-
* "enforceReadOnly"} flag is set to {@code true} and the transaction definition indicates a read-only transaction.
424+
* "enforceReadOnly"} flag is set to {@literal true} and the transaction definition indicates a read-only transaction.
425425
* <p>
426426
* The "SET TRANSACTION READ ONLY" is understood by Oracle, MySQL and Postgres and may work with other databases as
427427
* well. If you'd like to adapt this treatment, override this method accordingly.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.connectionfactory.init;
17+
18+
import org.springframework.core.io.support.EncodedResource;
19+
20+
/**
21+
* Thrown by {@link ScriptUtils} if an SQL script cannot be read.
22+
*
23+
* @author Mark Paluch
24+
*/
25+
public class CannotReadScriptException extends ScriptException {
26+
27+
private static final long serialVersionUID = 7253084944991764250L;
28+
29+
/**
30+
* Creates a new {@link CannotReadScriptException}.
31+
*
32+
* @param resource the resource that cannot be read from.
33+
* @param cause the underlying cause of the resource access failure.
34+
*/
35+
public CannotReadScriptException(EncodedResource resource, Throwable cause) {
36+
super("Cannot read SQL script from " + resource, cause);
37+
}
38+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.connectionfactory.init;
17+
18+
import io.r2dbc.spi.Connection;
19+
import reactor.core.publisher.Flux;
20+
import reactor.core.publisher.Mono;
21+
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.Collection;
25+
import java.util.List;
26+
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* Composite {@link DatabasePopulator} that delegates to a list of given {@link DatabasePopulator} implementations,
31+
* executing all scripts.
32+
*
33+
* @author Mark Paluch
34+
*/
35+
public class CompositeDatabasePopulator implements DatabasePopulator {
36+
37+
private final List<DatabasePopulator> populators = new ArrayList<>(4);
38+
39+
/**
40+
* Creates an empty {@link CompositeDatabasePopulator}.
41+
*
42+
* @see #setPopulators
43+
* @see #addPopulators
44+
*/
45+
public CompositeDatabasePopulator() {}
46+
47+
/**
48+
* Creates a {@link CompositeDatabasePopulator}. with the given populators.
49+
*
50+
* @param populators one or more populators to delegate to.
51+
*/
52+
public CompositeDatabasePopulator(Collection<DatabasePopulator> populators) {
53+
54+
Assert.notNull(populators, "Collection of DatabasePopulator must not be null!");
55+
56+
this.populators.addAll(populators);
57+
}
58+
59+
/**
60+
* Creates a {@link CompositeDatabasePopulator} with the given populators.
61+
*
62+
* @param populators one or more populators to delegate to.
63+
*/
64+
public CompositeDatabasePopulator(DatabasePopulator... populators) {
65+
66+
Assert.notNull(populators, "DatabasePopulators must not be null!");
67+
68+
this.populators.addAll(Arrays.asList(populators));
69+
}
70+
71+
/**
72+
* Specify one or more populators to delegate to.
73+
*/
74+
public void setPopulators(DatabasePopulator... populators) {
75+
76+
Assert.notNull(populators, "DatabasePopulators must not be null!");
77+
78+
this.populators.clear();
79+
this.populators.addAll(Arrays.asList(populators));
80+
}
81+
82+
/**
83+
* Add one or more populators to the list of delegates.
84+
*/
85+
public void addPopulators(DatabasePopulator... populators) {
86+
87+
Assert.notNull(populators, "DatabasePopulators must not be null!");
88+
89+
this.populators.addAll(Arrays.asList(populators));
90+
}
91+
92+
@Override
93+
public Mono<Void> populate(Connection connection) throws ScriptException {
94+
95+
Assert.notNull(connection, "Connection must not be null!");
96+
97+
return Flux.fromIterable(this.populators).concatMap(it -> it.populate(connection)).then();
98+
}
99+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.connectionfactory.init;
17+
18+
import io.r2dbc.spi.ConnectionFactory;
19+
20+
import org.springframework.beans.factory.DisposableBean;
21+
import org.springframework.beans.factory.InitializingBean;
22+
import org.springframework.lang.Nullable;
23+
import org.springframework.util.Assert;
24+
25+
/**
26+
* Used to {@link #setDatabasePopulator set up} a database during initialization and {@link #setDatabaseCleaner clean
27+
* up} a database during destruction.
28+
*
29+
* @author Mark Paluch
30+
* @see DatabasePopulator
31+
*/
32+
public class ConnectionFactoryInitializer implements InitializingBean, DisposableBean {
33+
34+
private @Nullable ConnectionFactory connectionFactory;
35+
36+
private @Nullable DatabasePopulator databasePopulator;
37+
38+
private @Nullable DatabasePopulator databaseCleaner;
39+
40+
private boolean enabled = true;
41+
42+
/**
43+
* The {@link ConnectionFactory} for the database to populate when this component is initialized and to clean up when
44+
* this component is shut down.
45+
* <p/>
46+
* This property is mandatory with no default provided.
47+
*
48+
* @param connectionFactory the R2DBC {@link ConnectionFactory}.
49+
*/
50+
public void setConnectionFactory(ConnectionFactory connectionFactory) {
51+
this.connectionFactory = connectionFactory;
52+
}
53+
54+
/**
55+
* Set the {@link DatabasePopulator} to execute during the bean initialization phase.
56+
*
57+
* @param databasePopulator the {@link DatabasePopulator} to use during initialization
58+
* @see #setDatabaseCleaner
59+
*/
60+
public void setDatabasePopulator(DatabasePopulator databasePopulator) {
61+
this.databasePopulator = databasePopulator;
62+
}
63+
64+
/**
65+
* Set the {@link DatabasePopulator} to execute during the bean destruction phase, cleaning up the database and
66+
* leaving it in a known state for others.
67+
*
68+
* @param databaseCleaner the {@link DatabasePopulator} to use during destruction
69+
* @see #setDatabasePopulator
70+
*/
71+
public void setDatabaseCleaner(DatabasePopulator databaseCleaner) {
72+
this.databaseCleaner = databaseCleaner;
73+
}
74+
75+
/**
76+
* Flag to explicitly enable or disable the {@link #setDatabasePopulator database populator} and
77+
* {@link #setDatabaseCleaner database cleaner}.
78+
*
79+
* @param enabled {@literal true} if the database populator and database cleaner should be called on startup and
80+
* shutdown, respectively
81+
*/
82+
public void setEnabled(boolean enabled) {
83+
this.enabled = enabled;
84+
}
85+
86+
/**
87+
* Use the {@link #setDatabasePopulator database populator} to set up the database.
88+
*/
89+
@Override
90+
public void afterPropertiesSet() {
91+
execute(this.databasePopulator);
92+
}
93+
94+
/**
95+
* Use the {@link #setDatabaseCleaner database cleaner} to clean up the database.
96+
*/
97+
@Override
98+
public void destroy() {
99+
execute(this.databaseCleaner);
100+
}
101+
102+
private void execute(@Nullable DatabasePopulator populator) {
103+
104+
Assert.state(this.connectionFactory != null, "ConnectionFactory must be set");
105+
106+
if (this.enabled && populator != null) {
107+
DatabasePopulatorUtils.execute(populator, this.connectionFactory);
108+
}
109+
}
110+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.connectionfactory.init;
17+
18+
import io.r2dbc.spi.Connection;
19+
import reactor.core.publisher.Mono;
20+
21+
/**
22+
* Strategy used to populate, initialize, or clean up a database.
23+
*
24+
* @author Mark Paluch
25+
* @see ResourceDatabasePopulator
26+
* @see DatabasePopulatorUtils
27+
* @see ConnectionFactoryInitializer
28+
*/
29+
@FunctionalInterface
30+
public interface DatabasePopulator {
31+
32+
/**
33+
* Populate, initialize, or clean up the database using the provided R2DBC {@link Connection}.
34+
*
35+
* @param connection the R2DBC connection to use to populate the db; already configured and ready to use, must not be
36+
* {@literal null}.
37+
* @return {@link Mono} that initiates script execution and is notified upon completion.
38+
* @throws ScriptException in all other error cases
39+
* @see DatabasePopulatorUtils#execute
40+
*/
41+
Mono<Void> populate(Connection connection) throws ScriptException;
42+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.connectionfactory.init;
17+
18+
import io.r2dbc.spi.Connection;
19+
import io.r2dbc.spi.ConnectionFactory;
20+
import reactor.core.publisher.Mono;
21+
import reactor.util.function.Tuple2;
22+
23+
import org.springframework.dao.DataAccessException;
24+
import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils;
25+
import org.springframework.util.Assert;
26+
27+
/**
28+
* Utility methods for executing a {@link DatabasePopulator}.
29+
*
30+
* @author Mark Paluch
31+
*/
32+
public abstract class DatabasePopulatorUtils {
33+
34+
// utility constructor
35+
private DatabasePopulatorUtils() {}
36+
37+
/**
38+
* Execute the given {@link DatabasePopulator} against the given {@link io.r2dbc.spi.ConnectionFactory}.
39+
*
40+
* @param populator the {@link DatabasePopulator} to execute.
41+
* @param connectionFactory the {@link ConnectionFactory} to execute against.
42+
* @return {@link Mono} that initiates {@link DatabasePopulator#populate(Connection)} and is notified upon completion.
43+
*/
44+
public static Mono<Void> execute(DatabasePopulator populator, ConnectionFactory connectionFactory)
45+
throws DataAccessException {
46+
47+
Assert.notNull(populator, "DatabasePopulator must not be null");
48+
Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
49+
50+
return Mono.usingWhen(ConnectionFactoryUtils.getConnection(connectionFactory).map(Tuple2::getT1), //
51+
populator::populate, //
52+
it -> ConnectionFactoryUtils.releaseConnection(it, connectionFactory), //
53+
it -> ConnectionFactoryUtils.releaseConnection(it, connectionFactory))
54+
.onErrorMap(ex -> !(ex instanceof ScriptException), ex -> {
55+
return new UncategorizedScriptException("Failed to execute database script", ex);
56+
});
57+
}
58+
}

0 commit comments

Comments
 (0)