Skip to content

Commit 55bd893

Browse files
authored
optimize: add support for Jackson serialization and deserialization of PostgreSQL array types (#7669)
1 parent a977730 commit 55bd893

File tree

4 files changed

+298
-1
lines changed

4 files changed

+298
-1
lines changed

changes/en-us/2.x.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Add changes here for all PR submitted to the 2.x branch.
2525
- [[#7492](https://github.com/apache/incubator-seata/pull/7492)] upgrade HTTP client in common module to support HTTP/2
2626
- [[#7551](https://github.com/apache/incubator-seata/pull/7551)] XAUtils add support for DM Database
2727
- [[#7559](https://github.com/apache/incubator-seata/pull/7559)] Introduce Cleanup API for TableMetaRefreshHolder Instance
28+
- [[#7669](https://github.com/apache/incubator-seata/pull/7669)] add support for Jackson serialization and deserialization of PostgreSQL array types
2829

2930
### bugfix:
3031

changes/zh-cn/2.x.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- [[#7492](https://github.com/apache/incubator-seata/pull/7492)] 升级 common 模块中的 HTTP 客户端以支持 HTTP/2
2626
- [[#7551](https://github.com/apache/incubator-seata/pull/7551)] XAUtils支持达梦数据库
2727
- [[#7559](https://github.com/apache/incubator-seata/pull/7559)] 为 TableMetaRefreshHolder 实例引入清理 API
28+
- [[#7669](https://github.com/apache/incubator-seata/pull/7669)] 添加对 Jackson 序列化和反序列化 PostgreSQL 数组类型的支持
2829

2930

3031
### bugfix:

rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParser.java

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.fasterxml.jackson.databind.DeserializationContext;
2626
import com.fasterxml.jackson.databind.DeserializationFeature;
2727
import com.fasterxml.jackson.databind.JsonDeserializer;
28+
import com.fasterxml.jackson.databind.JsonNode;
2829
import com.fasterxml.jackson.databind.JsonSerializer;
2930
import com.fasterxml.jackson.databind.MapperFeature;
3031
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -40,6 +41,7 @@
4041
import org.apache.seata.common.loader.EnhancedServiceNotFoundException;
4142
import org.apache.seata.common.loader.LoadLevel;
4243
import org.apache.seata.common.util.CollectionUtils;
44+
import org.apache.seata.rm.datasource.sql.serial.SerialArray;
4345
import org.apache.seata.rm.datasource.undo.BranchUndoLog;
4446
import org.apache.seata.rm.datasource.undo.UndoLogParser;
4547
import org.apache.seata.rm.datasource.undo.parser.spi.JacksonSerializer;
@@ -136,6 +138,16 @@ public class JacksonUndoLogParser implements UndoLogParser, Initialize {
136138
*/
137139
private final JsonDeserializer dmdbTimestampDeserializer = new DmdbTimestampDeserializer();
138140

141+
/**
142+
* customize serializer for org.apache.seata.rm.datasource.sql.serial.SerialArray
143+
*/
144+
private final JsonSerializer serialArraySerializer = new SerialArraySerializer();
145+
146+
/**
147+
* customize deserializer for org.apache.seata.rm.datasource.sql.serial.SerialArray
148+
*/
149+
private final JsonDeserializer serialArrayDeserializer = new SerialArrayDeserializer();
150+
139151
@Override
140152
public void init() {
141153
try {
@@ -170,6 +182,8 @@ public void init() {
170182
module.addDeserializer(SerialClob.class, clobDeserializer);
171183
module.addSerializer(LocalDateTime.class, localDateTimeSerializer);
172184
module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
185+
module.addSerializer(SerialArray.class, serialArraySerializer);
186+
module.addDeserializer(SerialArray.class, serialArrayDeserializer);
173187
registerDmdbTimestampModuleIfPresent();
174188
mapper.registerModule(module);
175189
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@@ -483,7 +497,7 @@ private Instant getInstant(Object dmdbTimestamp) throws IOException {
483497
}
484498
}
485499

486-
public class DmdbTimestampDeserializer extends JsonDeserializer<Object> {
500+
private class DmdbTimestampDeserializer extends JsonDeserializer<Object> {
487501

488502
@Override
489503
public Object deserialize(JsonParser p, DeserializationContext ctxt) {
@@ -532,4 +546,104 @@ public static void setZoneOffset(ZoneId zoneId) {
532546
Objects.requireNonNull(zoneId, "zoneId must be not null");
533547
JacksonUndoLogParser.zoneId = zoneId;
534548
}
549+
550+
/**
551+
* the class of serialize SerialArray type
552+
*/
553+
private static class SerialArraySerializer extends JsonSerializer<SerialArray> {
554+
555+
@Override
556+
public void serializeWithType(
557+
SerialArray serialArray,
558+
JsonGenerator gen,
559+
SerializerProvider serializers,
560+
TypeSerializer typeSerializer)
561+
throws IOException {
562+
WritableTypeId typeIdDef =
563+
typeSerializer.writeTypePrefix(gen, typeSerializer.typeId(serialArray, JsonToken.START_OBJECT));
564+
serializeValue(serialArray, gen, serializers);
565+
typeSerializer.writeTypeSuffix(gen, typeIdDef);
566+
}
567+
568+
@Override
569+
public void serialize(SerialArray serialArray, JsonGenerator gen, SerializerProvider serializers)
570+
throws IOException {
571+
gen.writeStartObject();
572+
serializeValue(serialArray, gen, serializers);
573+
gen.writeEndObject();
574+
}
575+
576+
private void serializeValue(SerialArray serialArray, JsonGenerator gen, SerializerProvider serializers)
577+
throws IOException {
578+
gen.writeFieldName("baseType");
579+
try {
580+
gen.writeNumber(serialArray.getBaseType());
581+
} catch (SQLException e) {
582+
gen.writeNull();
583+
}
584+
gen.writeFieldName("baseTypeName");
585+
try {
586+
gen.writeString(serialArray.getBaseTypeName());
587+
} catch (SQLException e) {
588+
gen.writeNull();
589+
}
590+
gen.writeFieldName("elements");
591+
try {
592+
Object[] elements = serialArray.getElements();
593+
gen.writeStartArray();
594+
if (elements != null) {
595+
for (Object element : elements) {
596+
gen.writeObject(element);
597+
}
598+
}
599+
gen.writeEndArray();
600+
} catch (Exception e) {
601+
gen.writeNull();
602+
}
603+
}
604+
}
605+
606+
/**
607+
* the class of deserialize SerialArray type
608+
*/
609+
private static class SerialArrayDeserializer extends JsonDeserializer<SerialArray> {
610+
@Override
611+
public SerialArray deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
612+
try {
613+
JsonNode node = p.getCodec().readTree(p);
614+
SerialArray serialArray = new SerialArray();
615+
616+
if (node.has("baseType") && !node.get("baseType").isNull()) {
617+
serialArray.setBaseType(node.get("baseType").asInt());
618+
}
619+
620+
if (node.has("baseTypeName") && !node.get("baseTypeName").isNull()) {
621+
serialArray.setBaseTypeName(node.get("baseTypeName").asText());
622+
}
623+
624+
if (node.has("elements") && node.get("elements").isArray()) {
625+
JsonNode elementsNode = node.get("elements");
626+
Object[] elements = new Object[elementsNode.size()];
627+
for (int i = 0; i < elementsNode.size(); i++) {
628+
JsonNode elementNode = elementsNode.get(i);
629+
if (elementNode.isNull()) {
630+
elements[i] = null;
631+
} else if (elementNode.isNumber()) {
632+
elements[i] = elementNode.asLong();
633+
} else if (elementNode.isTextual()) {
634+
elements[i] = elementNode.asText();
635+
} else {
636+
elements[i] = elementNode;
637+
}
638+
}
639+
serialArray.setElements(elements);
640+
}
641+
642+
return serialArray;
643+
} catch (Exception e) {
644+
LOGGER.error("deserialize SerialArray error: {}", e.getMessage(), e);
645+
return null;
646+
}
647+
}
648+
}
535649
}

rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/JacksonUndoLogParserTest.java

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.fasterxml.jackson.databind.ObjectMapper;
2020
import org.apache.seata.common.loader.EnhancedServiceLoader;
2121
import org.apache.seata.rm.datasource.DataCompareUtils;
22+
import org.apache.seata.rm.datasource.sql.serial.SerialArray;
2223
import org.apache.seata.rm.datasource.sql.struct.Field;
2324
import org.apache.seata.rm.datasource.undo.AbstractUndoLogManager;
2425
import org.apache.seata.rm.datasource.undo.BaseUndoLogParserTest;
@@ -33,14 +34,18 @@
3334
import java.lang.reflect.InvocationTargetException;
3435
import java.lang.reflect.Method;
3536
import java.math.BigDecimal;
37+
import java.sql.Array;
3638
import java.sql.JDBCType;
39+
import java.sql.ResultSet;
3740
import java.sql.SQLException;
3841
import java.sql.Timestamp;
42+
import java.sql.Types;
3943
import java.time.Instant;
4044
import java.time.LocalDateTime;
4145
import java.time.ZoneId;
4246
import java.time.ZonedDateTime;
4347
import java.util.Date;
48+
import java.util.Map;
4449

4550
import static org.mockito.Mockito.mockStatic;
4651

@@ -181,6 +186,182 @@ public void testSerializeAndDeserializeDmdbTimestampWithNoZone()
181186
}
182187
}
183188

189+
@Test
190+
public void testSerializeAndDeserializeSerialArray()
191+
throws NoSuchFieldException, IllegalAccessException, IOException, SQLException {
192+
// get the jackson mapper
193+
java.lang.reflect.Field reflectField = parser.getClass().getDeclaredField("mapper");
194+
reflectField.setAccessible(true);
195+
ObjectMapper mapper = (ObjectMapper) reflectField.get(parser);
196+
197+
// create a mock Array object for testing SerialArray
198+
Array mockArray = new MockArray();
199+
SerialArray serialArray = new SerialArray(mockArray);
200+
201+
// test SerialArray with BIGINT array (PostgreSQL _int8 type)
202+
Field field = new Field("dept_ids", JDBCType.ARRAY.getVendorTypeNumber(), serialArray);
203+
byte[] bytes = mapper.writeValueAsBytes(field);
204+
Field sameField = mapper.readValue(bytes, Field.class);
205+
Assertions.assertTrue(DataCompareUtils.isFieldEquals(field, sameField).getResult());
206+
207+
// verify the SerialArray properties are correctly serialized/deserialized
208+
SerialArray deserializedArray = (SerialArray) sameField.getValue();
209+
Assertions.assertEquals(serialArray.getBaseType(), deserializedArray.getBaseType());
210+
Assertions.assertEquals(serialArray.getBaseTypeName(), deserializedArray.getBaseTypeName());
211+
Assertions.assertArrayEquals(serialArray.getElements(), deserializedArray.getElements());
212+
}
213+
214+
@Test
215+
public void testSerializeAndDeserializeSerialArrayWithNulls()
216+
throws NoSuchFieldException, IllegalAccessException, IOException, SQLException {
217+
// get the jackson mapper
218+
java.lang.reflect.Field reflectField = parser.getClass().getDeclaredField("mapper");
219+
reflectField.setAccessible(true);
220+
ObjectMapper mapper = (ObjectMapper) reflectField.get(parser);
221+
222+
// create SerialArray with null elements
223+
Array mockArrayWithNulls = new MockArrayWithNulls();
224+
SerialArray serialArray = new SerialArray(mockArrayWithNulls);
225+
226+
Field field = new Field("nullable_array", JDBCType.ARRAY.getVendorTypeNumber(), serialArray);
227+
byte[] bytes = mapper.writeValueAsBytes(field);
228+
Field sameField = mapper.readValue(bytes, Field.class);
229+
230+
Assertions.assertTrue(DataCompareUtils.isFieldEquals(field, sameField).getResult());
231+
232+
// verify null elements are handled correctly
233+
SerialArray deserializedArray = (SerialArray) sameField.getValue();
234+
Object[] elements = deserializedArray.getElements();
235+
Assertions.assertEquals(3, elements.length);
236+
Assertions.assertEquals(1L, elements[0]);
237+
Assertions.assertNull(elements[1]);
238+
Assertions.assertEquals(3L, elements[2]);
239+
}
240+
241+
/**
242+
* Mock Array class for testing SerialArray serialization
243+
*/
244+
private static class MockArray implements Array {
245+
private final Object[] elements = {1L, 2L, 3L, 4L, 5L};
246+
247+
@Override
248+
public String getBaseTypeName() throws SQLException {
249+
return "int8";
250+
}
251+
252+
@Override
253+
public int getBaseType() throws SQLException {
254+
return Types.BIGINT;
255+
}
256+
257+
@Override
258+
public Object getArray() throws SQLException {
259+
return elements;
260+
}
261+
262+
@Override
263+
public Object getArray(Map<String, Class<?>> map) throws SQLException {
264+
return elements;
265+
}
266+
267+
@Override
268+
public Object getArray(long index, int count) throws SQLException {
269+
return elements;
270+
}
271+
272+
@Override
273+
public Object getArray(long index, int count, Map<String, Class<?>> map) throws SQLException {
274+
return elements;
275+
}
276+
277+
@Override
278+
public ResultSet getResultSet() throws SQLException {
279+
return null;
280+
}
281+
282+
@Override
283+
public ResultSet getResultSet(Map<String, Class<?>> map) throws SQLException {
284+
return null;
285+
}
286+
287+
@Override
288+
public ResultSet getResultSet(long index, int count) throws SQLException {
289+
return null;
290+
}
291+
292+
@Override
293+
public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map) throws SQLException {
294+
return null;
295+
}
296+
297+
@Override
298+
public void free() throws SQLException {
299+
// do nothing
300+
}
301+
}
302+
303+
/**
304+
* Mock Array class with null elements for testing edge cases
305+
*/
306+
private static class MockArrayWithNulls implements Array {
307+
private final Object[] elements = {1L, null, 3L};
308+
309+
@Override
310+
public String getBaseTypeName() throws SQLException {
311+
return "int8";
312+
}
313+
314+
@Override
315+
public int getBaseType() throws SQLException {
316+
return Types.BIGINT;
317+
}
318+
319+
@Override
320+
public Object getArray() throws SQLException {
321+
return elements;
322+
}
323+
324+
@Override
325+
public Object getArray(Map<String, Class<?>> map) throws SQLException {
326+
return elements;
327+
}
328+
329+
@Override
330+
public Object getArray(long index, int count) throws SQLException {
331+
return elements;
332+
}
333+
334+
@Override
335+
public Object getArray(long index, int count, Map<String, Class<?>> map) throws SQLException {
336+
return elements;
337+
}
338+
339+
@Override
340+
public ResultSet getResultSet() throws SQLException {
341+
return null;
342+
}
343+
344+
@Override
345+
public ResultSet getResultSet(Map<String, Class<?>> map) throws SQLException {
346+
return null;
347+
}
348+
349+
@Override
350+
public ResultSet getResultSet(long index, int count) throws SQLException {
351+
return null;
352+
}
353+
354+
@Override
355+
public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map) throws SQLException {
356+
return null;
357+
}
358+
359+
@Override
360+
public void free() throws SQLException {
361+
// do nothing
362+
}
363+
}
364+
184365
@Override
185366
public UndoLogParser getParser() {
186367
return parser;

0 commit comments

Comments
 (0)