spring-data-neo4j
spring-data-neo4j copied to clipboard
Investigate to convert non-domain-objects into JSON-like maps.
In #2308 the question was raised whether non-domain objects can be serialized in some form as JSON-like map.
Several options could be possible:
- Depending on Jackson / ObjectMapper and just creating a
Map<String, Object>from non-domain objects, pass it to the driver and hope for the best - Or doing this but pipe every non-key through our converters, making sure the map contain only valid
Valueinstances - Create ad-hoc entities via the
Neo4jMappingContextand use the same supported format as we do for domain entities.
I understand you have concerns about the solution 1. Would you mind explaining quickly what they are? I believe that's the approach that was taken in version 5? I have personally never experienced any issue with this type of serialization, and we use the version 5 very intensively. The drawback I see for you is the dependency on Jackson. But was there any other issue with this approach I don't see?
About proposition 3, correct me if I'm wrong, but would that require all the arbitrary parameters to have an attribute annotated with @Id? In which case, I think it wouldn't make sense. If not, then it would have the advantage to keep the same serialization format as with node entities with a little extra work. However, it's limited to flat objects. Deeply nested inputs wouldn't work well.
That being said, solution 2 seems to be the best overall, since it relies entirely on neo4j conversions only and not on Jackson. It would require to map the object to a MapValue recursively and use the conversions of neo4j. Might however require the most work. It would require basically to re-create Jackson (a much simpler version) with your own conversions, is that right?
Re 1
- Date time values are serialized as strings, not as native types (the database supports them native)
- No support for spatial
Re 3
- No, we can create ad hoc entities.
Re 2 Yes, it would be indeed the most work intensive. One solution would be requiring Jackson, but doing custom conversions correctly (we had this topic over there https://github.com/neo4j/neo4j-ogm/issues/772 already)
I believe solution 1 is not a good long term solution for you. I agree with that.
As for solution 3, I'm concerned it might not work well with nested input objects (Some queries in our projects have that).
So solution 2 seems to work best. I'm not sure, how it can be implemented with Jackson, but that's probably doable.
For whoever might be interested, since I believe it might take time to solve this issue depending on the solution taken, I implemented something to work this around:
My solution starts with an interface
interface Neo4jParameter
and for example, one object I needed to convert to a map was this:
data class DateRange(
val after: Instant?,
val before: Instant?
)
data class HostsFilters(
val ids: List<String>?,
val hostnames: List<String>?,
val platforms: List<String>?,
val osNames: List<String>?,
val lastSeen: DateRange?, // <-- recursive mapping
val hasFile: List<String>?
) : Neo4jParameter // <-- implement this interface
Then, I created this converter
class Neo4jParameterConverter(otherConversions: List<GenericConverter>) : GenericConverter {
private val conversionService = DefaultConversionService()
init {
Neo4jConversions(otherConversions).registerConvertersIn(conversionService)
}
override fun getConvertibleTypes(): Set<GenericConverter.ConvertiblePair> {
return setOf(
GenericConverter.ConvertiblePair(Neo4jParameter::class.java, Value::class.java)
)
}
override fun convert(source: Any?, sourceType: TypeDescriptor?, targetType: TypeDescriptor?): Any {
return convertObject(source)
}
private fun convertObject(input: Any?): Value {
return when (input) {
null -> Values.NULL
is Collection<*> -> convertCollection(input)
is Array<*> -> convertCollection(input.toList())
is Map<*, *> -> convertMap(input)
else -> {
if (conversionService.canConvert(input::class.java, Value::class.java)) {
conversionService.convert(input, Value::class.java)
} else {
convertObjectToMapOfValues(input)
}
}
}
}
private fun convertObjectToMapOfValues(input: Any?): Value {
if (input == null) return Values.NULL
val output = HashMap<String, Value>()
ReflectionUtils.doWithFields(input::class.java) {
ReflectionUtils.makeAccessible(it)
val fieldValue = ReflectionUtils.getField(it, input)
output[it.name] = convertObject(fieldValue)
}
return MapValue(output)
}
private fun convertCollection(input: Collection<*>): ListValue {
return ListValue(*input.map { convertObject(it) }.toTypedArray())
}
private fun convertMap(input: Map<*, *>): MapValue {
return MapValue(input.map { it.key.toString() to convertObject(it.value) }.toMap())
}
}
then just pass this converter to your Neo4jConversions bean.
It's basically going to convert every Neo4jParameter to a Value recursively field for field, and it will even use Neo4j's conversion to map your parameters, so it will serialize your datetimes as a DateTimeValue and will convert your spatial fields as well.
This is a basic implementation of solution 2. The only disadvantage is the interface that you have to implement, which I find acceptable personally for a workaround.
That’s a great idea. I think it’s worth adding something like this to the FAQ. Thank you.
Sunny Pelletier @.***> schrieb am Mi. 23. Juni 2021 um 23:18:
For whoever might be interested, since I believe it might take time to solve this issue depending on the solution taken, I implemented something to work this around:
My solution starts with an interface
interface Neo4jParameter
and for example, one object I needed to convert to a map was this:
data class DateRange( val after: Instant?, val before: Instant? ) data class HostsFilters( val ids: List<String>?, val hostnames: List<String>?, val platforms: List<String>?, val osNames: List<String>?, val lastSeen: DateRange?, // <-- recursive mapping val hasFile: List<String>? ) : Neo4jParameter // <-- implement this interface
Then, I created this converter
class Neo4jParameterConverter(otherConversions: List<GenericConverter>) : GenericConverter { private val conversionService = DefaultConversionService()
init { Neo4jConversions(otherConversions).registerConvertersIn(conversionService) } override fun getConvertibleTypes(): Set<GenericConverter.ConvertiblePair> { return setOf( GenericConverter.ConvertiblePair(Neo4jParameter::class.java, Value::class.java) ) } override fun convert(source: Any?, sourceType: TypeDescriptor?, targetType: TypeDescriptor?): Any { return convertObject(source) } private fun convertObject(input: Any?): Value { if (input == null) return Values.NULL return when (input) { null -> Values.NULL is Collection<*> -> convertCollection(input) is Array<*> -> convertCollection(input.toList()) is Map<*, *> -> convertMap(input) else -> { if (conversionService.canConvert(input::class.java, Value::class.java)) { conversionService.convert(input, Value::class.java) } else { convertObjectToMapOfValues(input) } } } } private fun convertObjectToMapOfValues(input: Any?): Value { if (input == null) return Values.NULL val output = HashMap<String, Value>() ReflectionUtils.doWithFields(input::class.java) { ReflectionUtils.makeAccessible(it) val fieldValue = ReflectionUtils.getField(it, input) output[it.name] = convertObject(fieldValue) } return MapValue(output) } private fun convertCollection(input: Collection<*>): ListValue { return ListValue(*input.map { convertObject(it) }.toTypedArray()) } private fun convertMap(input: Map<*, *>): MapValue { return MapValue(input.map { it.key.toString() to convertObject(it.value) }.toMap()) }}
then just pass this converter to your Neo4jConversions bean.
It's basically going to convert every Neo4jParameter to a Value by using Jackson recursively, and it will even use Neo4j's conversion to map your parameters, so it will serialize your datetimes as a DateTimeValue and will convert your spatial fields as well.
This is a basic implementation of solution 2. The only disadvantage is the interface that you have to implement, which I find acceptable personally for a workaround.
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/spring-projects/spring-data-neo4j/issues/2309#issuecomment-867166218, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEAQL6647GHB6XMA357NJTTUJFT7ANCNFSM47FN6CLQ .