#73 - Introduce PreparedOperation.

We now encapsulate prepared operations from the StatementFactory within PreparedOperation that renders SQL and provides binding values.

StatementFactory supports SELECT/INSERT/UPDATE/DELETE statement creation considering Dialect-specific rendering.
StatementFactory replaces String-based statement methods in ReactiveDataAccessStrategy.

PreparedOperation<Update> operation = accessStrategy.getStatements().update(entity.getTableName(), binder -> {
	binder.bind("name", "updated value");
	binder.filterBy("id", SettableValue.from(42));
});

databaseClient.execute().sql(operation).then();

Original pull request: #82.
This commit is contained in:
Mark Paluch
2019-03-18 14:52:30 +01:00
parent 72ffccbfbc
commit ef2d885b1e
12 changed files with 991 additions and 585 deletions

View File

@@ -1,103 +0,0 @@
package org.springframework.data.r2dbc.function;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import io.r2dbc.spi.Statement;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.data.r2dbc.domain.SettableValue;
/**
* Unit tests for {@link DefaultReactiveDataAccessStrategy}.
*
* @author Mark Paluch
*/
public class DefaultReactiveDataAccessStrategyUnitTests {
DefaultReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE);
@Test // gh-20
public void shouldRenderInsertAndReturnGeneratedKeysQuery() {
BindableOperation operation = strategy.insertAndReturnGeneratedKeys("table",
new HashSet<>(Arrays.asList("firstname", "lastname")));
assertThat(operation.toQuery()).isEqualTo("INSERT INTO table (firstname, lastname) VALUES($1, $2)");
}
@Test // gh-20
public void shouldRenderUpdateByIdQuery() {
BindableOperation operation = strategy.updateById("table", new HashSet<>(Arrays.asList("firstname", "lastname")),
"id");
assertThat(operation.toQuery()).isEqualTo("UPDATE table SET firstname = $2, lastname = $3 WHERE id = $1");
}
@Test // gh-20
public void shouldRenderDeleteByIdQuery() {
BindableOperation operation = strategy.deleteById("table", "id");
assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id = $1");
}
@Test // gh-20
public void shouldRenderDeleteByIdInQuery() {
Statement statement = mock(Statement.class);
BindIdOperation operation = strategy.deleteByIdIn("table", "id");
operation.bindId(statement, Collections.singleton("foo"));
assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id IN ($1)");
operation.bindId(statement, "bar");
assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id IN ($1, $2)");
}
@Test // gh-22
public void shouldUpdateArray() {
Map<String, SettableValue> columnsToUpdate = strategy
.getOutboundRow(new WithCollectionTypes(new String[] { "one", "two" }, null));
Object stringArray = columnsToUpdate.get("string_array").getValue();
assertThat(stringArray).isInstanceOf(String[].class);
assertThat((String[]) stringArray).hasSize(2).contains("one", "two");
}
@Test // gh-22
public void shouldConvertListToArray() {
Map<String, SettableValue> columnsToUpdate = strategy
.getOutboundRow(new WithCollectionTypes(null, Arrays.asList("one", "two")));
Object stringArray = columnsToUpdate.get("string_collection").getValue();
assertThat(stringArray).isInstanceOf(String[].class);
assertThat((String[]) stringArray).hasSize(2).contains("one", "two");
}
static class WithCollectionTypes {
String[] stringArray;
List<String> stringCollection;
WithCollectionTypes(String[] stringArray, List<String> stringCollection) {
this.stringArray = stringArray;
this.stringCollection = stringCollection;
}
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.function;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import io.r2dbc.spi.Statement;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.relational.core.dialect.RenderContextFactory;
import org.springframework.data.relational.core.sql.Delete;
import org.springframework.data.relational.core.sql.Insert;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.Update;
/**
* Unit tests for {@link StatementFactory}.
*
* @author Mark Paluch
*/
public class StatementFactoryUnitTests {
// See https://github.com/spring-projects/spring-data-r2dbc/issues/55
DefaultStatementFactory statements = new DefaultStatementFactory(PostgresDialect.INSTANCE,
new RenderContextFactory(org.springframework.data.relational.core.dialect.PostgresDialect.INSTANCE)
.createRenderContext());
Statement statementMock = mock(Statement.class);
@Test
public void shouldToQuerySimpleSelectWithoutBindings() {
PreparedOperation<Select> select = statements.select("foo", Arrays.asList("bar", "baz"), it -> {});
assertThat(select.getSource()).isInstanceOf(Select.class);
assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo");
select.bind(statementMock);
verifyZeroInteractions(statementMock);
}
@Test
public void shouldToQuerySimpleSelectWithSimpleFilter() {
PreparedOperation<Select> select = statements.select("foo", Arrays.asList("bar", "baz"), it -> {
it.filterBy("doe", SettableValue.from("John"));
});
assertThat(select.getSource()).isInstanceOf(Select.class);
assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo WHERE foo.doe = $1");
select.bind(statementMock);
verify(statementMock).bind(0, "John");
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldToQuerySimpleSelectWithMultipleFilters() {
PreparedOperation<Select> select = statements.select("foo", Arrays.asList("bar", "baz"), it -> {
it.filterBy("doe", SettableValue.from("John"));
it.filterBy("baz", SettableValue.from("Jake"));
});
assertThat(select.getSource()).isInstanceOf(Select.class);
assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo WHERE foo.doe = $1 AND foo.baz = $2");
select.bind(statementMock);
verify(statementMock).bind(0, "John");
verify(statementMock).bind(1, "Jake");
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldToQuerySimpleSelectWithNullFilter() {
PreparedOperation<Select> select = statements.select("foo", Arrays.asList("bar", "baz"), it -> {
it.filterBy("doe", SettableValue.empty(String.class));
});
assertThat(select.getSource()).isInstanceOf(Select.class);
assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo WHERE foo.doe IS NULL");
select.bind(statementMock);
verifyZeroInteractions(statementMock);
}
@Test
public void shouldToQuerySimpleSelectWithIterableFilter() {
PreparedOperation<Select> select = statements.select("foo", Arrays.asList("bar", "baz"), it -> {
it.filterBy("doe", SettableValue.from(Arrays.asList("John", "Jake")));
});
assertThat(select.getSource()).isInstanceOf(Select.class);
assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo WHERE foo.doe IN ($1, $2)");
select.bind(statementMock);
verify(statementMock).bind(0, "John");
verify(statementMock).bind(1, "Jake");
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldFailInsertToQueryingWithoutValueBindings() {
assertThatThrownBy(() -> statements.insert("foo", Collections.emptyList(), it -> {}))
.isInstanceOf(IllegalStateException.class);
}
@Test
public void shouldToQuerySimpleInsert() {
PreparedOperation<Insert> insert = statements.insert("foo", Collections.emptyList(), it -> {
it.bind("bar", SettableValue.from("Foo"));
});
assertThat(insert.getSource()).isInstanceOf(Insert.class);
assertThat(insert.toQuery()).isEqualTo("INSERT INTO foo (bar) VALUES ($1)");
insert.bind(statementMock);
verify(statementMock).bind(0, "Foo");
verify(statementMock).returnGeneratedValues(any(String[].class));
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldFailUpdateToQueryingWithoutValueBindings() {
assertThatThrownBy(() -> statements.update("foo", it -> it.filterBy("foo", SettableValue.empty(Object.class))))
.isInstanceOf(IllegalStateException.class);
}
@Test
public void shouldToQuerySimpleUpdate() {
PreparedOperation<Update> update = statements.update("foo", it -> {
it.bind("bar", SettableValue.from("Foo"));
});
assertThat(update.getSource()).isInstanceOf(Update.class);
assertThat(update.toQuery()).isEqualTo("UPDATE foo SET bar = $1");
update.bind(statementMock);
verify(statementMock).bind(0, "Foo");
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldToQueryNullUpdate() {
PreparedOperation<Update> update = statements.update("foo", it -> {
it.bind("bar", SettableValue.empty(String.class));
});
assertThat(update.getSource()).isInstanceOf(Update.class);
assertThat(update.toQuery()).isEqualTo("UPDATE foo SET bar = $1");
update.bind(statementMock);
verify(statementMock).bindNull(0, String.class);
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldToQueryUpdateWithFilter() {
PreparedOperation<Update> update = statements.update("foo", it -> {
it.bind("bar", SettableValue.from("Foo"));
it.filterBy("baz", SettableValue.from("Baz"));
});
assertThat(update.getSource()).isInstanceOf(Update.class);
assertThat(update.toQuery()).isEqualTo("UPDATE foo SET bar = $1 WHERE foo.baz = $2");
update.bind(statementMock);
verify(statementMock).bind(0, "Foo");
verify(statementMock).bind(1, "Baz");
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldToQuerySimpleDeleteWithSimpleFilter() {
PreparedOperation<Delete> delete = statements.delete("foo", it -> {
it.filterBy("doe", SettableValue.from("John"));
});
assertThat(delete.getSource()).isInstanceOf(Delete.class);
assertThat(delete.toQuery()).isEqualTo("DELETE FROM foo WHERE foo.doe = $1");
delete.bind(statementMock);
verify(statementMock).bind(0, "John");
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldToQuerySimpleDeleteWithMultipleFilters() {
PreparedOperation<Delete> delete = statements.delete("foo", it -> {
it.filterBy("doe", SettableValue.from("John"));
it.filterBy("baz", SettableValue.from("Jake"));
});
assertThat(delete.getSource()).isInstanceOf(Delete.class);
assertThat(delete.toQuery()).isEqualTo("DELETE FROM foo WHERE foo.doe = $1 AND foo.baz = $2");
delete.bind(statementMock);
verify(statementMock).bind(0, "John");
verify(statementMock).bind(1, "Jake");
verifyNoMoreInteractions(statementMock);
}
@Test
public void shouldToQuerySimpleDeleteWithNullFilter() {
PreparedOperation<Delete> delete = statements.delete("foo", it -> {
it.filterBy("doe", SettableValue.empty(String.class));
});
assertThat(delete.getSource()).isInstanceOf(Delete.class);
assertThat(delete.toQuery()).isEqualTo("DELETE FROM foo WHERE foo.doe IS NULL");
delete.bind(statementMock);
verifyZeroInteractions(statementMock);
}
}