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?
Related
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
(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.
I have a database service using Spring Boot 1.5.1 and Spring Data Rest. I am storing my entities in a MySQL database, and accessing them over REST using Spring's PagingAndSortingRepository. I found this which states that sorting by nested parameters is supported, but I cannot find a way to sort by nested fields.
I have these classes:
#Entity(name = "Person")
#Table(name = "PERSON")
public class Person {
#ManyToOne
protected Address address;
#ManyToOne(targetEntity = Name.class, cascade = {
CascadeType.ALL
})
#JoinColumn(name = "NAME_PERSON_ID")
protected Name name;
#Id
protected Long id;
// Setter, getters, etc.
}
#Entity(name = "Name")
#Table(name = "NAME")
public class Name{
protected String firstName;
protected String lastName;
#Id
protected Long id;
// Setter, getters, etc.
}
For example, when using the method:
Page<Person> findByAddress_Id(#Param("id") String id, Pageable pageable);
And calling the URI http://localhost:8080/people/search/findByAddress_Id?id=1&sort=name_lastName,desc, the sort parameter is completely ignored by Spring.
The parameters sort=name.lastName and sort=nameLastName did not work either.
Am I forming the Rest request wrong, or missing some configuration?
Thank you!
The workaround I found is to create an extra read-only property for sorting purposes only. Building on the example above:
#Entity(name = "Person")
#Table(name = "PERSON")
public class Person {
// read only, for sorting purposes only
// #JsonIgnore // we can hide it from the clients, if needed
#RestResource(exported=false) // read only so we can map 2 fields to the same database column
#ManyToOne
#JoinColumn(name = "address_id", insertable = false, updatable = false)
private Address address;
// We still want the linkable association created to work as before so we manually override the relation and path
#RestResource(exported=true, rel="address", path="address")
#ManyToOne
private Address addressLink;
...
}
The drawback for the proposed workaround is that we now have to explicitly duplicate all the properties for which we want to support nested sorting.
LATER EDIT: another drawback is that we cannot hide the embedded property from the clients. In my original answer, I was suggesting we can add #JsonIgnore, but apparently that breaks the sort.
I debugged through that and it looks like the issue that Alan mentioned.
I found workaround that could help:
Create own controller, inject your repo and optionally projection factory (if you need projections). Implement get method to delegate call to your repository
#RestController
#RequestMapping("/people")
public class PeopleController {
#Autowired
PersonRepository repository;
//#Autowired
//PagedResourcesAssembler<MyDTO> resourceAssembler;
#GetMapping("/by-address/{addressId}")
public Page<Person> getByAddress(#PathVariable("addressId") Long addressId, Pageable page) {
// spring doesn't spoil your sort here ...
Page<Person> page = repository.findByAddress_Id(addressId, page)
// optionally, apply projection
// to return DTO/specifically loaded Entity objects ...
// return type would be then PagedResources<Resource<MyDTO>>
// return resourceAssembler.toResource(page.map(...))
return page;
}
}
This works for me with 2.6.8.RELEASE; the issue seems to be in all versions.
From Spring Data REST documentation:
Sorting by linkable associations (that is, links to top-level resources) is not supported.
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#paging-and-sorting.sorting
An alternative that I found was use #ResResource(exported=false).
This is not valid (expecially for legacy Spring Data REST projects) because avoid that the resource/entity will be loaded HTTP links:
JacksonBinder
BeanDeserializerBuilder updateBuilder throws
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of ' com...' no String-argument constructor/factory method to deserialize from String value
I tried activate sort by linkable associations with help of annotations but without success because we need always need override the mappPropertyPath method of JacksonMappingAwareSortTranslator.SortTranslator detect the annotation:
if (associations.isLinkableAssociation(persistentProperty)) {
if(!persistentProperty.isAnnotationPresent(SortByLinkableAssociation.class)) {
return Collections.emptyList();
}
}
Annotation
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface SortByLinkableAssociation {
}
At project mark association as #SortByLinkableAssociation:
#ManyToOne
#SortByLinkableAssociation
private Name name;
Really I didn't find a clear and success solution to this issue but decide to expose it to let think about it or even Spring team take in consideration to include at nexts releases.
Please see https://stackoverflow.com/a/66135148/6673169 for possible workaround/hack, when we wanted sorting by linked entity.
I have an entity that looks like this:
#Entity
#Table(uniqueConstraints={#UniqueConstraint(columnNames={"slug"})})
public class BlogPost {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String title;
#Column
private String slug;
}
I would like to generate the value of slug before persisting by doing the following:
Transforming the title from e.g. Blog Post Title to blog-post-title
Making sure that blog-post-title is unique in table BlogPost, and if it's not unique, I want to append some suffix to the title so it becomes e.g. blog-post-title-2
Since I need this on a lot of entities, my original idea was to create an EntityListener which would do this at #PrePersist. However, documentation generally states that I should not call EntityManager or Query methods and should not access any other entity objects from lifecycle callbacks. I need to do that in order to make sure that my generated slug is indeed unique.
I tried to be cheeky, but it is indeed very hard to autowire a repository into an EntityListener with Spring anyway.
How should I best tackle this problem?
Thanks!
Both OndrejM and MirMasej are definitely right that generating a slug would not be something to be done in an Entity. I was hoping EntityListeners could be a little "smarter", but that's not an option.
What I ended up doing is using aspects to accomplish what I wanted. Instead of "hooking" into entities, I am rather hooking into save method of CrudRepository.
First, I created an annotation so I can recognize which field needs to be sluggified:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface Slug {
/**
* The string slug is generated from
*/
String source() default "title";
/**
* Strategy for generating a slug
*/
Class strategy() default DefaultSlugGenerationStrategy.class;
}
Then, I created an aspect which is something like this:
#Aspect
#Component
public class SlugAspect {
... // Removed some code for bravity
#Before("execution(* org.springframework.data.repository.CrudRepository+.save(*))")
public void onRepoSave(JoinPoint joinPoint) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Object entity = joinPoint.getArgs()[0];
for (Field field: entity.getClass().getDeclaredFields()) {
Slug annotation = field.getAnnotation(Slug.class);
if (annotation != null) {
CrudRepository repository = (CrudRepository) joinPoint.getTarget();
Long count = 0L;
SlugGenerationStrategy generator = (SlugGenerationStrategy)annotation.strategy().newInstance();
String slug = generator.generateSlug(slugOrigin(entity));
if (id(entity) != null) {
Method method = repository.getClass().getMethod("countBySlugAndIdNot", String.class, Long.class);
count = (Long)method.invoke(repository, slug, id(entity));
} else {
Method method = repository.getClass().getMethod("countBySlug", String.class);
count = (Long)method.invoke(repository, slug);
}
// If count is zero, use the generated slug, or generate an incremented slug if count > 0 and then set it like so:
setSlug(entity, slug);
}
}
}
}
I put the code on github (though it's still just a proof of concept) if anyone is interested at: https://github.com/cabrilo/jpa-slug
It relies on having CrudRepository from Spring Data and having these two methods on a repo: countBySlug and countBySlugAndIdNot.
Thanks again for the answers.
The most straightforward solutions seems to make a check before setting the value of the title. It would mean however that the logic of calculating the slug would be outside of the entity and both would come from outside.
You have to think of an entity as a plain object without any connection to the database - this is the idea of ORM. However, you may pass a reference to EntityManager or DAO as an additional argument to a setter method, or somehow inject a reference to it. Then you may call a query directly from the setter method. The drawback of this solution is that you need to always provide EntityManager, either when you set title, or when you create/load the entity.
This is the best object oriented way of solving this problem.
I'm using Spring + Spring Data MongoDB.
My model is like this:
#Document(collection = "actors")
public class Actor extends DomainEntity {
private String name;
private String surname;
#DBRef(lazy = true)
private List<Class> classes;
The other class is pretty generic, so I don't post it.
My problem is that the list "classes" isn't loaded when i try to access it, the attribute remains being some kind of proxy object.
Example:
Actor a = actorRepository.findOne(id);
//At this moment classes are a proxy object because of the lazy
//Now I try to load the reference and nothing works
a.getClasses();
a.getClasses().size();
a.getClases().get(0).getAttr();
for(Class g:a.getClasses()){
g.getAttr();
}
I considered a ton of options, but no way to make it working...
I'm using spring-data-mongodb-1.7.0.RELEASE and I was able to solve this issue by initializing the lazy-loaded collection in its declaration, for instance:
#DBRef(lazy = true)
private List<Class> classes = new ArrayList<>();