Neo4J (spring boot) with sealed classes in kotlin - spring

I'm trying to create a somewhat generic implementation using kotlin and neo4j. My idea right now is that I want to have a GeoNode that can point to any kind of GeoJson feature (e.g. "Feature" or "FeatureCollection")
I tried to do this using kotlins sealed classes, e.g.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
#JsonSubTypes(
JsonSubTypes.Type(value = GeoJsonFeature::class, name = "GeoJsonFeature")
)
#Node
sealed class FeatureContract(
#GeneratedValue #Id var id: Long? = null,
#Version var version: Long? = null
) {
companion object {
#JsonCreator
#JvmStatic
private fun creator(name: String): FeatureContract? {
return FeatureContract::class.sealedSubclasses.firstOrNull {
it.simpleName == name
}?.objectInstance
}
}
}
data class GeoJsonFeature(
val geometry: GeometryContract,
) : FeatureContract()
data class GeoJsonFeatureCollection(
val features: List<GeoJsonFeature>,
) : FeatureContract()
// and the "GeoNode" that holds this
#Node
data class GeoNode(
#Id val id: String,
#Version var version: Long? = null
#Relationship(type = "feature") var feature: FeatureContract?, // Should be either a featureCollection or a feature,
)
The idea is that I can have a point on a map that points to any kind of GeoJson.
It seems I am successful in serializing this and getting it into the Neo4J-db on write, however, on reading I get
Failed to instantiate [FeatureContract]: Is it an abstract class?; nested exception is java.lang.InstantiationException
The Jackson annotations are there cause I hoped they would help me (that Neo4J-OGM was using it under the hood) but it doesn't seem to have done the trick. I've read about Neo4JEntityConverters but I haven't understood how one can to this for a full object like this. Is there any good way to use sealed classes in kotlin with the neo4j-OGM for both serialization and deserialization?
Using spring boot 2.4.4 and spring-boot-starter-data-neo4j

Related

Can't Autowire #Repository interface in Spring Boot

The problem appeared when I tried to migrate on WebFlux.
I have one package university. It contains 4 files: Document, Controller, Service and Repository.
#Document
data class University(
#Id
#JsonSerialize(using = ToStringSerializer::class)
var id: ObjectId?,
var name: String,
var city: String,
var yearOfFoundation: Int,
#JsonSerialize(using = ToStringSerializer::class)
var students: MutableList<ObjectId> = mutableListOf(),
#Version
var version: Int?
)
#Service
class UniversityService(#Autowired private var universityRepository: UniversityRepository) {
fun getAllUniversities(): Flux<University> =
universityRepository.findAll()
fun getUniversityById(id: ObjectId): Mono<University> =
universityRepository.findById(id)
}
#RestController
#RequestMapping("api/universities", consumes = [MediaType.APPLICATION_NDJSON_VALUE])
class UniversityController(#Autowired val universityService: UniversityService) {
#GetMapping("/all")
fun getAll(): Flux<University> =
universityService.getAllUniversities().log()
#GetMapping("/getById")
fun getUniversityById(#RequestParam("id") id: ObjectId): Mono<University> =
universityService.getUniversityById(id)
}
#Repository
interface UniversityRepository: ReactiveMongoRepository<University, ObjectId>, CustomUniversityRepository {
fun existsByNameIgnoreCase(name: String): Mono<Boolean>
fun removeUniversityById(id: ObjectId): Mono<University?>
fun findUniversitiesByNameIgnoreCase(name: String): Flux<University>
}
All in separate files regarding their names.
I am getting a problem with my service, cause it cannot find repository. Consider defining a bean of type 'demo.university.UniversityRepository' in your configuration. But my repository file with exact name and interface is directly there.
I've tried to mark my repository with Bean annotation, but I can't do so with interfaces. Also, #EnableJpaRepositories does not help.
P.S. I know, it seems like a duplicate, but I really didn't find an answer in previous questions.
My problem was in a wrong project dependencies. As I mentioned, I migrated from simple Web to WebFlux. But I didn't change my MongoDB dependency. It should be marked as a reactive explicitly even if ReactiveMongoRepository interface is found correctly.
implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive:2.7.1")

How to prevent saving over an existing entity with Spring Data REST

(Samples in Kotlin)
I have an entity with manually assigned IDs:
#Entity
#Table(name = "Item")
class Item {
#Id
#Column(name = "ItemId", nullable = false, updatable = false)
var id: Int? = null
#Column(name = "Name", nullable = false)
var name: String? = null
}
and the Spring Data REST repository for it:
interface ItemRepository : PagingAndSortingRepository<Item, Int>
If I do a POST to /items using an existing ID, the existing object is overwritten. I would expect it to throw back an error. Is there a way to configure that behavior without rolling my own save method for each resource type?
Thanks.
I ended up using a Spring Validator for this with the help of this article.
I created the validator like this:
class BeforeCreateItemValidator(private val itemRepository: ItemRepository) : Validator {
override fun supports(clazz: Class<*>) = Item::class.java == clazz
override fun validate(target: Any, errors: Errors) {
if (target is Item) {
itemRepository
.findById(target.id!!)
.ifPresent {
errors.rejectValue("id",
"item.exists",
arrayOf(target.id.toString()),
"no default message")
}
}
}
}
And then set it up with this configuration:
#Configuration
class RestRepositoryConfiguration(
private val beforeCreateItemValidator: BeforeCreateItemValidator
) : RepositoryRestConfigurer {
override fun configureValidatingRepositoryEventListener(
validatingListener: ValidatingRepositoryEventListener) {
validatingListener.addValidator("beforeCreate", beforeCreateItemValidator)
}
}
Doing this causes the server to return a 400 Bad Request (I'd prefer the ability to change to a 409 Conflict, but a 400 will do) along with a JSON body with an errors property containing my custom message. This is fine for my purposes of checking one entity, but if my whole application had manually assigned IDs it might get a little messy to have to do it this way. I'd like to see a Spring Data REST configuration option to just disable overwrites.
You can add a version attribute to the entity annotated with #Version this will enable optimistic locking. If you provide always the version 0 with new entities you'll should get an exception when that entity does already exist (with a different version).
Of course you then need to provide that version for updates as well.

Neo4jRepository saves empty node from Kotlin data class

I have a Kotlin data class node for Neo4j nodes:
#NodeEntity
data class MyNode (
#Id #GeneratedValue var dbId: Long? = null,
#Index(unique = true) val name: String,
val description: String
)
and a Spring repository:
interface MyNodesRepository : Neo4jRepository<MyNode, Long>
Then, when I save a node into the DB via this repository it is empty, without any properties:
val node = MyNode(null, "name 1", "lorem ipsum")
myNodesRepository.save(node)
after the save(node) call, the node.dbId is set to the Neo4j's internal id i.e. it is null before save() and has a value afterwards. I can also see the node in the Neo4j browser, but it does not have name and description properties.
After that, when I try to load all nodes the call crashes with InvalidDataAccessApiUsageException because it cannot deserialize/map the nodes with null/missing name and description:
val allNodes = myNodesRepository.findAll()
If I add a custom save method to my repository, where I manually create the node with CQL query, then everything works.
interface MyNodesRepository : Neo4jRepository<MyNode, Long> {
#Query(
"MERGE (mn:MyNode {name:{name}})\n" +
"ON CREATE SET m += {description:{description}}"
)
fun customSave(#Param("name") name: String, #Param("description") description: String)
}
Now the findAll() loads my newly created and/or updated nodes.
I am using org.springframework.boot:spring-boot-starter-data-neo4j:2.1.6.RELEASE and this is inside a Spring Boot CLI application so no web server and RestControllers.
What am I doing wrong?
EDIT: This is now solved in Neo4j OGM 3.1.13
EDIT: This is now solved in Neo4j OGM 3.1.13 and the workaround below is not needed anymore.
ORIGINAL ANSWER
After few days of debugging it looks like there is a bug in Neo4j OGM for Kotlin data classes where it does not save val properties -- both in data classes and normal classes. So change from val properties:
#NodeEntity
data class MyNode (
#Id #GeneratedValue var dbId: Long? = null,
#Index(unique = true) val name: String,
val description: String
)
to all var properties:
#NodeEntity
data class MyNode (
#Id #GeneratedValue var dbId: Long? = null,
#Index(unique = true) var name: String,
var description: String
)
works. Now both saving and loading works, but it is not idiomatic Kotlin.
So at least we have a workaround.

Kotlin & Spring (Data): custom setters

I currently am working on a project that uses Spring Boot, written in Kotlin. It has got to be mentioned that I am still relatively new to Kotlin, coming from Java. I have a small database consisting of a single table that is used to look up files. In this database, I store the path of the file (that for this testing purpose is simply stored in the the resources of the project).
The object in question looks as follows:
#Entity
#Table(name = "NOTE_FILE")
class NoteFile {
#Id
#GeneratedValue
#Column(name = "id")
var id: Int
#Column(name = "note")
#Enumerated(EnumType.STRING)
var note: Note
#Column(name = "instrument")
var instrument: String
#Column(name = "file_name")
var fileName: String
set(fileName) {
field = fileName
try {
file = ClassPathResource(fileName).file
} catch (ignored: Exception) {
}
}
#Transient
var file: File? = null
private set
constructor(id: Int, instrument: String, note: Note, fileName: String) {
this.id = id
this.instrument = instrument
this.note = note
this.fileName = fileName
}
}
I have created the following repository for retrieving this object from the database:
#Repository
interface NoteFileRepository : CrudRepository<NoteFile, Int>
And the following service:
#Service
class NoteFileService #Autowired constructor(private val noteFileRepository: NoteFileRepository) {
fun getNoteFile(id: Int): NoteFile? {
return noteFileRepository.findById(id).orElse(null)
}
}
The problem that I have is when I call the getNoteFile function, neither the constructor nor the setter of the constructed NoteFile object are called. As a result of this, the file field stays null, whereas I expect it to contain a value. A workaround this problem is to set the fileName field with the value of that field, but this looks weird and is bound to cause problems:
val noteFile: NoteFile? = noteFileService.getNoteFile(id)
noteFile.fileName = noteFile.fileName
This way, the setter is called and the file field gets the correct value. But this is not the way to go, as mentioned above. The cause here could be that with the Spring Data framework, a default constructor is necessary. I am using the necessary Maven plugins described here to get Kotlin and JPA to work together to begin with.
Is there some way that the constructor and/or the setter does get called when the object is constructed by the Spring (Data) / JPA framework? Or maybe should I explicitly call the setter of fileName in the service that retrieves the object? Or is the best course of action here to remove the file field as a whole and simply turn it into a function that fetches the file and returns it like that?

Does Room support entity inheritance?

I am trying to migrate our project to use Room, which, by the way, I think is an awesome step forward.
I have the following structure:
public class Entity extends BaseObservable {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "_id", typeAffinity = ColumnInfo.INTEGER)
private long mId;
#ColumnInfo(name = "is_dirty")
#TypeConverters(BooleanTypeConverter.class)
private boolean mIsDirty;
// default constructor and accessors omitted for brevity
}
#Entity(tableName = "some_entities")
public class SomeEntity extends Entity {
#ColumnInfo(name = "type", typeAffinity = ColumnInfo.TEXT)
private String mType;
#ColumnInfo(name = "timestamp", typeAffinity = ColumnInfo.INTEGER)
private long mTimestamp;
// constructor, accessors
}
When I try to compile my project, it fails with no specific error.
If I try to compile it with a flat entity hierarchy, all is well.
So, my main question is:
Does Room support entity inheritance? Will it be able to get the column definitions from the parent Entity class?
I would also like to know if extending BaseObservable (which I need to get the Data Binding working) can cause problems with Room? BaseObservable has one private transient field, so maybe this is causing some issues with the code generation.
Are there any recommended patterns to deal with this, or will I just have to flatten my entity hierarchy?
After further investigation it turns out that Room Entities should not extend the BaseObservable class. It contains fields that can't be marked with #Ignore and break the code generation.
Room works well with inheritance. The annotations are processed as expected and the DB operations behave normally. You can extend from both an Entity and a POJO.

Resources