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..acf5622d02
--- /dev/null
+++ b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java
@@ -0,0 +1,151 @@
+/*****************************************************************
+ * 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.CallableStatement;
+import java.sql.JDBCType;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Types;
+import java.util.List;
+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.
+ *
+ * @since 4.2
+ */
+public class UUIDType implements ExtendedType {
+
+ public 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 (Format.forJdbcType(type)) {
+ case CHAR: {
+ String s = rs.getString(index);
+ return s != null ? UUID.fromString(s) : null;
+ }
+ case BINARY: {
+ byte[] b = rs.getBytes(index);
+ return b != null ? fromBytes(b) : null;
+ }
+ default: {
+ throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type));
+ }
+ }
+ }
+
+ @Override
+ public UUID materializeObject(CallableStatement cs, int index, int type) throws Exception {
+ switch (Format.forJdbcType(type)) {
+ case CHAR: {
+ String s = cs.getString(index);
+ return s != null ? UUID.fromString(s) : null;
+ }
+ case BINARY: {
+ byte[] b = cs.getBytes(index);
+ return b != null ? fromBytes(b) : null;
+ }
+ default: {
+ throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type));
+ }
+ }
+ }
+
+ @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 (Format.forJdbcType(type)) {
+ case CHAR: {
+ statement.setString(pos, value.toString());
+ return;
+ }
+ case BINARY: {
+ statement.setBytes(pos, toBytes(value));
+ return;
+ }
+ default: {
+ throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type));
+ }
+ }
+ }
+
+ @Override
+ public String toString(UUID value) {
+ return value == null ? "NULL" : value.toString();
+ }
+
+ protected static UUID fromBytes(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+ if (bytes.length != UUID_BYTES) {
+ throw new CayenneRuntimeException("Invalid UUID length (" + bytes.length + "), expected " + UUID_BYTES);
+ }
+ ByteBuffer bb = ByteBuffer.wrap(bytes);
+ return new UUID(bb.getLong(), bb.getLong());
+ }
+
+ protected static byte[] toBytes(UUID uuid) {
+ if (uuid == null) {
+ return null;
+ }
+ return ByteBuffer.allocate(Long.BYTES * 2)
+ .putLong(uuid.getMostSignificantBits())
+ .putLong(uuid.getLeastSignificantBits())
+ .array();
+ }
+
+ 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;
+ }
+ }
+}
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 @@
+
+
+
+
+
+
+