Skip to content

Commit 5a20e9d

Browse files
committed
Refine conversion directions between boolean and byte using MySQL/MariaDB.
ByteToBooleanConverter is now marked as reading converter and we've added a writing converter with BooleanToByteConverter to ensure proper conversion. Previously, all byte values were converted to boolean resulting in data precision loss. Closes #589
1 parent cde3d2a commit 5a20e9d

File tree

2 files changed

+140
-2
lines changed

2 files changed

+140
-2
lines changed

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@
2020
import java.net.URL;
2121
import java.util.Arrays;
2222
import java.util.Collection;
23-
import java.util.Collections;
2423
import java.util.HashSet;
2524
import java.util.List;
2625
import java.util.Set;
2726
import java.util.UUID;
2827

2928
import org.springframework.core.convert.converter.Converter;
29+
import org.springframework.data.convert.ReadingConverter;
30+
import org.springframework.data.convert.WritingConverter;
3031
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
3132

3233
/**
@@ -50,7 +51,8 @@ public class MySqlDialect extends org.springframework.data.relational.core.diale
5051
/**
5152
* MySQL specific converters.
5253
*/
53-
private static final List<Object> CONVERTERS = Collections.singletonList(ByteToBooleanConverter.INSTANCE);
54+
private static final List<Object> CONVERTERS = Arrays.asList(ByteToBooleanConverter.INSTANCE,
55+
BooleanToByteConverter.INSTANCE);
5456

5557
/*
5658
* (non-Javadoc)
@@ -85,6 +87,7 @@ public Collection<Object> getConverters() {
8587
*
8688
* @author Michael Berry
8789
*/
90+
@ReadingConverter
8891
public enum ByteToBooleanConverter implements Converter<Byte, Boolean> {
8992

9093
INSTANCE;
@@ -99,4 +102,21 @@ public Boolean convert(Byte s) {
99102
return s != 0;
100103
}
101104
}
105+
106+
/**
107+
* Simple singleton to convert {@link Boolean}s to their {@link Byte} representation. MySQL does not have a built-in
108+
* boolean type by default, so relies on using a byte instead. {@literal true} maps to {@code 1}.
109+
*
110+
* @author Mark Paluch
111+
*/
112+
@WritingConverter
113+
public enum BooleanToByteConverter implements Converter<Boolean, Byte> {
114+
115+
INSTANCE;
116+
117+
@Override
118+
public Byte convert(Boolean s) {
119+
return (byte) (s.booleanValue() ? 1 : 0);
120+
}
121+
}
102122
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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.convert;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import io.r2dbc.spi.test.MockColumnMetadata;
21+
import io.r2dbc.spi.test.MockRow;
22+
import io.r2dbc.spi.test.MockRowMetadata;
23+
import lombok.Value;
24+
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
28+
29+
import org.junit.jupiter.api.BeforeEach;
30+
import org.junit.jupiter.api.Test;
31+
32+
import org.springframework.data.convert.CustomConversions;
33+
import org.springframework.data.r2dbc.dialect.MySqlDialect;
34+
import org.springframework.data.r2dbc.mapping.OutboundRow;
35+
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
36+
import org.springframework.data.r2dbc.testing.OutboundRowAssert;
37+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
38+
39+
/**
40+
* MySQL-specific unit tests for {@link MappingR2dbcConverter}.
41+
*
42+
* @author Mark Paluch
43+
*/
44+
class MySqlMappingR2dbcConverterUnitTests {
45+
46+
private RelationalMappingContext mappingContext = new R2dbcMappingContext();
47+
private MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext);
48+
49+
@BeforeEach
50+
void before() {
51+
52+
List<Object> converters = new ArrayList<>(MySqlDialect.INSTANCE.getConverters());
53+
converters.addAll(R2dbcCustomConversions.STORE_CONVERTERS);
54+
CustomConversions.StoreConversions storeConversions = CustomConversions.StoreConversions
55+
.of(MySqlDialect.INSTANCE.getSimpleTypeHolder(), converters);
56+
57+
R2dbcCustomConversions customConversions = new R2dbcCustomConversions(storeConversions, Collections.emptyList());
58+
59+
mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder());
60+
61+
converter = new MappingR2dbcConverter(mappingContext, customConversions);
62+
}
63+
64+
@Test // gh-589
65+
void shouldWriteBooleanToByte() {
66+
67+
BooleanMapping object = new BooleanMapping(0, true, false);
68+
OutboundRow row = new OutboundRow();
69+
70+
converter.write(object, row);
71+
72+
OutboundRowAssert.assertThat(row).containsColumnWithValue("flag1", (byte) 1).containsColumnWithValue("flag2",
73+
(byte) 0);
74+
}
75+
76+
@Test // gh-589
77+
void shouldReadByteToBoolean() {
78+
79+
MockRow row = MockRow.builder().identified("flag1", Object.class, (byte) 1)
80+
.identified("flag2", Object.class, (byte) 0).build();
81+
82+
MockRowMetadata metadata = MockRowMetadata.builder()
83+
.columnMetadata(MockColumnMetadata.builder().name("flag1").build())
84+
.columnMetadata(MockColumnMetadata.builder().name("flag2").build()).build();
85+
86+
BooleanMapping mapped = converter.read(BooleanMapping.class, row, metadata);
87+
88+
assertThat(mapped.flag1).isTrue();
89+
assertThat(mapped.flag2).isFalse();
90+
}
91+
92+
@Test // gh-589
93+
void shouldPreserveByteValue() {
94+
95+
WithByte object = new WithByte(0, (byte) 3);
96+
OutboundRow row = new OutboundRow();
97+
98+
converter.write(object, row);
99+
100+
OutboundRowAssert.assertThat(row).containsColumnWithValue("state", (byte) 3);
101+
}
102+
103+
@Value
104+
private static class BooleanMapping {
105+
106+
Integer id;
107+
boolean flag1;
108+
boolean flag2;
109+
}
110+
111+
@Value
112+
private static class WithByte {
113+
114+
Integer id;
115+
byte state;
116+
}
117+
118+
}

0 commit comments

Comments
 (0)