Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,21 @@ public Object fromDatabase(@Nonnull Object[] values, @Nonnull RefFactory refFact
if (value == null) {
return null;
}
// A custom deserializer may issue a query, nesting another fromDatabase call on this thread. The previous
// factory is therefore restored rather than removed, so the remaining fields of the outer value still
// deserialize with the outer factory and produce attached refs.
RefFactory outerRefFactory = REF_FACTORY.get();
REF_FACTORY.set(refFactory);
try {
REF_FACTORY.set(refFactory);
return mapper.readValue((String) values[0], typeReference);
} catch (JsonProcessingException e) {
throw new SqlTemplateException(e);
} finally {
REF_FACTORY.remove();
if (outerRefFactory == null) {
REF_FACTORY.remove();
} else {
REF_FACTORY.set(outerRefFactory);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
import st.orm.Json;
import st.orm.PK;
import st.orm.PersistenceException;
import st.orm.Ref;
import st.orm.jackson.model.Address;
import st.orm.jackson.model.Owner;

/**
* Tests for {@link st.orm.jackson.spi.JsonORMConverterImpl} targeting uncovered branches:
Expand Down Expand Up @@ -277,4 +279,48 @@ public void jsonWithFailOnMissingTrueShouldSucceedWithCompleteJson() {
.getSingleResult(OwnerWithFailOnMissing.class);
assertNotNull(result.address());
}

// Nested deserialization: a custom deserializer that issues a query mid-deserialization.

public static class NestedQueryMarkerDeserializer extends JsonDeserializer<String> {
static DataSource nestedDataSource;

@Override
public String deserialize(JsonParser parser, DeserializationContext context)
throws java.io.IOException {
String text = parser.getText();
// Maps Owner, whose @Json address field enters fromDatabase re-entrantly on this thread.
of(nestedDataSource)
.query("SELECT id, first_name, last_name, address, telephone FROM owner WHERE id = 2")
.getSingleResult(Owner.class);
return text;
}
}

public record OwnerSnapshot(
@JsonDeserialize(using = NestedQueryMarkerDeserializer.class) String marker,
Ref<Owner> owner
) {}

@Builder(toBuilder = true)
@DbTable("owner")
public record OwnerWithSnapshot(
@PK Integer id,
@Nonnull @Json OwnerSnapshot snapshot,
@Nullable String telephone
) implements Entity<Integer> {}

@Test
public void refDeserializedAfterNestedConversionShouldRemainAttached() {
// The marker field deserializes first and issues a nested query, which binds and unbinds the nested
// conversion's RefFactory. The owner field that follows must still see the outer factory: the
// resulting ref is attached and fetches the owner.
NestedQueryMarkerDeserializer.nestedDataSource = dataSource;
var orm = of(dataSource);
var query = orm.query("SELECT id, JSON_OBJECT('marker' VALUE 'audit', 'owner' VALUE id) AS snapshot, telephone FROM owner WHERE id = 1");
var result = query.getSingleResult(OwnerWithSnapshot.class);
var ownerRef = result.snapshot().owner();
assertTrue(ownerRef.isFetchable());
assertEquals("Betty", ownerRef.fetch().firstName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,21 @@ public Object fromDatabase(@Nonnull Object[] values, @Nonnull RefFactory refFact
if (value == null) {
return null;
}
// A custom deserializer may issue a query, nesting another fromDatabase call on this thread. The previous
// factory is therefore restored rather than removed, so the remaining fields of the outer value still
// deserialize with the outer factory and produce attached refs.
RefFactory outerRefFactory = REF_FACTORY.get();
REF_FACTORY.set(refFactory);
try {
REF_FACTORY.set(refFactory);
return mapper.readValue((String) values[0], typeReference);
} catch (JacksonException e) {
throw new SqlTemplateException(e);
} finally {
REF_FACTORY.remove();
if (outerRefFactory == null) {
REF_FACTORY.remove();
} else {
REF_FACTORY.set(outerRefFactory);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -454,4 +454,53 @@ public void polymorphicJsonWithExplicitTypeNamesShouldResolveSubtype() {
assertEquals(10, owner.size());
assertTrue(owner.stream().allMatch(x -> x.person instanceof NamedPersonA));
}

// Nested deserialization: a custom deserializer that issues a query mid-deserialization.

public static class NestedQueryMarkerDeserializer extends tools.jackson.databind.deser.std.StdDeserializer<String> {
static DataSource nestedDataSource;

public NestedQueryMarkerDeserializer() {
super(String.class);
}

@Override
public String deserialize(tools.jackson.core.JsonParser parser,
tools.jackson.databind.DeserializationContext ctxt)
throws tools.jackson.core.JacksonException {
String text = parser.getText();
// Maps Owner, whose @Json address field enters fromDatabase re-entrantly on this thread.
of(nestedDataSource)
.query("SELECT id, first_name, last_name, address, telephone FROM owner WHERE id = 2")
.getSingleResult(Owner.class);
return text;
}
}

public record OwnerSnapshot(
@tools.jackson.databind.annotation.JsonDeserialize(using = NestedQueryMarkerDeserializer.class) String marker,
Ref<Owner> owner
) {}

@Builder(toBuilder = true)
@DbTable("owner")
public record OwnerWithSnapshot(
@PK Integer id,
@Nonnull @Json OwnerSnapshot snapshot,
@Nullable String telephone
) implements Entity<Integer> {}

@Test
public void refDeserializedAfterNestedConversionShouldRemainAttached() {
// The marker field deserializes first and issues a nested query, which binds and unbinds the nested
// conversion's RefFactory. The owner field that follows must still see the outer factory: the
// resulting ref is attached and fetches the owner.
NestedQueryMarkerDeserializer.nestedDataSource = dataSource;
var orm = of(dataSource);
var query = orm.query("SELECT id, JSON_OBJECT('marker' VALUE 'audit', 'owner' VALUE id) AS snapshot, telephone FROM owner WHERE id = 1");
var result = query.getSingleResult(OwnerWithSnapshot.class);
var ownerRef = result.snapshot().owner();
assertTrue(ownerRef.isFetchable());
assertEquals("Betty", ownerRef.fetch().firstName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,21 @@ internal class JsonORMConverterImpl(

override fun fromDatabase(values: Array<Any?>, refFactory: RefFactory): Any? {
val raw = values[0] as? String? ?: return null
// A custom serializer may issue a query, nesting another fromDatabase call on this thread. The previous
// factory is therefore restored rather than removed, so the remaining fields of the outer value still
// deserialize with the outer factory and produce attached refs.
val outerRefFactory = REF_FACTORY.get()
REF_FACTORY.set(refFactory)
return try {
REF_FACTORY.set(refFactory)
this@JsonORMConverterImpl.json.decodeFromString(serializer, raw)
} catch (e: SerializationException) {
throw SqlTemplateException(e)
} finally {
REF_FACTORY.remove()
if (outerRefFactory == null) {
REF_FACTORY.remove()
} else {
REF_FACTORY.set(outerRefFactory)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package st.orm.serialization

import kotlinx.serialization.Contextual
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonClassDiscriminator
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertNotNull
Expand Down Expand Up @@ -452,4 +458,63 @@ open class JsonORMConverterIntegrationTest(@Autowired val dataSource: DataSource
assertNotNull(result)
assertEquals(listOf("a", "b", "c"), result.names)
}

// Nested deserialization: a custom serializer that issues a query mid-deserialization.

object NestedQueryMarkerSerializer : KSerializer<String> {
lateinit var nestedDataSource: DataSource

override val descriptor = PrimitiveSerialDescriptor("NestedQueryMarker", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: String) = encoder.encodeString(value)

override fun deserialize(decoder: Decoder): String {
val text = decoder.decodeString()
// Maps Owner, whose @Json address field enters fromDatabase re-entrantly on this thread.
ORMTemplate.of(nestedDataSource)
.query("SELECT id, first_name, last_name, address, telephone FROM owner WHERE id = 2")
.getSingleResult(Owner::class)
return text
}
}

// The ref target must be @Serializable itself, since the compiler requires a serializer for the type
// argument of a @Contextual Ref property. Maps the owner table without its @Json address column.
@Serializable
@DbTable("owner")
data class SerializableOwner(
@PK val id: Int = 0,
val firstName: String,
val lastName: String,
val telephone: String?,
) : Entity<Int>

@Serializable
data class OwnerSnapshot(
@Serializable(with = NestedQueryMarkerSerializer::class) val marker: String,
@Contextual val owner: Ref<SerializableOwner>,
)

@DbTable("owner")
data class OwnerWithSnapshot(
@PK val id: Int,
@Json val snapshot: OwnerSnapshot,
val telephone: String?,
) : Entity<Int>

@Test
fun `ref deserialized after nested conversion should remain attached`() {
// The marker field deserializes first and issues a nested query, which binds and unbinds the nested
// conversion's RefFactory. The owner field that follows must still see the outer factory: the
// resulting ref is attached and fetches the owner.
NestedQueryMarkerSerializer.nestedDataSource = dataSource
val orm = ORMTemplate.of(dataSource)
val query = orm.query(
"SELECT id, JSON_OBJECT('marker' VALUE 'audit', 'owner' VALUE id) AS snapshot, telephone FROM owner WHERE id = 1",
)
val result = query.getSingleResult(OwnerWithSnapshot::class)
val ownerRef = result.snapshot.owner
Assertions.assertTrue(ownerRef.isFetchable)
assertEquals("Betty", ownerRef.fetch().firstName)
}
}
Loading