Skip to content

Commit 37e7399

Browse files
committed
#24: @CustomValueType and FieldValueType improvements
- Add `@Deprecated(forRemoval=true)` and "will be removed in YOJ 3.0.0" warnings to `FieldValueType` pitfalls (`BINARY`, `isSortable()` etc.) - Remove `@CustomValueType.columnValueType` because it can always be deduced from `@CustomValueType.columnClass` instead. - Disallow *recursive* custom value types (that is, types whose converter in turn produce custom value types). - Allow preconverted values to be of a subclass of the column class. This is valuable if columnClass is a enum class, because Java makes anonymous inner classes of enums. Remove fast-path for postconverted values because it is never used by any real code (preconvert fast-path is used by `FieldValue`). - Add "map enum by ordinal" custom value type converter as a demonstration. Custom value types might become a viable alternatives for `@Column(dbQualifier="...")`.
1 parent 64c5315 commit 37e7399

File tree

16 files changed

+201
-100
lines changed

16 files changed

+201
-100
lines changed

databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import tech.ydb.yoj.ExperimentalApi;
44
import tech.ydb.yoj.databind.converter.ValueConverter;
5+
import tech.ydb.yoj.databind.schema.Column;
56
import tech.ydb.yoj.databind.schema.Schema;
67

78
import java.lang.annotation.Inherited;
@@ -16,24 +17,24 @@
1617
/**
1718
* Annotates the class, or entity field/record component as having a custom {@link ValueConverter value converter}.
1819
* <br>The specified converter will be used by YOJ instead of the default (Database column&harr;Java field) mapping.
19-
* <p>Annotation on entity field/record component has priority over annotation on the field's/record component's class.
20+
* <p>{@link Column#customValueType() @Column(customValueType=...)} annotation on an entity field/record component
21+
* has priority over annotation on the field's/record component's class.
2022
* <p>This annotation is <em>inherited</em>, so make sure that your {@link #converter() converter} either supports all
2123
* possible subclasses of your class, or restrict subclassing by making your class {@code final} or {@code sealed}.
24+
* <p>Defining <em>recursive</em> custom value types is prohibited: that is, you cannot have a custom value type with
25+
* a converter that returns value of {@link #columnClass() another custom value type}.
2226
*/
2327
@Inherited
2428
@Retention(RUNTIME)
2529
@Target({TYPE, FIELD, RECORD_COMPONENT})
2630
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24")
2731
public @interface CustomValueType {
2832
/**
29-
* Simple value type that the {@link #converter()} represents a custom value type as.
30-
* Cannot be {@link FieldValueType#COMPOSITE} or {@link FieldValueType#UNKNOWN}.
31-
*/
32-
FieldValueType columnValueType();
33-
34-
/**
35-
* Exact type of value that {@link #converter() converter's} {@link ValueConverter#toColumn(Schema.JavaField, Object) toColumn()} method returns.
36-
* Must implement {@link Comparable}.
33+
* Class of the values that the {@link ValueConverter#toColumn(Schema.JavaField, Object) toColumn()} method of the {@link #converter() converter}
34+
* returns.
35+
* <p>Column class itself cannot be a custom value type. It must be one of the {@link FieldValueType database column value types supported by YOJ}
36+
* and it must implement {@link Comparable}.
37+
* <p>It is allowed to return value of a subclass of {@code columnClass}, e.g. in case of {@code columnClass} being an {@code enum} class.
3738
*/
3839
@SuppressWarnings("rawtypes")
3940
Class<? extends Comparable> columnClass();

databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ private CustomValueTypes() {
2424
public static Object preconvert(@NonNull JavaField field, @NonNull Object value) {
2525
var cvt = field.getCustomValueType();
2626
if (cvt != null) {
27-
if (cvt.columnClass().equals(value.getClass())) {
28-
// Already preconverted
27+
if (cvt.columnClass().isInstance(value)) {
28+
// Value is already preconverted
2929
return value;
3030
}
3131

@@ -41,11 +41,6 @@ public static Object preconvert(@NonNull JavaField field, @NonNull Object value)
4141
public static Object postconvert(@NonNull JavaField field, @NonNull Object value) {
4242
var cvt = field.getCustomValueType();
4343
if (cvt != null) {
44-
if (field.getRawType().equals(value.getClass())) {
45-
// Already postconverted
46-
return value;
47-
}
48-
4944
value = createCustomValueTypeConverter(cvt).toJava(field, value);
5045
}
5146
return value;
@@ -74,15 +69,31 @@ public static CustomValueType getCustomValueType(@NonNull Type type, @Nullable C
7469
var columnCvt = cvtAnnotation == null || cvtAnnotation.converter().equals(ValueConverter.NoConverter.class) ? null : cvtAnnotation;
7570
var cvt = columnCvt == null ? rawType.getAnnotation(CustomValueType.class) : columnCvt;
7671
if (cvt != null) {
77-
Preconditions.checkArgument(!cvt.columnValueType().isComposite(), "@CustomValueType.columnValueType must be != COMPOSITE");
78-
Preconditions.checkArgument(!cvt.columnValueType().isUnknown(), "@CustomValueType.columnValueType must be != UNKNOWN");
72+
var columnClass = cvt.columnClass();
73+
74+
var recursiveCvt = getCustomValueType(columnClass, null);
75+
Preconditions.checkArgument(recursiveCvt == null,
76+
"Defining recursive custom value types is prohibited, but @CustomValueType.columnClass=%s is annotated with %s",
77+
columnClass.getCanonicalName(),
78+
recursiveCvt);
79+
80+
Preconditions.checkArgument(!columnClass.isInterface() && !isAbstract(columnClass.getModifiers()),
81+
"@CustomValueType.columnClass=%s must not be an interface or an abstract class", columnClass.getCanonicalName());
82+
83+
var fvt = FieldValueType.forJavaType(columnClass, null);
84+
Preconditions.checkArgument(!fvt.isComposite(),
85+
"@CustomValueType.columnClass=%s must not map to FieldValueType.COMPOSITE", columnClass.getCanonicalName());
86+
Preconditions.checkArgument(!fvt.isUnknown(),
87+
"@CustomValueType.columnClass=%s must not map to FieldValueType.UNKNOWN", columnClass.getCanonicalName());
88+
89+
var converterClass = cvt.converter();
7990
Preconditions.checkArgument(
80-
!cvt.converter().equals(ValueConverter.NoConverter.class)
81-
&& !cvt.converter().isInterface()
82-
&& !isAbstract(cvt.converter().getModifiers())
83-
&& (cvt.converter().getDeclaringClass() == null || isStatic(cvt.converter().getModifiers())),
84-
"@CustomValueType.converter must not be an interface, abstract class, non-static inner class, or NoConverter.class, but got: %s",
85-
cvt);
91+
!converterClass.equals(ValueConverter.NoConverter.class)
92+
&& !converterClass.isInterface()
93+
&& !isAbstract(converterClass.getModifiers())
94+
&& (converterClass.getDeclaringClass() == null || isStatic(converterClass.getModifiers())),
95+
"@CustomValueType.converter=%s must not be an interface, abstract class, non-static inner class, or NoConverter.class",
96+
converterClass.getCanonicalName());
8697
}
8798

8899
return cvt;

databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,30 +25,29 @@
2525
public enum FieldValueType {
2626
/**
2727
* Integer value.
28-
* Java-side <strong>must</strong> either be a numeric primitive, or extend {@link Number java.lang.Number} and
29-
* implement {@link Comparable java.lang.Comparable}.
28+
* Java-side <strong>must</strong> be a {@code long}, {@code int}, {@code short} or {@code byte},
29+
* or an instance of their wrapper classes {@code Long}, {@code Integer}, {@code Short} or {@code Byte}.
3030
*/
3131
INTEGER,
3232
/**
3333
* Real (floating-point) number value.
34-
* Java-side <strong>must</strong> either be a numeric primitive, or extend {@link Number java.lang.Number} and
35-
* implement {@link Comparable java.lang.Comparable}.
34+
* Java-side <strong>must</strong> be a {@code double} or a {@code float}, or an instance of their
35+
* wrapper classes {@code Double} or {@code Float}.
3636
*/
3737
REAL,
3838
/**
3939
* String value.
40-
* Java-side <strong>must</strong> be serialized to simple string value.
40+
* Java-side <strong>must</strong> be a {@code String}.
4141
*/
4242
STRING,
4343
/**
4444
* Boolean value.
45-
* Java-side <strong>must</strong> either be an instance of {@link Boolean java.lang.Boolean} or a {@code boolean}
46-
* primitive.
45+
* Java-side <strong>must</strong> either be a {@code boolean} primitive, or an instance of its
46+
* wrapper class {@code Boolean}.
4747
*/
4848
BOOLEAN,
4949
/**
50-
* Enum value. Java-side <strong>must</strong> be an instance of {@link Enum java.lang.Enum}.<br>
51-
* Typically stored as {@link Enum#name() enum constant name} or its {@link Enum#ordinal() ordinal}.
50+
* Enum value. Java-side <strong>must</strong> be a concrete subclass of {@link Enum java.lang.Enum}.
5251
*/
5352
ENUM,
5453
/**
@@ -61,12 +60,14 @@ public enum FieldValueType {
6160
INTERVAL,
6261
/**
6362
* Binary value: just a stream of uninterpreted bytes.
64-
* Java-side <strong>must</strong> be a byte array.
63+
* Java-side <strong>must</strong> be a {@code byte[]}.
6564
* <p>
66-
* @deprecated It is strongly recommended to use a {@link ByteArray} that is properly {@code Comparable}
67-
* and has a sane {@code equals()}.
65+
*
66+
* @deprecated Support for mapping raw {@code byte[]} will be removed in YOJ 3.0.0.
67+
* Even now, it is strongly recommended to use a {@link ByteArray}: it is properly {@code Comparable}
68+
* and has a sane {@code equals()}, which ensures that queries will work the same for in-memory database and YDB.
6869
*/
69-
@Deprecated
70+
@Deprecated(forRemoval = true)
7071
BINARY,
7172
/**
7273
* Binary value: just a stream of uninterpreted bytes.
@@ -75,19 +76,24 @@ public enum FieldValueType {
7576
BYTE_ARRAY,
7677
/**
7778
* Composite value. Can contain any other values, including other composite values.<br>
78-
* Java-side must be an immutable POJO with all-args constructor, e.g. a Lombok {@code @Value}-annotated
79-
* class.
79+
* Java-side must be an immutable value reflectable by YOJ: a Java {@code Record},
80+
* a Kotlin {@code data class}, an immutable POJO with all-args constructor annotated with
81+
* {@code @ConstructorProperties} etc.
8082
*/
8183
COMPOSITE,
8284
/**
8385
* Polymorphic object stored in an opaque form (i.e., individual fields cannot be accessed by data binding).<br>
84-
* Serialized form strongly depends on the the marshalling mechanism (<em>e.g.</em>, JSON, YAML, ...).<br>
86+
* Serialized form strongly depends on the the marshalling mechanism (<em>e.g.</em>, JSON, YAML, ...).
8587
*/
8688
OBJECT,
8789
/**
90+
* @deprecated This enum constant will be removed in YOJ 3.0.0; {@link #forJavaType(Type, Column)} will instead
91+
* throw an {@code IllegalArgumentException} if an unmappable type is encountered.
92+
* <p>
8893
* Value type is unknown.<br>
8994
* It <em>might</em> be supported by the data binding implementation, but relying on that fact is not recommended.
9095
*/
96+
@Deprecated(forRemoval = true)
9197
UNKNOWN;
9298

9399
private static final Set<FieldValueType> SORTABLE_VALUE_TYPES = Set.of(
@@ -107,16 +113,13 @@ public enum FieldValueType {
107113
));
108114

109115
/**
110-
* @deprecated It is recommended to use the {@link CustomValueType} annotation with a {@link StringValueConverter}
111-
* instead of calling this method.
112-
* <p>
113-
* To register a class <em>not in your code</em> (e.g., {@code UUID} from the JDK) as a string-value type, use
114-
* a {@link Column &#64;Column(customValueType=&#64;CustomValueType(...))} annotation on the specific field.
115-
* <p>
116-
* Future versions of YOJ might remove this method entirely.
117-
*
118116
* @param clazz class to register as string-value. Must either be final or sealed with permissible final-only implementations.
119117
* All permissible implementations of a sealed class will be registered automatically.
118+
* @deprecated This method will be removed in YOJ 3.0.0.
119+
* Use the {@link CustomValueType} annotation with a {@link StringValueConverter} instead of calling this method.
120+
* <p>
121+
* To register a class <em>not in your code</em> (e.g., {@code UUID} from the JDK) as a string-value type, use
122+
* a {@link Column &#64;Column(customValueType=&#64;CustomValueType(...))} annotation on a specific field.
120123
*/
121124
@Deprecated(forRemoval = true)
122125
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24")
@@ -145,7 +148,7 @@ public static void registerStringValueType(@NonNull Class<?> clazz) {
145148
public static FieldValueType forJavaType(Type type, Column columnAnnotation) {
146149
var cvt = CustomValueTypes.getCustomValueType(type, columnAnnotation);
147150
if (cvt != null) {
148-
return cvt.columnValueType();
151+
type = cvt.columnClass();
149152
}
150153

151154
boolean flatten = columnAnnotation == null || columnAnnotation.flatten();
@@ -158,11 +161,6 @@ private static FieldValueType forJavaType(@NonNull Type type) {
158161
if (type instanceof ParameterizedType || type instanceof TypeVariable) {
159162
return OBJECT;
160163
} else if (type instanceof Class<?> clazz) {
161-
var cvt = CustomValueTypes.getCustomValueType(clazz, null);
162-
if (cvt != null) {
163-
return cvt.columnValueType();
164-
}
165-
166164
if (isStringValueType(clazz)) {
167165
return STRING;
168166
} else if (INTEGER_NUMERIC_TYPES.contains(clazz)) {
@@ -206,10 +204,13 @@ private static boolean isStringValueType(Class<?> clazz) {
206204
* Checks whether Java object of type {@code type} is mapped to a composite database value
207205
* (i.e. > 1 database field)
208206
*
209-
* @deprecated This method does not properly take into account the customizations specified in the
210-
* {@link Column &#64;Column} annotation on the field. Please do not call it directly, instead use
211-
* {@code FieldValueType.forJavaType(type, column).isComposite()} where {@code column} is the
212-
* {@link Column &#64;Column} annotation's value.
207+
* @deprecated This method will be removed in YOJ 3.0.0.
208+
* This method does not properly take into account the customizations specified in the
209+
* {@link Column &#64;Column} annotation on the field.
210+
* <br>Please do not call this method directly, instead use
211+
* {@link #forJavaType(Type, Column) FieldValueType.forJavaType(type, column).isComposite()}
212+
* where {@code column} is the {@link Column &#64;Column} annotation's value or {@code null} if
213+
* there is no annotation/you explicitly don't care.
213214
*
214215
* @param type Java object type
215216
* @return {@code true} if {@code type} maps to a composite database value; {@code false} otherwise
@@ -229,12 +230,28 @@ public boolean isComposite() {
229230
}
230231

231232
/**
233+
* @deprecated This method will be removed in YOJ 3.0.0 along with the {@link #UNKNOWN} enum constant.
234+
*
232235
* @return {@code true} if there is no fitting database value type for the type provided; {@code false} otherwise
233236
*/
237+
@Deprecated(forRemoval = true)
234238
public boolean isUnknown() {
235239
return this == UNKNOWN;
236240
}
237241

242+
/**
243+
* @deprecated This method will be removed in YOJ 3.0.0. This method is misleadingly named and is not generally useful.
244+
* <ul>
245+
* <li>It does not return the list of all Comparable single-column value types (INTERVAL and BOOLEAN are missing).
246+
* In fact, all single-column value types except for BINARY are Comparable.</li>
247+
* <li>What is considered <em>sortable</em> generally depends on your business logic.
248+
* <br>E.g.: Are boolean values sortable or not? They're certainly Comparable.
249+
* <br>E.g.: How do you sort columns with FieldValueType.STRING? Depends on your Locale for in-memory DB and your locale+collation+phase of the moon
250+
* for a real database
251+
* <br><em>etc.</em></li>
252+
* </ul>
253+
*/
254+
@Deprecated(forRemoval = true)
238255
public boolean isSortable() {
239256
return SORTABLE_VALUE_TYPES.contains(this);
240257
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package tech.ydb.yoj.databind.converter;
2+
3+
import com.google.common.base.Preconditions;
4+
import lombok.NonNull;
5+
import tech.ydb.yoj.databind.schema.Schema.JavaField;
6+
7+
/**
8+
* A generic converter that can be applied to represent your enum values as their {@link Enum#ordinal() ordinal}s
9+
* instead of their {@link Enum#name() constant name}s or {@link Enum#toString() string representation}s.
10+
* You can use it in a {@link tech.ydb.yoj.databind.schema.Column @Column} annotation, like this:
11+
* <blockquote><pre>
12+
* &#64;Column(
13+
* customValueType=&#64;CustomValueType(
14+
* columnClass=Integer.class,
15+
* converter=EnumOrdinalConverter.class
16+
* )
17+
* )
18+
* </pre></blockquote>
19+
* or as a global default for some of your enum type, like this:
20+
* <blockquote><pre>
21+
* &#64;CustomValueType(
22+
* columnClass=Integer.class,
23+
* converter=EnumOrdinalConverter.class
24+
* )
25+
* public enum MyEnum {
26+
* FOO,
27+
* BAR,
28+
* }
29+
* </pre></blockquote>
30+
*
31+
* @param <E> Java type
32+
*/
33+
public final class EnumOrdinalConverter<E extends Enum<E>> implements ValueConverter<E, Integer> {
34+
private EnumOrdinalConverter() {
35+
}
36+
37+
@Override
38+
public @NonNull Integer toColumn(@NonNull JavaField field, @NonNull E value) {
39+
return value.ordinal();
40+
}
41+
42+
@Override
43+
public @NonNull E toJava(@NonNull JavaField field, @NonNull Integer ordinal) {
44+
@SuppressWarnings("unchecked")
45+
E[] constants = (E[]) field.getRawType().getEnumConstants();
46+
Preconditions.checkState(constants != null, "Not an enum field: %s", field);
47+
Preconditions.checkArgument(ordinal >= 0, "Negative ordinal %s for field %s", ordinal, field);
48+
Preconditions.checkArgument(ordinal < constants.length, "Unknown enum ordinal %s for field %s", ordinal, field);
49+
50+
return constants[ordinal];
51+
}
52+
}

databind/src/main/java/tech/ydb/yoj/databind/converter/StringValueConverter.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
* <pre>
1919
* &#64;Column(
2020
* customValueType=&#64;CustomValueType(
21-
* columnValueType=STRING,
22-
* columnClass=&lt;your type&gt;,
21+
* columnClass=String.class,
2322
* converter=StringValueConverter.class
2423
* )
2524
* )

databind/src/main/java/tech/ydb/yoj/databind/converter/ValueConverter.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,41 @@
88
* Custom conversion logic between database column values and Java field values.
99
* <br><strong>Must</strong> have a no-args public constructor.
1010
*
11-
* @param <J> Java value type
12-
* @param <C> Database column value type
11+
* @param <J> Java field value type
12+
* @param <C> Database column value type. <strong>Must not</strong> be the same type as {@code <J>}.
1313
*/
1414
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24")
1515
public interface ValueConverter<J, C> {
16+
/**
17+
* Converts a field value to a {@link tech.ydb.yoj.databind.FieldValueType database column value} supported by YOJ.
18+
*
19+
* @param field schema field
20+
* @param v field value, guaranteed to not be {@code null}
21+
* @return database column value corresponding to the Java field value, must not be {@code null}
22+
*
23+
* @see #toJava(JavaField, Object)
24+
*/
1625
@NonNull
1726
C toColumn(@NonNull JavaField field, @NonNull J v);
1827

28+
/**
29+
* Converts a database column value to a Java field value.
30+
*
31+
* @param field schema field
32+
* @param c database column value, guaranteed to not be {@code null}
33+
* @return Java field value corresponding to the database column value, must not be {@code null}
34+
*
35+
* @see #toColumn(JavaField, Object)
36+
*/
1937
@NonNull
2038
J toJava(@NonNull JavaField field, @NonNull C c);
2139

22-
class NoConverter implements ValueConverter<Void, Void> {
40+
/**
41+
* Represents "no custom converter is defined" for {@link tech.ydb.yoj.databind.CustomValueType @CustomValueType}
42+
* annotation inside a {@link tech.ydb.yoj.databind.schema.Column @Column} annotation.
43+
* <p>Non-instantiable, every method including the constructor throws {@link UnsupportedOperationException}.
44+
*/
45+
final class NoConverter implements ValueConverter<Void, Void> {
2346
private NoConverter() {
2447
throw new UnsupportedOperationException("Not instantiable");
2548
}

0 commit comments

Comments
 (0)