diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index 40b7db04e..7c33fe549 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -281,9 +281,9 @@ public class ServerConfig { private boolean updatesDeleteMissingChildren = true; /** - * Setting to indicate if UUID should be stored as binary(16) or varchar(40). + * Setting to indicate if UUID should be stored as binary(16) or varchar(40) or native DB type (for H2 and Postgres). */ - private boolean uuidStoreAsBinary; + private DbUuid dbUuid = DbUuid.AUTO; private List findControllers = new ArrayList(); @@ -1466,19 +1466,18 @@ public class ServerConfig { this.dbEncrypt = dbEncrypt; } - /** - * Return true if UUID should be stored as binary(16) (as opposed to varchar(40)). + * Return the DB type used to store UUID. */ - public boolean isUuidStoreAsBinary() { - return uuidStoreAsBinary; + public DbUuid getDbUuid() { + return dbUuid; } /** - * Set to true if UUID should be stored as binary(16) (as opposed to varchar(40)). + * Set the DB type used to store UUID. */ - public void setUuidStoreAsBinary(boolean uuidStoreAsBinary) { - this.uuidStoreAsBinary = uuidStoreAsBinary; + public void setDbUuid(DbUuid dbUuid) { + this.dbUuid = dbUuid; } /** @@ -2152,7 +2151,10 @@ public class ServerConfig { databaseBooleanTrue = p.get("databaseBooleanTrue", databaseBooleanTrue); databaseBooleanFalse = p.get("databaseBooleanFalse", databaseBooleanFalse); databasePlatformName = p.get("databasePlatformName", databasePlatformName); - uuidStoreAsBinary = p.getBoolean("uuidStoreAsBinary", uuidStoreAsBinary); + dbUuid = p.getEnum(DbUuid.class, "dbuuid", dbUuid); + if (p.getBoolean("uuidStoreAsBinary", false)) { + dbUuid = DbUuid.BINARY; + } localTimeWithNanos = p.getBoolean("localTimeWithNanos", localTimeWithNanos); lazyLoadBatchSize = p.getInt("lazyLoadBatchSize", lazyLoadBatchSize); @@ -2274,4 +2276,25 @@ public class ServerConfig { public void setExpressionEqualsWithNullAsNoop(boolean expressionEqualsWithNullAsNoop) { this.expressionEqualsWithNullAsNoop = expressionEqualsWithNullAsNoop; } + + /** + * Specify how UUID is stored. + */ + public enum DbUuid { + + /** + * Store using native UUID in H2 and Postgres. + */ + AUTO, + + /** + * Store using DB VARCHAR. + */ + VARCHAR, + + /** + * Store using DB BINARY. + */ + BINARY + } } diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java index e73c319c4..a4c8f73e9 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java @@ -1,18 +1,16 @@ package com.avaje.ebean.config.dbplatform; -import java.sql.Types; - -import javax.sql.DataSource; - import com.avaje.ebean.BackgroundExecutor; import com.avaje.ebean.Query; - import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.dbmigration.ddlgeneration.DdlHandler; import com.avaje.ebean.dbmigration.ddlgeneration.platform.PlatformDdl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.sql.DataSource; +import java.sql.Types; + /** * Database platform specific settings. */ @@ -54,28 +52,33 @@ public class DatabasePlatform { */ protected OnQueryOnly onQueryOnly = OnQueryOnly.ROLLBACK; - /** - * The open quote used by quoted identifiers. + /** + * The open quote used by quoted identifiers. */ protected String openQuote = "\""; /** - * The close quote used by quoted identifiers. + * The close quote used by quoted identifiers. */ protected String closeQuote = "\""; - /** - * For limit/offset, row_number etc limiting of SQL queries. + /** + * For limit/offset, row_number etc limiting of SQL queries. */ protected SqlLimiter sqlLimiter = new LimitOffsetSqlLimiter(); - /** - * Mapping of JDBC to Database types. + /** + * Mapping of JDBC to Database types. */ protected DbTypeMap dbTypeMap = new DbTypeMap(); /** - * Defines DB identity/sequence features. + * Set to true if the DB has native UUID type support. + */ + protected boolean nativeUuidType; + + /** + * Defines DB identity/sequence features. */ protected DbIdentity dbIdentity = new DbIdentity(); @@ -84,28 +87,28 @@ public class DatabasePlatform { */ protected DbHistorySupport historySupport; - /** - * The JDBC type to map booleans to (by default). + /** + * The JDBC type to map booleans to (by default). */ protected int booleanDbType = Types.BOOLEAN; - /** - * The JDBC type to map Blob to. + /** + * The JDBC type to map Blob to. */ protected int blobDbType = Types.BLOB; - /** - * The JDBC type to map Clob to. + /** + * The JDBC type to map Clob to. */ protected int clobDbType = Types.CLOB; - /** - * For Oracle treat empty strings as null. + /** + * For Oracle treat empty strings as null. */ protected boolean treatEmptyStringsAsNull; - /** - * The database platform name. + /** + * The database platform name. */ protected String name = "generic"; @@ -124,7 +127,7 @@ public class DatabasePlatform { * The like clause. Can be overridden to disable default escape character. */ protected String likeClause = "like ?"; - + protected DbEncrypt dbEncrypt; protected boolean idInExpandedForm; @@ -219,16 +222,12 @@ public class DatabasePlatform { /** * Return a DB Sequence based IdGenerator. - * - * @param be - * the BackgroundExecutor that can be used to load the sequence if - * desired - * @param ds - * the DataSource - * @param seqName - * the name of the sequence - * @param batchSize - * the number of sequences that should be loaded + * + * @param be the BackgroundExecutor that can be used to load the sequence if + * desired + * @param ds the DataSource + * @param seqName the name of the sequence + * @param batchSize the number of sequences that should be loaded */ public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) { return null; @@ -276,9 +275,16 @@ public class DatabasePlatform { this.historySupport = historySupport; } + /** + * Return true if the DB supports native UUID. + */ + public boolean isNativeUuidType() { + return nativeUuidType; + } + /** * Return the mapping of JDBC to DB types. - * + * * @return the db type map */ public DbTypeMap getDbTypeMap() { @@ -315,7 +321,7 @@ public class DatabasePlatform { /** * Return the close quote for quoted identifiers. - * + * * @return the close quote */ public String getCloseQuote() { @@ -324,7 +330,7 @@ public class DatabasePlatform { /** * Return the open quote for quoted identifiers. - * + * * @return the open quote */ public String getOpenQuote() { @@ -333,7 +339,7 @@ public class DatabasePlatform { /** * Return the JDBC type used to store booleans. - * + * * @return the boolean db type */ public int getBooleanDbType() { @@ -363,7 +369,7 @@ public class DatabasePlatform { /** * Return true if empty strings should be treated as null. - * + * * @return true, if checks if is treat empty strings as null */ public boolean isTreatEmptyStringsAsNull() { @@ -399,7 +405,7 @@ public class DatabasePlatform { /** * Return the DB identity/sequence features for this platform. - * + * * @return the db identity */ public DbIdentity getDbIdentity() { @@ -412,7 +418,7 @@ public class DatabasePlatform { *

* Basically add the clauses for limit/offset, rownum, row_number(). *

- * + * * @return the sql limiter */ public SqlLimiter getSqlLimiter() { @@ -421,15 +427,12 @@ public class DatabasePlatform { /** * Convert backticks to the platform specific open quote and close quote - * *

* Specific plugins may implement this method to cater for platform specific * naming rules. *

- * - * @param dbName - * the db name - * + * + * @param dbName the db name * @return the string */ public String convertQuotedIdentifiers(String dbName) { diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java index 91caa245e..b2b7080aa 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java @@ -8,6 +8,11 @@ package com.avaje.ebean.config.dbplatform; */ public class DbType { + /** + * DB native UUID type (H2 and Postgres). + */ + public static final int UUID = 5010; + /** * Type to map Map content to Postgres HSTORE. */ diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java index 6f5f254ee..ab15a30e1 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java @@ -49,6 +49,8 @@ public class DbTypeMap { lookup.put("TIME", Types.TIME); lookup.put("TIMESTAMP", Types.TIMESTAMP); + lookup.put("UUID", DbType.UUID); + // Not standard java.sql.Types // logical JSON storage types lookup.put("JSON", DbType.JSON); @@ -101,6 +103,9 @@ public class DbTypeMap { put(Types.BLOB, new DbType("blob")); put(Types.CLOB, new DbType("clob")); + // DB native UUID support (H2 and Postgres) + put(DbType.UUID, new DbType("uuid")); + if (logicalTypes) { // keep it logical for 2 layer DDL generation put(DbType.HSTORE, new DbType("hstore")); diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java index 8e8189032..588e1a96d 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java @@ -16,6 +16,7 @@ public class H2Platform extends DatabasePlatform { this.dbEncrypt = new H2DbEncrypt(); this.platformDdl = new H2Ddl(this.dbTypeMap, dbIdentity); this.historySupport = new H2HistorySupport(); + this.nativeUuidType = true; // only support getGeneratedKeys with non-batch JDBC // so generally use SEQUENCE instead of IDENTITY for H2 diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java b/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java index c853ffc00..fdaacacdf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/Binder.java @@ -9,6 +9,7 @@ import java.util.List; import javax.persistence.PersistenceException; +import com.avaje.ebean.config.dbplatform.DbType; import com.avaje.ebeaninternal.api.BindParams; import com.avaje.ebeaninternal.server.core.JsonExpressionHandler; import com.avaje.ebeaninternal.server.core.Message; @@ -307,6 +308,11 @@ public class Binder { b.setBytes((byte[]) data); break; + case DbType.UUID: + // native UUID support in H2 and Postgres + b.setObject(data); + break; + case java.sql.Types.OTHER: b.setObject(data); break; diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java index c3d02d74e..ea1a7ee47 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java @@ -768,7 +768,6 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { */ protected void initialiseStandard(JsonConfig.DateTime mode, ServerConfig config) { - boolean binaryUUID = config.isUuidStoreAsBinary(); DatabasePlatform databasePlatform = config.getDatabasePlatform(); int platformClobType = databasePlatform.getClobDbType(); int platformBlobType = databasePlatform.getBlobDbType(); @@ -795,9 +794,17 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { nativeMap.put(Types.BIT, booleanType); } - // Store UUID as binary(16) or varchar(40) - ScalarType uuidType = (binaryUUID) ? new ScalarTypeUUIDBinary() : new ScalarTypeUUIDVarchar(); - typeMap.put(UUID.class, uuidType); + boolean nativeUuidType = databasePlatform.isNativeUuidType(); + ServerConfig.DbUuid dbUuid = config.getDbUuid(); + + if (nativeUuidType && dbUuid == ServerConfig.DbUuid.AUTO) { + // DB has native support for UUID + typeMap.put(UUID.class, new ScalarTypeUUIDNative()); + } else { + // Store UUID as binary(16) or varchar(40) + ScalarType uuidType = (ServerConfig.DbUuid.BINARY == dbUuid) ? new ScalarTypeUUIDBinary() : new ScalarTypeUUIDVarchar(); + typeMap.put(UUID.class, uuidType); + } typeMap.put(File.class, fileType); typeMap.put(InetAddress.class, inetAddressType); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeUUIDNative.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeUUIDNative.java new file mode 100644 index 000000000..a809f2dfd --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeUUIDNative.java @@ -0,0 +1,120 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebean.config.dbplatform.DbType; +import com.avaje.ebean.text.json.JsonWriter; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.sql.SQLException; +import java.util.UUID; + +/** + * Postgres Hstore type which maps Map to a single 'HStore column' in the DB. + */ +@SuppressWarnings("rawtypes") +public class ScalarTypeUUIDNative extends ScalarTypeBase { + + public ScalarTypeUUIDNative() { + super(UUID.class, false, DbType.UUID); + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public boolean isDirty(Object value) { + return true; + } + + @SuppressWarnings("unchecked") + @Override + public UUID read(DataReader dataReader) throws SQLException { + + Object value = dataReader.getObject(); + if (value == null) { + return null; + } + return (UUID)value; + } + + @Override + public void bind(DataBind b, UUID value) throws SQLException { + b.setObject(value); + } + + @Override + public Object toJdbcType(Object value) { + return value; + } + + @Override + public UUID toBeanType(Object value) { + return (UUID) value; + } + + @Override + public String formatValue(UUID v) { + return v.toString(); + } + + @Override + public UUID parse(String value) { + return UUID.fromString(value); + } + + @Override + public UUID convertFromMillis(long dateTime) { + throw new RuntimeException("Should never be called"); + } + + @Override + public boolean isDateTimeCapable() { + return false; + } + + @Override + public UUID readData(DataInput dataInput) throws IOException { + if (!dataInput.readBoolean()) { + return null; + } else { + String json = dataInput.readUTF(); + return parse(json); + } + } + + @Override + public void writeData(DataOutput dataOutput, UUID v) throws IOException { + if (v == null) { + dataOutput.writeBoolean(false); + } else { + dataOutput.writeBoolean(true); + String json = format(v); + dataOutput.writeUTF(json); + } + } + + @Override + public void jsonWrite(JsonWriter writer, String name, UUID value) throws IOException { + // write the field name followed by the Map/JSON Object + if (value == null) { + writer.writeNullField(name); + } else { + writer.writeStringField(name, formatValue(value)); + } + } + + @Override + public UUID jsonRead(JsonParser parser, JsonToken event) throws IOException { + // at this point the BeanProperty has read the START_OBJECT token + // to check for a null value. Pass the START_OBJECT token through to + // the EJson parsing so that it knows the first token has been read + String strValue = parser.getValueAsString(); + return strValue == null ? null : parse(strValue); + } + +} diff --git a/src/test/java/com/avaje/ebean/config/ServerConfigTest.java b/src/test/java/com/avaje/ebean/config/ServerConfigTest.java index 07c7ff72e..603ab289b 100644 --- a/src/test/java/com/avaje/ebean/config/ServerConfigTest.java +++ b/src/test/java/com/avaje/ebean/config/ServerConfigTest.java @@ -31,11 +31,13 @@ public class ServerConfigTest { Properties props = new Properties(); props.setProperty("persistBatch", "INSERT"); props.setProperty("persistBatchOnCascade", "INSERT"); + props.setProperty("dbuuid","binary"); serverConfig.loadFromProperties(props); assertEquals(PersistBatch.INSERT, serverConfig.getPersistBatch()); assertEquals(PersistBatch.INSERT, serverConfig.getPersistBatchOnCascade()); + assertEquals(ServerConfig.DbUuid.BINARY, serverConfig.getDbUuid()); serverConfig.setPersistBatch(PersistBatch.NONE); serverConfig.setPersistBatchOnCascade(PersistBatch.NONE); diff --git a/src/test/java/com/avaje/ebeaninternal/server/type/TestBinaryUUID.java b/src/test/java/com/avaje/ebeaninternal/server/type/TestBinaryUUID.java index e6659862d..25a8e9917 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/type/TestBinaryUUID.java +++ b/src/test/java/com/avaje/ebeaninternal/server/type/TestBinaryUUID.java @@ -1,22 +1,22 @@ package com.avaje.ebeaninternal.server.type; -import java.util.List; -import java.util.UUID; - -import org.junit.Assert; -import org.junit.Test; - import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; import com.avaje.ebean.SqlRow; import com.avaje.tests.model.basic.UUOne; +import org.junit.Test; + +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; public class TestBinaryUUID extends BaseTestCase { @Test public void test() { - - + UUOne one0 = new UUOne(); one0.setName("first one"); @@ -27,26 +27,31 @@ public class TestBinaryUUID extends BaseTestCase { Ebean.save(one0); Ebean.save(one1); - + UUOne fetch0 = Ebean.find(UUOne.class, one0.getId()); UUOne fetch1 = Ebean.find(UUOne.class, one1.getId()); - - Assert.assertEquals(one0.getId(), fetch0.getId()); - Assert.assertEquals(one0.getName(), fetch0.getName()); - Assert.assertEquals(one1.getId(), fetch1.getId()); - Assert.assertEquals(one1.getName(), fetch1.getName()); + assertEquals(one0.getId(), fetch0.getId()); + assertEquals(one0.getName(), fetch0.getName()); + + assertEquals(one1.getId(), fetch1.getId()); + assertEquals(one1.getName(), fetch1.getName()); String sql = "select id, name from uuone"; List list = Ebean.createSqlQuery(sql).findList(); for (SqlRow sqlRow : list) { Object sqlId = sqlRow.get("id"); - Assert.assertNotNull(sqlId); - Assert.assertTrue(sqlId instanceof byte[]); + assertNotNull(sqlId); UUID uuid = sqlRow.getUUID("id"); - Assert.assertNotNull(uuid); + assertNotNull(uuid); } - + + + String asJson = Ebean.json().toJson(fetch0); + UUOne bean = Ebean.json().toBean(UUOne.class, asJson); + + assertEquals(fetch0.getId(), bean.getId()); + assertEquals(fetch0.getName(), bean.getName()); } - + }