From 9d097a394726adeb4be31e062023d55448325713 Mon Sep 17 00:00:00 2001 From: Mikhail Dzianishchyts Date: Thu, 27 Nov 2025 19:12:45 +0300 Subject: [PATCH 1/2] Migrate UUID mapping to ExtendedType Supports binary mapping --- .../apache/cayenne/access/types/UUIDType.java | 200 ++++++++++++++++++ .../cayenne/access/types/UUIDValueType.java | 1 + .../configuration/runtime/CoreModule.java | 2 +- .../org/apache/cayenne/access/UUIDIT.java | 32 +++ .../testdo/uuid/UuidBinTestEntity.java | 9 + .../testdo/uuid/auto/_UuidBinTestEntity.java | 94 ++++++++ .../unit/di/runtime/RuntimeCaseModule.java | 2 +- cayenne/src/test/resources/uuid.map.xml | 7 + 8 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java create mode 100644 cayenne/src/test/java/org/apache/cayenne/testdo/uuid/UuidBinTestEntity.java create mode 100644 cayenne/src/test/java/org/apache/cayenne/testdo/uuid/auto/_UuidBinTestEntity.java diff --git a/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java new file mode 100644 index 0000000000..e9d0e04e78 --- /dev/null +++ b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java @@ -0,0 +1,200 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cayenne.access.types; + +import java.nio.ByteBuffer; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.UUID; + +import org.apache.cayenne.CayenneRuntimeException; + +/** + * ExtendedType for java.util.UUID that supports string and binary JDBC columns.
+ * For char columns, stores UUID as canonical string.
+ * For binary columns, stores UUID in 16-byte big-endian form.
+ * For other columns (e.g. PostgreSQL native UUID), delegates to driver's native handling where possible. + * + * @since 4.2 + */ +public class UUIDType implements ExtendedType { + + private static final int UUID_BYTES = 2 * Long.BYTES; + + @Override + public String getClassName() { + return UUID.class.getName(); + } + + @Override + public UUID materializeObject(ResultSet rs, int index, int type) throws Exception { + switch (type) { + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: { + String s = rs.getString(index); + return s != null ? UUID.fromString(s) : null; + } + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: { + byte[] b = rs.getBytes(index); + return b != null ? fromBytes(b) : null; + } + case Types.BLOB: { + return fromBlob(rs.getBlob(index)); + } + default: { + return fromObject(rs.getObject(index)); + } + } + } + + @Override + public UUID materializeObject(CallableStatement cs, int index, int type) throws Exception { + switch (type) { + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: { + String s = cs.getString(index); + return s != null ? UUID.fromString(s) : null; + } + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: { + byte[] b = cs.getBytes(index); + return b != null ? fromBytes(b) : null; + } + case Types.BLOB: { + return fromBlob(cs.getBlob(index)); + } + default: { + Object o = cs.getObject(index); + return fromObject(o); + } + } + } + + @Override + public void setJdbcObject(PreparedStatement statement, UUID value, int pos, int type, int precision) throws Exception { + if (value == null) { + statement.setNull(pos, type); + return; + } + + switch (type) { + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: { + statement.setString(pos, value.toString()); + return; + } + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: { + statement.setBytes(pos, toBytes(value)); + return; + } + case Types.BLOB: { + byte[] b = toBytes(value); + if (precision != -1) { + statement.setObject(pos, b, type, precision); + } else { + statement.setObject(pos, b, type); + } + return; + } + case Types.OTHER: { + // native UUID + statement.setObject(pos, value); + return; + } + default: { + if (precision != -1) { + statement.setObject(pos, value.toString(), type, precision); + } else { + statement.setObject(pos, value.toString(), type); + } + } + } + } + + @Override + public String toString(UUID value) { + return value == null ? "NULL" : value.toString(); + } + + private static UUID fromBlob(Blob blob) throws SQLException { + if (blob == null) { + return null; + } + long len = blob.length(); + if (len == 0) { + return null; + } + if (len != UUID_BYTES) { + invalidUuidLength(len); + } + byte[] b = blob.getBytes(1, (int) len); + return fromBytes(b); + } + + private static UUID fromObject(Object o) { + if (o == null) { + return null; + } + if (o instanceof UUID) { + return (UUID) o; + } + if (o instanceof byte[]) { + return fromBytes((byte[]) o); + } + return UUID.fromString(o.toString()); + } + + private static UUID fromBytes(byte[] bytes) { + if (bytes == null) { + return null; + } + if (bytes.length != UUID_BYTES) { + invalidUuidLength(bytes.length); + } + ByteBuffer bb = ByteBuffer.wrap(bytes); + return new UUID(bb.getLong(), bb.getLong()); + } + + private static byte[] toBytes(UUID uuid) { + if (uuid == null) { + return null; + } + return ByteBuffer.allocate(Long.BYTES * 2) + .putLong(uuid.getMostSignificantBits()) + .putLong(uuid.getLeastSignificantBits()) + .array(); + } + + private static void invalidUuidLength(long length) { + throw new CayenneRuntimeException("Invalid UUID length, expected " + UUID_BYTES + " bytes: " + length); + } +} diff --git a/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDValueType.java b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDValueType.java index 60a3662e60..c09eaea261 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDValueType.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDValueType.java @@ -25,6 +25,7 @@ /** * @since 4.0 + * @deprecated since 4.2, use {@link UUIDType} instead */ public class UUIDValueType implements ValueObjectType { diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java index b7ba5ee1bb..f56477cd80 100644 --- a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java +++ b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java @@ -394,6 +394,7 @@ public void configure(Binder binder) { .addDefaultExtendedType(new UtilDateType()) .addDefaultExtendedType(new CalendarType<>(GregorianCalendar.class)) .addDefaultExtendedType(new CalendarType<>(Calendar.class)) + .addDefaultExtendedType(new UUIDType()) .addDefaultExtendedType(new GeoJsonType()) .addDefaultExtendedType(new WktType()) @@ -402,7 +403,6 @@ public void configure(Binder binder) { // ValueObjectTypes .addValueObjectType(BigIntegerValueType.class) .addValueObjectType(BigDecimalValueType.class) - .addValueObjectType(UUIDValueType.class) .addValueObjectType(LocalDateValueType.class) .addValueObjectType(LocalTimeValueType.class) .addValueObjectType(LocalDateTimeValueType.class) diff --git a/cayenne/src/test/java/org/apache/cayenne/access/UUIDIT.java b/cayenne/src/test/java/org/apache/cayenne/access/UUIDIT.java index 9871348752..70ddd75248 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/UUIDIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/UUIDIT.java @@ -18,6 +18,7 @@ ****************************************************************/ package org.apache.cayenne.access; +import java.nio.ByteBuffer; import java.util.UUID; import org.apache.cayenne.Cayenne; @@ -28,6 +29,7 @@ import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.test.jdbc.DBHelper; import org.apache.cayenne.test.jdbc.TableHelper; +import org.apache.cayenne.testdo.uuid.UuidBinTestEntity; import org.apache.cayenne.testdo.uuid.UuidPkEntity; import org.apache.cayenne.testdo.uuid.UuidTestEntity; import org.apache.cayenne.unit.di.runtime.CayenneProjects; @@ -49,10 +51,12 @@ public class UUIDIT extends RuntimeCase { private DBHelper dbHelper; private TableHelper uuidPkEntity; + private TableHelper uuidBinTable; @Before public void setUp() throws Exception { uuidPkEntity = new TableHelper(dbHelper, "UUID_PK_ENTITY", "ID"); + uuidBinTable = new TableHelper(dbHelper, "UUID_BIN_TEST"); } @Test @@ -116,4 +120,32 @@ public void testUUIDColumnSelect() throws Exception { assertEquals(id, readValue2); } + + @Test + public void testUUIDBinary_InsertSelect() { + UuidBinTestEntity test = context.newObject(UuidBinTestEntity.class); + UUID expected = UUID.randomUUID(); + test.setUuid(expected); + context.commitChanges(); + + UuidBinTestEntity testRead = ObjectSelect.query(UuidBinTestEntity.class).selectFirst(context); + assertNotNull(testRead.getUuid()); + assertEquals(expected, testRead.getUuid()); + } + + @Test + public void testUUIDBinary_RawBytes() throws Exception { + UuidBinTestEntity test = context.newObject(UuidBinTestEntity.class); + UUID expected = UUID.randomUUID(); + test.setUuid(expected); + context.commitChanges(); + + byte[] raw = uuidBinTable.getBytes("UUID"); + assertNotNull(raw); + assertEquals(16, raw.length); + + ByteBuffer bb = ByteBuffer.wrap(raw); + UUID actual = new UUID(bb.getLong(), bb.getLong()); + assertEquals(expected, actual); + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/UuidBinTestEntity.java b/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/UuidBinTestEntity.java new file mode 100644 index 0000000000..9d210130b6 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/UuidBinTestEntity.java @@ -0,0 +1,9 @@ +package org.apache.cayenne.testdo.uuid; + +import org.apache.cayenne.testdo.uuid.auto._UuidBinTestEntity; + +public class UuidBinTestEntity extends _UuidBinTestEntity { + + private static final long serialVersionUID = 1L; + +} diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/auto/_UuidBinTestEntity.java b/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/auto/_UuidBinTestEntity.java new file mode 100644 index 0000000000..341e773f41 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/auto/_UuidBinTestEntity.java @@ -0,0 +1,94 @@ +package org.apache.cayenne.testdo.uuid.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.UUID; + +import org.apache.cayenne.PersistentObject; +import org.apache.cayenne.exp.property.BaseProperty; +import org.apache.cayenne.exp.property.NumericIdProperty; +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.SelfProperty; +import org.apache.cayenne.testdo.uuid.UuidBinTestEntity; + +/** + * Class _UuidBinTestEntity was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _UuidBinTestEntity extends PersistentObject { + + private static final long serialVersionUID = 1L; + + public static final SelfProperty SELF = PropertyFactory.createSelf(UuidBinTestEntity.class); + + public static final NumericIdProperty ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "UuidBinTestEntity", Integer.class); + public static final String ID_PK_COLUMN = "ID"; + + public static final BaseProperty UUID = PropertyFactory.createBase("uuid", UUID.class); + + protected UUID uuid; + + + public void setUuid(UUID uuid) { + beforePropertyWrite("uuid", this.uuid, uuid); + this.uuid = uuid; + } + + public UUID getUuid() { + beforePropertyRead("uuid"); + return this.uuid; + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "uuid": + return this.uuid; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "uuid": + this.uuid = (UUID)val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.uuid); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.uuid = (UUID)in.readObject(); + } + +} diff --git a/cayenne/src/test/java/org/apache/cayenne/unit/di/runtime/RuntimeCaseModule.java b/cayenne/src/test/java/org/apache/cayenne/unit/di/runtime/RuntimeCaseModule.java index 6b2507e7b9..023156d465 100644 --- a/cayenne/src/test/java/org/apache/cayenne/unit/di/runtime/RuntimeCaseModule.java +++ b/cayenne/src/test/java/org/apache/cayenne/unit/di/runtime/RuntimeCaseModule.java @@ -193,12 +193,12 @@ public void configure(Binder binder) { .addDefaultExtendedType(new CalendarType<>(GregorianCalendar.class)) .addDefaultExtendedType(new CalendarType<>(Calendar.class)) .addDefaultExtendedType(new DurationType()) + .addDefaultExtendedType(new UUIDType()) .addExtendedTypeFactory(new InternalUnsupportedTypeFactory()) .addValueObjectType(BigIntegerValueType.class) .addValueObjectType(BigDecimalValueType.class) - .addValueObjectType(UUIDValueType.class) .addValueObjectType(LocalDateValueType.class) .addValueObjectType(LocalTimeValueType.class) .addValueObjectType(LocalDateTimeValueType.class) diff --git a/cayenne/src/test/resources/uuid.map.xml b/cayenne/src/test/resources/uuid.map.xml index 85730d5886..5404e11796 100644 --- a/cayenne/src/test/resources/uuid.map.xml +++ b/cayenne/src/test/resources/uuid.map.xml @@ -11,10 +11,17 @@ + + + + + + + From 19c45d38e337e1fb5c15cbf4d8475cee0f6b83ff Mon Sep 17 00:00:00 2001 From: Mikhail Dzianishchyts Date: Fri, 5 Dec 2025 16:46:25 +0300 Subject: [PATCH 2/2] Update UUIDType - Protected class members. - Format enum. - Drop support of BLOB and OTHER (cannot be mapped to native UUID) types. --- .../apache/cayenne/access/types/UUIDType.java | 125 ++++++------------ 1 file changed, 38 insertions(+), 87 deletions(-) diff --git a/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java index e9d0e04e78..acf5622d02 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java @@ -20,12 +20,12 @@ package org.apache.cayenne.access.types; import java.nio.ByteBuffer; -import java.sql.Blob; import java.sql.CallableStatement; +import java.sql.JDBCType; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.sql.SQLException; import java.sql.Types; +import java.util.List; import java.util.UUID; import org.apache.cayenne.CayenneRuntimeException; @@ -33,14 +33,13 @@ /** * ExtendedType for java.util.UUID that supports string and binary JDBC columns.
* For char columns, stores UUID as canonical string.
- * For binary columns, stores UUID in 16-byte big-endian form.
- * For other columns (e.g. PostgreSQL native UUID), delegates to driver's native handling where possible. + * For binary columns, stores UUID in 16-byte big-endian form. * * @since 4.2 */ public class UUIDType implements ExtendedType { - private static final int UUID_BYTES = 2 * Long.BYTES; + public static final int UUID_BYTES = 2 * Long.BYTES; @Override public String getClassName() { @@ -49,49 +48,34 @@ public String getClassName() { @Override public UUID materializeObject(ResultSet rs, int index, int type) throws Exception { - switch (type) { - case Types.CHAR: - case Types.VARCHAR: - case Types.LONGVARCHAR: { + switch (Format.forJdbcType(type)) { + case CHAR: { String s = rs.getString(index); return s != null ? UUID.fromString(s) : null; } - case Types.BINARY: - case Types.VARBINARY: - case Types.LONGVARBINARY: { + case BINARY: { byte[] b = rs.getBytes(index); return b != null ? fromBytes(b) : null; } - case Types.BLOB: { - return fromBlob(rs.getBlob(index)); - } default: { - return fromObject(rs.getObject(index)); + throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type)); } } } @Override public UUID materializeObject(CallableStatement cs, int index, int type) throws Exception { - switch (type) { - case Types.CHAR: - case Types.VARCHAR: - case Types.LONGVARCHAR: { + switch (Format.forJdbcType(type)) { + case CHAR: { String s = cs.getString(index); return s != null ? UUID.fromString(s) : null; } - case Types.BINARY: - case Types.VARBINARY: - case Types.LONGVARBINARY: { + case BINARY: { byte[] b = cs.getBytes(index); return b != null ? fromBytes(b) : null; } - case Types.BLOB: { - return fromBlob(cs.getBlob(index)); - } default: { - Object o = cs.getObject(index); - return fromObject(o); + throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type)); } } } @@ -103,39 +87,17 @@ public void setJdbcObject(PreparedStatement statement, UUID value, int pos, int return; } - switch (type) { - case Types.CHAR: - case Types.VARCHAR: - case Types.LONGVARCHAR: { + switch (Format.forJdbcType(type)) { + case CHAR: { statement.setString(pos, value.toString()); return; } - case Types.BINARY: - case Types.VARBINARY: - case Types.LONGVARBINARY: { + case BINARY: { statement.setBytes(pos, toBytes(value)); return; } - case Types.BLOB: { - byte[] b = toBytes(value); - if (precision != -1) { - statement.setObject(pos, b, type, precision); - } else { - statement.setObject(pos, b, type); - } - return; - } - case Types.OTHER: { - // native UUID - statement.setObject(pos, value); - return; - } default: { - if (precision != -1) { - statement.setObject(pos, value.toString(), type, precision); - } else { - statement.setObject(pos, value.toString(), type); - } + throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type)); } } } @@ -145,46 +107,18 @@ public String toString(UUID value) { return value == null ? "NULL" : value.toString(); } - private static UUID fromBlob(Blob blob) throws SQLException { - if (blob == null) { - return null; - } - long len = blob.length(); - if (len == 0) { - return null; - } - if (len != UUID_BYTES) { - invalidUuidLength(len); - } - byte[] b = blob.getBytes(1, (int) len); - return fromBytes(b); - } - - private static UUID fromObject(Object o) { - if (o == null) { - return null; - } - if (o instanceof UUID) { - return (UUID) o; - } - if (o instanceof byte[]) { - return fromBytes((byte[]) o); - } - return UUID.fromString(o.toString()); - } - - private static UUID fromBytes(byte[] bytes) { + protected static UUID fromBytes(byte[] bytes) { if (bytes == null) { return null; } if (bytes.length != UUID_BYTES) { - invalidUuidLength(bytes.length); + throw new CayenneRuntimeException("Invalid UUID length (" + bytes.length + "), expected " + UUID_BYTES); } ByteBuffer bb = ByteBuffer.wrap(bytes); return new UUID(bb.getLong(), bb.getLong()); } - private static byte[] toBytes(UUID uuid) { + protected static byte[] toBytes(UUID uuid) { if (uuid == null) { return null; } @@ -194,7 +128,24 @@ private static byte[] toBytes(UUID uuid) { .array(); } - private static void invalidUuidLength(long length) { - throw new CayenneRuntimeException("Invalid UUID length, expected " + UUID_BYTES + " bytes: " + length); + public enum Format { + CHAR(List.of(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR)), + BINARY(List.of(Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY)), + OTHER(List.of()); + + private final List jdbcTypes; + + Format(List jdbcTypes) { + this.jdbcTypes = jdbcTypes; + } + + public static Format forJdbcType(int jdbcType) { + for (Format format : values()) { + if (format.jdbcTypes.contains(jdbcType)) { + return format; + } + } + return OTHER; + } } }