Skip to content

Commit efe2514

Browse files
schaudermp911de
authored andcommitted
Registration of columns for generated keys is now dialect dependent.
Different R2DBC drivers expect generated keys when they are in camel case to be quoted or unquoted. This can now be controlled by implementing `Dialect.renderForGeneratedKeys`. Closes: #483 Original pull request: #602.
1 parent 5a20e9d commit efe2514

21 files changed

+836
-12
lines changed

src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
*
5656
* @author Mark Paluch
5757
* @author Louis Morgan
58+
* @author Jens Schauder
5859
*/
5960
public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy {
6061

@@ -360,6 +361,11 @@ public R2dbcConverter getConverter() {
360361
return this.mappingContext;
361362
}
362363

364+
@Override
365+
public String renderForGeneratedKeys(SqlIdentifier identifier) {
366+
return dialect.renderForGeneratedKeys(identifier);
367+
}
368+
363369
private RelationalPersistentEntity<?> getRequiredPersistentEntity(Class<?> typeToRead) {
364370
return this.mappingContext.getRequiredPersistentEntity(typeToRead);
365371
}

src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
*
8989
* @author Mark Paluch
9090
* @author Bogdan Ilchyshyn
91+
* @author Jens Schauder
9192
* @since 1.1
9293
*/
9394
public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAware, ApplicationContextAware {
@@ -604,7 +605,7 @@ private <T> Mono<T> doInsert(T entity, SqlIdentifier tableName, OutboundRow outb
604605
return statement.returnGeneratedValues();
605606
}
606607

607-
return statement.returnGeneratedValues(dataAccessStrategy.toSql(identifierColumns.get(0)));
608+
return statement.returnGeneratedValues(dataAccessStrategy.renderForGeneratedKeys(identifierColumns.get(0)));
608609
})
609610
.map(this.dataAccessStrategy.getConverter().populateIdIfNecessary(entity)) //
610611
.all() //

src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
import org.springframework.lang.Nullable;
3030
import org.springframework.r2dbc.core.Parameter;
3131
import org.springframework.r2dbc.core.PreparedOperation;
32+
import org.springframework.util.Assert;
3233

3334
/**
3435
* Data access strategy that generalizes convenience operations using mapped entities. Typically used internally by
3536
* {@link DatabaseClient} and repository support. SQL creation is limited to single-table operations and single-column
3637
* primary keys.
3738
*
3839
* @author Mark Paluch
40+
* @author Jens Schauder
3941
* @see org.springframework.r2dbc.core.PreparedOperation
4042
* @deprecated since 1.2 in favor of using direct usage of {@link StatementMapper},
4143
* {@link org.springframework.data.r2dbc.query.UpdateMapper} and {@link R2dbcConverter}.
@@ -135,6 +137,19 @@ public interface ReactiveDataAccessStrategy {
135137
*/
136138
String toSql(SqlIdentifier identifier);
137139

140+
/**
141+
* Render a {@link SqlIdentifier} in a way suitable for registering it as a generated key with a statement.
142+
*
143+
* @param identifier to render. Must not be {@literal null}.
144+
* @return rendered identifier. Guaranteed to be not {@literal null}.
145+
*/
146+
default String renderForGeneratedKeys(SqlIdentifier identifier) {
147+
148+
Assert.notNull(identifier, "Indentifier must not be null.");
149+
150+
return identifier.toSql(IdentifierProcessing.NONE);
151+
}
152+
138153
/**
139154
* Interface to retrieve parameters for named parameter processing.
140155
*/
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package org.springframework.data.r2dbc.dialect;
22

3+
import org.springframework.data.relational.core.sql.SqlIdentifier;
4+
35
/**
46
* An SQL dialect for H2 in Postgres Compatibility mode.
57
*
68
* @author Mark Paluch
9+
* @author Jens Schauder
710
*/
811
public class H2Dialect extends PostgresDialect {
912

1013
/**
1114
* Singleton instance.
1215
*/
1316
public static final H2Dialect INSTANCE = new H2Dialect();
17+
18+
@Override
19+
public String renderForGeneratedKeys(SqlIdentifier identifier) {
20+
return identifier.getReference(getIdentifierProcessing());
21+
}
1422
}

src/main/java/org/springframework/data/r2dbc/dialect/MySqlDialect.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@
2828
import org.springframework.core.convert.converter.Converter;
2929
import org.springframework.data.convert.ReadingConverter;
3030
import org.springframework.data.convert.WritingConverter;
31+
import org.springframework.data.relational.core.sql.SqlIdentifier;
3132
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
3233

3334
/**
3435
* An SQL dialect for MySQL.
3536
*
3637
* @author Mark Paluch
38+
* @author Jens Schauder
3739
*/
3840
public class MySqlDialect extends org.springframework.data.relational.core.dialect.MySqlDialect
3941
implements R2dbcDialect {
@@ -103,6 +105,11 @@ public Boolean convert(Byte s) {
103105
}
104106
}
105107

108+
@Override
109+
public String renderForGeneratedKeys(SqlIdentifier identifier) {
110+
return identifier.getReference(getIdentifierProcessing());
111+
}
112+
106113
/**
107114
* Simple singleton to convert {@link Boolean}s to their {@link Byte} representation. MySQL does not have a built-in
108115
* boolean type by default, so relies on using a byte instead. {@literal true} maps to {@code 1}.

src/main/java/org/springframework/data/r2dbc/dialect/R2dbcDialect.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.data.mapping.model.SimpleTypeHolder;
99
import org.springframework.data.r2dbc.mapping.R2dbcSimpleTypeHolder;
1010
import org.springframework.data.relational.core.dialect.Dialect;
11+
import org.springframework.data.relational.core.sql.SqlIdentifier;
1112
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
1213

1314
/**
@@ -59,4 +60,16 @@ default SimpleTypeHolder getSimpleTypeHolder() {
5960
default Collection<Object> getConverters() {
6061
return Collections.emptySet();
6162
}
63+
64+
/**
65+
* Render a {@link SqlIdentifier} in a way suitable for registering it as a generated key with a statement. The
66+
* default implementation renders it as it would render a SQL representation of the identifier, i.e. with quotes where
67+
* applicable.
68+
*
69+
* @param identifier to render. Must not be {@literal null}.
70+
* @return rendered identifier. Guaranteed to be not {@literal null}.
71+
*/
72+
default String renderForGeneratedKeys(SqlIdentifier identifier) {
73+
return identifier.toSql(getIdentifierProcessing());
74+
}
6275
}

src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ public QueryMapper(R2dbcDialect dialect, R2dbcConverter converter) {
8282

8383
/**
8484
* Render a {@link SqlIdentifier} for SQL usage.
85+
* The resulting String might contain quoting characters.
8586
*
86-
* @param identifier
87-
* @return
87+
* @param identifier the identifier to be rendered.
88+
* @return an identifier String.
8889
* @since 1.1
8990
*/
9091
public String toSql(SqlIdentifier identifier) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2021 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.repository;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import io.r2dbc.spi.ConnectionFactory;
21+
import lombok.Getter;
22+
import lombok.NoArgsConstructor;
23+
import lombok.Setter;
24+
import reactor.test.StepVerifier;
25+
26+
import java.time.Duration;
27+
import java.util.Arrays;
28+
import java.util.List;
29+
import java.util.Objects;
30+
31+
import javax.sql.DataSource;
32+
33+
import org.junit.jupiter.api.BeforeEach;
34+
import org.junit.jupiter.api.Test;
35+
import org.springframework.beans.factory.annotation.Autowired;
36+
import org.springframework.dao.DataAccessException;
37+
import org.springframework.data.annotation.Id;
38+
import org.springframework.data.annotation.PersistenceConstructor;
39+
import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport;
40+
import org.springframework.data.relational.core.mapping.Column;
41+
import org.springframework.data.relational.core.mapping.Table;
42+
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
43+
import org.springframework.jdbc.core.JdbcTemplate;
44+
import org.springframework.lang.Nullable;
45+
46+
/**
47+
* Abstract base class for integration tests for {@link LegoSetRepository} with table and column names that contain
48+
* upper and lower case characters.
49+
*
50+
* @author Jens Schauder
51+
*/
52+
public abstract class AbstractR2dbcRepositoryWithMixedCaseNamesIntegrationTests extends R2dbcIntegrationTestSupport {
53+
54+
@Autowired private LegoSetRepository repository;
55+
protected JdbcTemplate jdbc;
56+
57+
@BeforeEach
58+
void before() {
59+
60+
this.jdbc = createJdbcTemplate(createDataSource());
61+
62+
try {
63+
this.jdbc.execute(getDropTableStatement());
64+
} catch (DataAccessException e) {}
65+
66+
this.jdbc.execute(getCreateTableStatement());
67+
}
68+
69+
/**
70+
* Creates a {@link DataSource} to be used in this test.
71+
*
72+
* @return the {@link DataSource} to be used in this test.
73+
*/
74+
protected abstract DataSource createDataSource();
75+
76+
/**
77+
* Creates a {@link ConnectionFactory} to be used in this test.
78+
*
79+
* @return the {@link ConnectionFactory} to be used in this test.
80+
*/
81+
protected abstract ConnectionFactory createConnectionFactory();
82+
83+
/**
84+
* Returns the CREATE TABLE statement for table {@code legoset} with the following three columns:
85+
* <ul>
86+
* <li>id integer (primary key), not null, auto-increment</li>
87+
* <li>name varchar(255), nullable</li>
88+
* <li>manual integer, nullable</li>
89+
* </ul>
90+
*
91+
* @return the CREATE TABLE statement for table {@code legoset} with three columns.
92+
*/
93+
protected abstract String getCreateTableStatement();
94+
95+
/**
96+
* Returns the the DROP TABLE statement for table {@code LegoSet}.
97+
*
98+
* @return the DROP TABLE statement for table {@code LegoSet}.
99+
*/
100+
protected abstract String getDropTableStatement();
101+
102+
@Test
103+
void insertAndReadEntities() {
104+
105+
LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12);
106+
LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13);
107+
108+
repository.saveAll(Arrays.asList(legoSet1, legoSet2)) //
109+
.as(StepVerifier::create) //
110+
.expectNextCount(2) //
111+
.verifyComplete();
112+
113+
List<LegoSet> legoSets = repository //
114+
.findAll() //
115+
.collectList() //
116+
.block(Duration.ofMillis(500));
117+
118+
assertThat(legoSets).containsExactlyInAnyOrder(legoSet1, legoSet2);
119+
}
120+
121+
interface LegoSetRepository extends ReactiveCrudRepository<LegoSet, Integer> {}
122+
123+
@Getter
124+
@Setter
125+
@Table("LegoSet")
126+
@NoArgsConstructor
127+
public static class LegoSet {
128+
129+
@Nullable @Column("Id") @Id Integer id;
130+
131+
@Column("Name") String name;
132+
133+
@Column("Manual") Integer manual;
134+
135+
@PersistenceConstructor
136+
LegoSet(@Nullable Integer id, String name, Integer manual) {
137+
this.id = id;
138+
this.name = name;
139+
this.manual = manual;
140+
}
141+
142+
@Override
143+
public boolean equals(Object o) {
144+
if (this == o)
145+
return true;
146+
if (o == null || getClass() != o.getClass())
147+
return false;
148+
LegoSet legoSet = (LegoSet) o;
149+
return Objects.equals(id, legoSet.id) && Objects.equals(name, legoSet.name)
150+
&& Objects.equals(manual, legoSet.manual);
151+
}
152+
153+
@Override
154+
public int hashCode() {
155+
return Objects.hash(id, name, manual);
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)