Skip to content

Commit d9ca930

Browse files
committed
Custom Value Types: unification and performance improvements (#24)
- Cache custom value types in `Schema.JavaField` - Remove legacy string value type conversion logic, only use `StringValueConverter` "under the hood" - Log attempts to use deprecated methods in `FieldValueType` - Improve documentation and add `@ObjectColumn` annotation to express `@Column(flatten=false)` more clearly
1 parent ea2d25c commit d9ca930

File tree

29 files changed

+419
-363
lines changed

29 files changed

+419
-363
lines changed

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,23 @@
1818
/**
1919
* Annotates the class, or entity field/record component as having a custom {@link ValueConverter value converter}.
2020
* <br>The specified converter will be used by YOJ instead of the default (Database column&harr;Java field) mapping.
21-
* <p>{@link Column#customValueType() @Column(customValueType=...)} annotation on an entity field/record component
22-
* has priority over annotation on the field's/record component's class.
23-
* <p>This annotation is <em>inherited</em>, so make sure that your {@link #converter() converter} either supports all
24-
* possible subclasses of your class, or restrict subclassing by making your class {@code final} or {@code sealed}.
25-
* <p>Defining <em>recursive</em> custom value types is prohibited: that is, you cannot have a custom value type with
21+
* <br>Defining <em>recursive</em> custom value types is prohibited: that is, you cannot have a custom value type with
2622
* a converter that returns value of {@link #columnClass() another custom value type}.
23+
* <ul>
24+
* <li>This annotation is <em>inherited</em>, so make sure that your {@link #converter() converter} either supports all
25+
* possible subclasses of your class, or restrict subclassing by making your class {@code final} or {@code sealed}.
26+
* <li>This is a <em>meta-annotation</em>: it can be applied to other annotations; if you use these annotations, YOJ
27+
* will correctly apply the {@code @CustomValueType} annotation. This allows to define custom value type configuration
28+
* once and then re-use it in multiple classes.
29+
* <li>{@link Column#customValueType() @Column(customValueType=...)} annotation on an entity field/record component
30+
* has priority over annotation on the field's/record component's class. {@link Column @Column} is also a meta-annotation,
31+
* so you can define custom value types for individual columns using {@code @Column.customValueType}.</li>
32+
* </ul>
33+
*
34+
* @see tech.ydb.yoj.databind.converter.StringValueConverter StringValueConverter
35+
* @see tech.ydb.yoj.databind.converter.StringColumn StringColumn
36+
* @see tech.ydb.yoj.databind.converter.StringValueType StringValueType
37+
* @see tech.ydb.yoj.databind.converter.EnumOrdinalConverter EnumOrdinalConverter
2738
*/
2839
@Inherited
2940
@Retention(RUNTIME)

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

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
import com.google.common.base.Preconditions;
44
import com.google.common.reflect.TypeToken;
55
import lombok.NonNull;
6-
import org.slf4j.Logger;
7-
import org.slf4j.LoggerFactory;
86
import tech.ydb.yoj.ExperimentalApi;
97
import tech.ydb.yoj.databind.converter.StringValueConverter;
108
import tech.ydb.yoj.databind.converter.ValueConverter;
119
import tech.ydb.yoj.databind.schema.Column;
1210
import tech.ydb.yoj.databind.schema.CustomConverterException;
11+
import tech.ydb.yoj.databind.schema.CustomValueTypeInfo;
1312
import tech.ydb.yoj.databind.schema.Schema.JavaField;
1413
import tech.ydb.yoj.util.lang.Annotations;
1514

@@ -19,63 +18,48 @@
1918

2019
import static java.lang.reflect.Modifier.isAbstract;
2120
import static java.lang.reflect.Modifier.isStatic;
22-
import static tech.ydb.yoj.databind.FieldValueType.STRING;
2321

2422
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24")
2523
public final class CustomValueTypes {
26-
private static final Logger log = LoggerFactory.getLogger(CustomValueTypes.class);
27-
2824
private CustomValueTypes() {
2925
}
3026

3127
public static Object preconvert(@NonNull JavaField field, @NonNull Object value) {
32-
var cvt = field.getCustomValueType();
33-
if (cvt != null) {
34-
if (cvt.columnClass().isInstance(value)) {
35-
// Value is already preconverted
36-
return value;
37-
}
38-
39-
value = createCustomValueTypeConverter(cvt).toColumn(field, value);
40-
41-
Preconditions.checkArgument(cvt.columnClass().isInstance(value),
42-
"Custom value type converter %s must produce a non-null value of type columnClass()=%s but got value of type %s",
43-
cvt.converter().getCanonicalName(), cvt.columnClass().getCanonicalName(), value.getClass().getCanonicalName());
44-
} else {
45-
// Legacy string-valued type (registered by FieldValueType.registerStringValueType())
46-
if (field.getValueType() == STRING && !String.class.equals(field.getRawType()) && !(value instanceof String)) {
47-
log.warn("You are using FieldValueType.registerStringValueType({}.class) which is deprecated for removal. "
48-
+ "Please use @StringColumn annotation on the Entity field or a @StringValueType annotation on the string-valued type",
49-
field.getRawType().getCanonicalName());
50-
return new StringValueConverter<>().toColumn(field, value);
51-
}
28+
var cvt = field.getCustomValueTypeInfo();
29+
if (cvt == null) {
30+
return value;
31+
}
32+
if (cvt.getColumnClass().isInstance(value)) {
33+
// Value is already preconverted
34+
return value;
5235
}
36+
37+
value = cvt.toColumn(field, value);
38+
Preconditions.checkArgument(cvt.getColumnClass().isInstance(value),
39+
"Custom value type converter %s must produce a non-null value of type columnClass()=%s but got value of type %s",
40+
cvt.getConverter().getClass().getCanonicalName(),
41+
cvt.getColumnClass().getCanonicalName(),
42+
value.getClass().getCanonicalName());
5343
return value;
5444
}
5545

56-
public static Object postconvert(@NonNull JavaField field, @NonNull Object value) {
57-
var cvt = field.getCustomValueType();
58-
if (cvt != null) {
59-
value = createCustomValueTypeConverter(cvt).toJava(field, value);
60-
} else {
61-
// Legacy string-valued type (registered by FieldValueType.registerStringValueType())
62-
if (field.getValueType() == STRING && !String.class.equals(field.getRawType()) && value instanceof String) {
63-
log.warn("You are using FieldValueType.registerStringValueType({}.class) which is deprecated for removal. "
64-
+ "Please use @StringColumn annotation on the Entity field or a @StringValueType annotation on the string-valued type",
65-
field.getRawType().getCanonicalName());
66-
return new StringValueConverter<>().toJava(field, (String) value);
67-
}
46+
public static <C extends Comparable<? super C>> Object postconvert(@NonNull JavaField field, @NonNull Object value) {
47+
CustomValueTypeInfo<?, C> cvt = field.getCustomValueTypeInfo();
48+
if (cvt == null) {
49+
return value;
6850
}
69-
return value;
51+
52+
Preconditions.checkArgument(value instanceof Comparable, "postconvert() only takes Comparable values, but got value of %s", value.getClass());
53+
54+
@SuppressWarnings("unchecked") C comparable = (C) value;
55+
return cvt.toJava(field, comparable);
7056
}
7157

72-
// TODO: Add caching to e.g. SchemaRegistry using @CustomValueType+[optionally JavaField if there is @Column annotation]+[type] as key,
73-
// to avoid repetitive construction of ValueConverters
74-
private static <V, C> ValueConverter<V, C> createCustomValueTypeConverter(CustomValueType cvt) {
58+
private static <J, C extends Comparable<? super C>> ValueConverter<J, C> createCustomValueTypeConverter(CustomValueType cvt) {
7559
try {
7660
var ctor = cvt.converter().getDeclaredConstructor();
7761
ctor.setAccessible(true);
78-
@SuppressWarnings("unchecked") var converter = (ValueConverter<V, C>) ctor.newInstance();
62+
@SuppressWarnings("unchecked") var converter = (ValueConverter<J, C>) ctor.newInstance();
7963
return converter;
8064
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | InvocationTargetException e) {
8165
throw new CustomConverterException(e, "Could not return custom value type converter " + cvt.converter());
@@ -84,9 +68,32 @@ private static <V, C> ValueConverter<V, C> createCustomValueTypeConverter(Custom
8468

8569
@Nullable
8670
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24")
87-
public static CustomValueType getCustomValueType(@NonNull Type type, @Nullable Column columnAnnotation) {
88-
var rawType = type instanceof Class<?> ? (Class<?>) type : TypeToken.of(type).getRawType();
71+
public static <J, C extends Comparable<? super C>> CustomValueTypeInfo<J, C> getCustomValueTypeInfo(
72+
@NonNull Type type, @Nullable Column columnAnnotation
73+
) {
74+
Class<?> rawType = type instanceof Class<?> ? (Class<?>) type : TypeToken.of(type).getRawType();
75+
CustomValueType cvt = getCustomValueType(rawType, columnAnnotation);
76+
if (cvt == null) {
77+
if (FieldValueType.isCustomStringValueType(rawType)) {
78+
@SuppressWarnings("unchecked")
79+
var legacyStringVtInfo = (CustomValueTypeInfo<J, C>) new CustomValueTypeInfo<>(String.class, new StringValueConverter<J>());
80+
81+
return legacyStringVtInfo;
82+
}
83+
84+
return null;
85+
}
86+
87+
@SuppressWarnings("unchecked")
88+
Class<C> columnClass = (Class<C>) cvt.columnClass();
8989

90+
ValueConverter<J, C> converter = createCustomValueTypeConverter(cvt);
91+
92+
return new CustomValueTypeInfo<>(columnClass, converter);
93+
}
94+
95+
@Nullable
96+
private static CustomValueType getCustomValueType(@NonNull Class<?> rawType, @Nullable Column columnAnnotation) {
9097
var cvtAnnotation = columnAnnotation == null ? null : columnAnnotation.customValueType();
9198

9299
var columnCvt = cvtAnnotation == null || cvtAnnotation.converter().equals(ValueConverter.NoConverter.class) ? null : cvtAnnotation;
@@ -103,10 +110,12 @@ public static CustomValueType getCustomValueType(@NonNull Type type, @Nullable C
103110
Preconditions.checkArgument(!columnClass.isInterface() && !isAbstract(columnClass.getModifiers()),
104111
"@CustomValueType.columnClass=%s must not be an interface or an abstract class", columnClass.getCanonicalName());
105112

106-
var fvt = FieldValueType.forJavaType(columnClass, null);
113+
var fvt = FieldValueType.forJavaType(columnClass);
107114
Preconditions.checkArgument(!fvt.isComposite(),
108115
"@CustomValueType.columnClass=%s must not map to FieldValueType.COMPOSITE", columnClass.getCanonicalName());
109-
Preconditions.checkArgument(!fvt.isUnknown(),
116+
117+
// TODO(entropia@): This won't be necessary when we remove FieldValueType.UNKNOWN in YOJ 3.0.0
118+
Preconditions.checkArgument(fvt != FieldValueType.UNKNOWN,
110119
"@CustomValueType.columnClass=%s must not map to FieldValueType.UNKNOWN", columnClass.getCanonicalName());
111120

112121
var converterClass = cvt.converter();

0 commit comments

Comments
 (0)