diff --git a/asql-api/src/main/java/me/zort/sqllib/api/DefsVals.java b/asql-api/src/main/java/me/zort/sqllib/api/DefsVals.java new file mode 100644 index 0000000..f0c275e --- /dev/null +++ b/asql-api/src/main/java/me/zort/sqllib/api/DefsVals.java @@ -0,0 +1,13 @@ +package me.zort.sqllib.api; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.concurrent.atomic.AtomicReference; + +@AllArgsConstructor +@Getter +public class DefsVals { + private final String[] defs; + private final AtomicReference[] vals; +} diff --git a/asql-api/src/main/java/me/zort/sqllib/api/ObjectMapper.java b/asql-api/src/main/java/me/zort/sqllib/api/ObjectMapper.java index 5cdffc2..4c18be7 100644 --- a/asql-api/src/main/java/me/zort/sqllib/api/ObjectMapper.java +++ b/asql-api/src/main/java/me/zort/sqllib/api/ObjectMapper.java @@ -4,13 +4,16 @@ import org.jetbrains.annotations.NotNull; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; import java.lang.reflect.Type; public interface ObjectMapper { void registerBackupValueResolver(@NotNull FieldValueResolver resolver); + void registerAdapter(@NotNull Class typeClass, @NotNull TypeAdapter adapter); - T assignValues(Row row, Class typeClass); + T deserializeValues(Row row, Class typeClass); + DefsVals serializeValues(Object obj); interface FieldValueResolver { Object obtainValue(SQLConnection connection, @@ -20,4 +23,30 @@ Object obtainValue(SQLConnection connection, String convertedName, Type type); } + + interface TypeAdapter { // TODO: Tests + /** + * Deserializes value from database to Java value. + * This value is commonly set to field value in corresponding + * object. Don't set the value to the element manually! + * + * @param element Element to deserialize value for. + * @param row Row to deserialize value from. + * @param raw Raw value to deserialize. + * @return Deserialized value that will be set to the element. + */ + T deserialize(AnnotatedElement element, Row row, Object raw); + + /** + * Serializes value to be inserted into database. + * + * @param element Element to serialize value for. + * @param value Value to serialize. + * This value should be suitable to the database type, + * so you should always check if the value corresponds to + * the column type, etc. + * @return The serialized value that will be inserted into database. + */ + Object serialize(AnnotatedElement element, Object value); + } } diff --git a/asql-core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java b/asql-core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java index 1492744..c395c93 100644 --- a/asql-core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java +++ b/asql-core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java @@ -1,15 +1,14 @@ package me.zort.sqllib; -import lombok.AllArgsConstructor; import lombok.Getter; +import me.zort.sqllib.api.DefsVals; +import me.zort.sqllib.api.ObjectMapper; import me.zort.sqllib.api.Query; import me.zort.sqllib.api.SQLConnection; import me.zort.sqllib.api.cache.CacheManager; import me.zort.sqllib.api.data.QueryResult; import me.zort.sqllib.api.data.QueryRowsResult; import me.zort.sqllib.api.data.Row; -import me.zort.sqllib.api.mapping.StatementMappingFactory; -import me.zort.sqllib.api.mapping.StatementMappingOptions; import me.zort.sqllib.api.model.SchemaSynchronizer; import me.zort.sqllib.api.model.TableSchema; import me.zort.sqllib.api.model.TableSchemaBuilder; @@ -71,6 +70,15 @@ public SQLDatabaseConnection(final @NotNull SQLConnectionFactory connectionFacto @ApiStatus.Experimental public abstract void setSchemaSynchronizer(SchemaSynchronizer synchronizer); + /** + * Sets the object mapper to use. + * Object mapper maps queries to objects, as specified in {@link SQLDatabaseConnection#query(Query, Class)}. + * + * @param objectMapper Object mapper to use. + */ + public abstract void setObjectMapper(final @NotNull ObjectMapper objectMapper); + public abstract ObjectMapper getObjectMapper(); + public abstract boolean buildEntitySchema(String tableName, Class entityClass); /** @@ -167,8 +175,6 @@ public SQLDatabaseConnection(final @NotNull SQLConnectionFactory connectionFacto public abstract boolean isTransactionActive(); - protected abstract DefsVals buildDefsVals(Object obj); - public abstract boolean isLogSqlErrors(); public abstract boolean isDebug(); @@ -220,12 +226,14 @@ public SQLDatabaseConnection cacheFor(long millis) { } public UpsertQuery save(final @NotNull String table, final @NotNull Object obj) { - if (buildDefsVals(obj) == null) throw new IllegalArgumentException("Cannot create save query! (defsVals == null)"); + if (getObjectMapper().serializeValues(obj) == null) { + throw new IllegalArgumentException("Cannot create save query! (defsVals == null)"); + } return save(obj).table(table); } public UpsertQuery save(final @NotNull Object obj) { - DefsVals defsVals = buildDefsVals(obj); + DefsVals defsVals = getObjectMapper().serializeValues(obj); if (defsVals == null) return null; String[] defs = defsVals.getDefs(); AtomicReference[] vals = defsVals.getVals(); @@ -242,7 +250,7 @@ public UpsertQuery save(final @NotNull Object obj) { } public QueryResult insert(final @NotNull String table, final @NotNull Object obj) { - DefsVals defsVals = buildDefsVals(obj); + DefsVals defsVals = getObjectMapper().serializeValues(obj); if (defsVals == null) return new QueryResultImpl(false); InsertQuery query = insert().into(table, defsVals.getDefs()); @@ -343,13 +351,6 @@ public interface CodeObserver { void onNotified(int code); } - @AllArgsConstructor - @Getter - protected static class DefsVals { - private final String[] defs; - private final AtomicReference[] vals; - } - public static final class Code { public static final int CONNECTED = 1; public static final int CLOSED = 2; diff --git a/asql-core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java b/asql-core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java index adce096..8e574f8 100644 --- a/asql-core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java +++ b/asql-core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java @@ -18,7 +18,6 @@ import me.zort.sqllib.api.model.TableSchemaBuilder; import me.zort.sqllib.api.options.NamingStrategy; import me.zort.sqllib.internal.Defaults; -import me.zort.sqllib.internal.annotation.JsonField; import me.zort.sqllib.internal.factory.SQLConnectionFactory; import me.zort.sqllib.internal.fieldResolver.ConstructorParameterResolver; import me.zort.sqllib.internal.fieldResolver.LinkedOneFieldResolver; @@ -29,19 +28,13 @@ import me.zort.sqllib.model.builder.EntitySchemaBuilder; import me.zort.sqllib.pool.PooledSQLDatabaseConnection; import me.zort.sqllib.transaction.Transaction; -import me.zort.sqllib.util.Validator; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.sql.*; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; /** @@ -72,6 +65,7 @@ static SQLDatabaseOptions defaultOptions() { @Getter private final ISQLDatabaseOptions options; + @Getter private transient ObjectMapper objectMapper; private transient CacheManager cacheManager; @Setter @@ -130,6 +124,7 @@ public void registerBackupValueResolver(final @NotNull ObjectMapper.FieldValueRe * * @param objectMapper Object mapper to use. */ + @Override public void setObjectMapper(final @NotNull ObjectMapper objectMapper) { this.objectMapper = Objects.requireNonNull(objectMapper, "Object mapper cannot be null!"); } @@ -254,7 +249,7 @@ public QueryRowsResult query(final @NotNull Query query, final @NotNull C QueryRowsResult result = new QueryRowsResult<>(resultRows.isSuccessful()); for (Row row : resultRows) { - Optional.ofNullable(objectMapper.assignValues(row, typeClass)) + Optional.ofNullable(objectMapper.deserializeValues(row, typeClass)) .ifPresent(result::add); } return result; @@ -360,48 +355,6 @@ public QueryResult exec(final @NotNull String query) { } } - @SuppressWarnings("unchecked") - @Nullable - protected final DefsVals buildDefsVals(Object obj) { - Objects.requireNonNull(obj); - - Class aClass = obj.getClass(); - - Map fields = new HashMap<>(); - for (Field field : aClass.getDeclaredFields()) { - - if (Modifier.isTransient(field.getModifiers())) { - // Transient fields are ignored. - continue; - } - - try { - field.setAccessible(true); - Object o = field.get(obj); - if (field.isAnnotationPresent(JsonField.class)) { - o = options.getGson().toJson(o); - } else if (Validator.validateAutoIncrement(field) && field.get(obj) == null) { - // If field is PrimaryKey and autoIncrement true and is null, - // We will skip this to use auto increment strategy on SQL server. - continue; - } - fields.put(options.getNamingStrategy().fieldNameToColumn(field.getName()), o); - } catch (IllegalAccessException e) { - e.printStackTrace(); - return null; - } - } - // I make entry array for indexing safety. - Map.Entry[] entryArray = fields.entrySet().toArray(new Map.Entry[0]); - String[] defs = new String[entryArray.length]; - AtomicReference[] vals = new AtomicReference[entryArray.length]; - for (int i = 0; i < entryArray.length; i++) { - defs[i] = entryArray[i].getKey(); - vals[i] = new AtomicReference<>(entryArray[i].getValue()); - } - return new DefsVals(defs, vals); - } - @ApiStatus.Experimental @SneakyThrows(SQLException.class) public final Transaction beginTransaction() { diff --git a/asql-core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnection.java b/asql-core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnection.java index 02d155f..f0d03ee 100644 --- a/asql-core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnection.java +++ b/asql-core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnection.java @@ -1,5 +1,6 @@ package me.zort.sqllib; +import me.zort.sqllib.api.DefsVals; import me.zort.sqllib.api.ISQLDatabaseOptions; import me.zort.sqllib.api.Query; import me.zort.sqllib.api.data.QueryResult; @@ -65,7 +66,7 @@ private void setup() { @NotNull @Override public final UpsertQuery save(@NotNull String table, @NotNull Object obj) { - DefsVals defsVals = buildDefsVals(obj); + DefsVals defsVals = getObjectMapper().serializeValues(obj); if (defsVals == null) throw new IllegalArgumentException("Cannot create save query! (defsVals == null)"); String[] defs = defsVals.getDefs(); AtomicReference[] vals = defsVals.getVals(); diff --git a/asql-core/src/main/java/me/zort/sqllib/internal/impl/DefaultObjectMapper.java b/asql-core/src/main/java/me/zort/sqllib/internal/impl/DefaultObjectMapper.java index 9721458..9c2fe8b 100644 --- a/asql-core/src/main/java/me/zort/sqllib/internal/impl/DefaultObjectMapper.java +++ b/asql-core/src/main/java/me/zort/sqllib/internal/impl/DefaultObjectMapper.java @@ -4,15 +4,23 @@ import lombok.AccessLevel; import lombok.Getter; import me.zort.sqllib.SQLDatabaseConnectionImpl; +import me.zort.sqllib.api.DefsVals; +import me.zort.sqllib.api.ISQLDatabaseOptions; import me.zort.sqllib.api.ObjectMapper; import me.zort.sqllib.api.data.Row; import me.zort.sqllib.internal.annotation.JsonField; +import me.zort.sqllib.util.Validator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.*; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; public class DefaultObjectMapper implements ObjectMapper { @@ -20,10 +28,12 @@ public class DefaultObjectMapper implements ObjectMapper { // in mapped object as backup. @Getter(AccessLevel.PROTECTED) private final List backupValueResolvers; + private final Map, ObjectMapper.TypeAdapter> typeAdapters; private final SQLDatabaseConnectionImpl connectionWrapper; public DefaultObjectMapper(SQLDatabaseConnectionImpl connectionWrapper) { this.backupValueResolvers = new CopyOnWriteArrayList<>(); + this.typeAdapters = new ConcurrentHashMap<>(); this.connectionWrapper = connectionWrapper; } @@ -32,8 +42,13 @@ public void registerBackupValueResolver(@NotNull FieldValueResolver resolver) { this.backupValueResolvers.add(resolver); } + @Override + public void registerAdapter(@NotNull Class typeClass, @NotNull TypeAdapter adapter) { + this.typeAdapters.put(typeClass, adapter); + } + @Nullable - public T assignValues(Row row, Class typeClass) { + public T deserializeValues(Row row, Class typeClass) { T instance = null; try { try { @@ -113,6 +128,11 @@ private Object buildElementValue(AnnotatedElement element, Row row) { debug(String.format("Cannot find column for class %s target %s (%s)", declaringClass.getName(), name, converted)); return null; } + } else { + TypeAdapter typeAdapter = typeAdapters.get(type.getClass()); + if (typeAdapter != null) { + return typeAdapter.deserialize(element, row, obj); + } } if (element.isAnnotationPresent(JsonField.class) && obj instanceof String) { String jsonString = (String) obj; @@ -123,6 +143,51 @@ private Object buildElementValue(AnnotatedElement element, Row row) { } } + @Override + public DefsVals serializeValues(Object obj) { + Objects.requireNonNull(obj); + + Class aClass = obj.getClass(); + + Map fields = new HashMap<>(); + for (Field field : aClass.getDeclaredFields()) { + + if (Modifier.isTransient(field.getModifiers())) { + // Transient fields are ignored. + continue; + } + + ISQLDatabaseOptions options = connectionWrapper.getOptions(); + + try { + field.setAccessible(true); + Object o = field.get(obj); + if (typeAdapters.containsKey(field.getType())) { + o = typeAdapters.get(field.getType()).serialize(field, o); + } else if (field.isAnnotationPresent(JsonField.class)) { + o = options.getGson().toJson(o); + } else if (Validator.validateAutoIncrement(field) && field.get(obj) == null) { + // If field is PrimaryKey and autoIncrement true and is null, + // We will skip this to use auto increment strategy on SQL server. + continue; + } + fields.put(options.getNamingStrategy().fieldNameToColumn(field.getName()), o); + } catch (IllegalAccessException e) { + e.printStackTrace(); + return null; + } + } + // I make entry array for indexing safety. + Map.Entry[] entryArray = fields.entrySet().toArray(new Map.Entry[0]); + String[] defs = new String[entryArray.length]; + AtomicReference[] vals = new AtomicReference[entryArray.length]; + for (int i = 0; i < entryArray.length; i++) { + defs[i] = entryArray[i].getKey(); + vals[i] = new AtomicReference<>(entryArray[i].getValue()); + } + return new DefsVals(defs, vals); + } + private void debug(String message) { connectionWrapper.debug(message); } diff --git a/asql-core/src/main/java/me/zort/sqllib/mapping/annotation/Where.java b/asql-core/src/main/java/me/zort/sqllib/mapping/annotation/Where.java index 5bc18ff..d2136f1 100644 --- a/asql-core/src/main/java/me/zort/sqllib/mapping/annotation/Where.java +++ b/asql-core/src/main/java/me/zort/sqllib/mapping/annotation/Where.java @@ -42,7 +42,7 @@ public static WhereStatement build(Conditional parent, Where annotation, P case BT: case LT: try { - int number = Integer.parseInt(value); + long number = Long.parseLong(value); if (condition.type().equals(Condition.Type.BT)) { where.bt(condition.column(), number); } else {