Using a custom ID in a Spring Data REST repository backed by MongoDB - spring

I'm working on a new project using Spring and MongoDB. I'm a novice with both of these so bear with me, I couldn't find a definitive answer to this question.
I'm using spring-boot-starter-data-rest and have a repository like this:
#RepositoryRestResource(collectionResourceRel = "widget", path = "widget")
interface WidgetRepository : MongoRepository<Widget, ObjectId> {
fun findByType(#Param("type") type: String): List<Widget>
}
For an entity like this:
data class Widget #JsonCreator constructor(#JsonProperty val type: String) {
#Id
lateinit var id: ObjectId
}
This automatically gives you a CRUD API using the Mongo document ID:
GET /widget/{mongo doc id}
GET /widget/search/findByType?type=
POST /widget
PUT /widget
PATCH /widget
But I don't want to use the Mongo document ID. I want to introduce a secondary identifier and use that everywhere in the API. This is because the "widgets" are items in the physical world that are barcoded, and we don't want to print the Mongo document ID in the barcode.
Obviously we can implement this using Spring REST API tools, eg
#GetMapping("/widget/{barcode}/")
fun getByBarcode(#PathVariable barcode: String): Widget {
return widgetRepository.findByBarcode(barcode)
}
etc.. but is there any clever way to get #RepositoryRestResource to build its automagic API for us with a custom ID? Maybe by implementing CrudRepository<Widget, Barcode> in such a way that we have to wrap a MongoRepository<Widget, ObjectId> ? I'm not familiar enough with how Spring works under the hood to know if anything like this is even possible.
Thanks in advance

I think you are looking for an EntityLookup:
SPI to customize which property of an entity is used as unique identifier and how the entity instance is looked up from the backend.
First - sorry if I make any mistake, I do not use to program in Kotlin - you need to include the barcode property in your entity:
data class Widget #JsonCreator constructor(#JsonProperty val type: String, #JsonProperty val barcode: String) {
#Id
lateinit var id: ObjectId
}
Then, modify your repository and define a new method that will provide a Widget given its barcode:
#RepositoryRestResource(collectionResourceRel = "widget", path = "widget")
interface WidgetRepository : MongoRepository<Widget, ObjectId> {
fun findByBarcode(#Param("barcode") barcode: String): Optional<Widget>
fun findByType(#Param("type") type: String): List<Widget>
}
Finally, configure a RestRepositoryConfigurer and register the EntityLookup through an EntityLookupRegistrar:
#Component
class RestRepositoryConfigurator : RepositoryRestConfigurer {
override fun configureRepositoryRestConfiguration(config: RepositoryRestConfiguration) {
config.withEntityLookup()
.forRepository(WidgetRepository::class.java)
.withIdMapping(Widget::barcode)
.withLookup(WidgetRepository::findByBarcode)
}
}
Please, review the section 15.1 of the Spring Data Rest documentation if you need more information.

Related

Custom ConditionalGenericConverter annotation doesn't convert parameters in Spring GraphQL

I have a REST API developed in Spring and now I'm learning how to use GraphQL in my Spring project so I'm "translating" my REST endpoints to GraphQL queries.
Just to give some background on what I'm trying to do, I will show how one of my REST controllers look like (code in Kotlin):
Rest Controller
#RestController
#RequestMapping("countries")
class CountryController(private val countryService: CountryService) {
#GetMapping("{code}")
fun findByCode(#PathVariable #Uppercase code: String): Country {
return countryService.findByCode(code)
}
}
It's basically an endpoint that fetches a country based on a code. For example /countries/bra will fetch information about the country Brazil.
In the code above there's also a custom annotation called #Uppercase. This annotation extends ConditionalGenericConverter and the only thing it does is to take the code parameter and convert it to uppercase. This is the code for this annotation:
#Uppercase implementation
#Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Uppercase
#Component
class UppercaseConverter : ConditionalGenericConverter {
override fun matches(sourceType: TypeDescriptor, targetType: TypeDescriptor): Boolean {
return targetType.getAnnotation(Uppercase::class.java) != null
}
override fun getConvertibleTypes(): MutableSet<ConvertiblePair>? {
return mutableSetOf(ConvertiblePair(String::class.java, String::class.java))
}
override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor): String {
return (source as String).uppercase()
}
}
Everything works as expected in the code above and the code parameter is always converted to uppercase if I use the #Uppercase annotation in my REST controller.
Now I created a similar controller for Spring GraphQL that looks like this:
GraphQL Controller
#Controller
class CountryResolver(private val countryService: CountryService) {
#QueryMapping(name = "country")
fun findByCode(#Argument #Uppercase code: String): Country {
return countryService.findByCode(code)
}
}
And my GraphQL is executed properly with the code above, except that the parameter code is not converted to uppercase when I use my custom annotation #Uppercase. It seems that my custom annotation only works with #PathVariable in the REST controller, but it doesn't with #Argument in the GraphQL controller.
Any idea why on how to make it work with GraphQL as well? Thanks in advance!

Testing Service layer in Kotlin Spring Boot when back end is using Spring Security

I'm looking to try and test my back end Service layer (ideally along with my Controller layer too). However, I'm not sure how I would be able to go about doing this.
I saw that I can use Mockito to do this, however, I was not able to figure out how to apply this with what I currently have.
I have the entity, then repository, service and then finally controller.
My services have the main logic for the application within. For example, saving an entity, fetching and updating.
My controller (which is a restcontroller) is what my front end client interfaces with in order to call the service methods.
My service has an interface like below:
interface CompanyService {
fun saveCompany(company: Company): Company
fun saveCompany(form: CompanyController.SaveCompanyForm): Company
fun getCompany(name: String): Company
fun getCompany(id: Long): Company
fun getCompanies(): MutableList<Company>
fun addContactToCompany(firstName: String, lastName: String, companyName: String)
fun saveContact(contact: Contact): Contact
fun saveContact(form: CompanyController.SaveContactForm): Contact
fun saveContact(contact: Contact, companyName: String): Contact
fun getContact(firstName: String, lastName: String): Contact
fun getContact(id: Long): Contact
fun getContacts(): MutableList<Contact>
}
and then a CompanyServiceImpl:
#Service
#Transactional
class CompanyServiceImpl(
private val companyRepository: CompanyRepository,
private val contactRepository: ContactRepository,
) : CompanyService {
private val log = logger<CompanyServiceImpl>()
override fun saveCompany(company: Company): Company {
log.info("Saving Company ${company.name} to database")
return companyRepository.save(company)
}
...
}
My question as the subject says is how I would go about unit testing this so I don't have to manually do it which is both time consuming as well as it being "not the right way of doing it".
Ideally I'd like to have my controllers tested the same way, however I have Spring Security enabled and am unsure if this would get in the way in any way.

How to avoid Spring Repository<T, ID> to leak persistence information into service tier

I'm using spring-data-mongodb at the moment so this question is primarily in context of MongoDB but I suspect my question applies to repository code in general.
Out of the box when using a MongoRepository<T, ID> interface (or any other Repository<T, ID> descendent) the entity type T is expected to be the document type (the type that defines the document schema).
As a result injecting such a repository into service component means this repository is leaking database schema information into the service tier (highly pseudo) :
class MyModel {
UUID id;
}
#Document
class MyDocument {
#Id
String id;
}
interface MyRepository extends MongoRepository<MyDocument, String> {
}
class MyService {
MyRepository repository;
MyModel getById(UUID id) {
var documentId = convert(id, ...);
var matchingDocument = repository.findById(documentId).orElse(...);
var model = convert(matchignDocument, ...);
return model;
}
}
Whilst ideally I'd want to do this :
class MyModel {
UUID id;
}
#Document
class MyDocument {
#Id
String id;
}
#Configuration
class MyMagicConversionConfig {
...
}
class MyDocumentToModelConverter implements Converter<MyModel, MyDocument> {
...
}
class MyModelToDocumentConverter implements Converter<MyDocument, MyModel> {
...
}
// Note that the model and the model's ID type are used in the repository declaration
interface MyRepository extends MongoRepository<MyModel, UUID> {
}
class MyService {
MyRepository repository;
MyModel getById(UUID id) {
// Repository now returns the model because it was converted upstream
// by the mongo persistence layer.
var matchingModel = repository.findById(documentId).orElse(...);
return matchingModel ;
}
}
Defining this conversion once seems significantly more practical than having to consistently do it throughout your service code so I suspect I'm just missing something.
But of course this requires some way to inform the mongo mapping layer to be aware of what conversion has to be applied to move between MyModel and MyDocument and to use the latter for it's actual source of mapping metadata (e.g. #Document, #Id, etc.).
I've been fiddling with custom converters but I just can't seem to make the MongoDB mapping component do the above.
My two questions are :
Is it currently possible to define custom converters or implement callbacks that allow me to define and implement this model <-> document conversion once and abstract it away from my service tier.
If not, what is the idiomatic way to approach cleaning this up such that the service layer can stay blissfully unaware of how or with what schema an entity is persisted? A lot of Spring Boot codebases appear to be fine with using the type that defines the database schema as their model but that seems supoptimal. Suggestions welcome!
Thanks!
I think you're blowing things a bit out of proportion. The service layer is not aware of the schema. It is aware of the types returned by the repository. How the properties of those are mapped onto the schema, depends on the object-document mapping. This, by default, uses the property name, as that's the most straightforward thing to do. That translation can either be customized using annotations on the document type or by registering a FieldNamingStrategy with Spring Data MongoDB.
Spring Data MongoDB's object-document mapping subsystem provides a lot of customization hooks that allows transforming arbitrary MongoDB documents into entities. The types which the repositories return are your domain objects that - again, only by default - are mapped onto a MongoDB document 1:1, simply because that's the most reasonable thing to do in the first place.
If really in doubt, you can manually implement repository methods individually that allow you to use the MongoTemplate API that allows you to explicitly define the type, the data should be projected into.
You can use something like MapStruct or write your own Singleton Mapper.
Then create default methods in your repository:
interface DogRepository extends MongoRepository<DogDocument, String> {
DogDocument findById(String id);
default DogModel dogById(String id) {
return DogMapper.INSTANCE.toModel(
findById(id)
);
}
}

spring-data-neo4j v6: No converter found capable of converting from type [MyDTO] to type [org.neo4j.driver.Value]

Situation
I'm migrating a kotlin spring data neo4j application from spring-data-neo4j version 5.2.0.RELEASE to version 6.0.11.
The original application has several Repository interfaces with custom queries which take some DTO as a parameter, and use the various DTO fields to construct the query. All those types of queries currently fail with
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [MyDTO] to type [org.neo4j.driver.Value]
The reference documentation for spring-data-neo4j v6 only provides examples where parameters passed to custom query methods of a #Repository interface are of the same type as the #Node class associated with that repository. The documentation does not explicitly state that only parameters of the Node class are allowed.
Question
Is there any way to pass an arbitrary DTO (not being a #Node class) to a custom query method in a #Repository interface in spring-data-neo4j v6 like it was possible in v5?
Code samples
Example node entity
#Node
data class MyEntity(
#Id
val attr1: String,
val attr2: String,
val attr3: String
)
Example DTO
data class MyDTO(
val field1: String,
val field2: String
)
Example Repository interface
#Repository
interface MyRepository : PagingAndSortingRepository<MyEntity, String> {
// ConverterNotFoundException is thrown when this method is called
#Query("MATCH (e:MyEntity {attr1: {0}.field1}) " +
"CREATE (e)-[l:LINK]->(n:OtherEntity {attr2: {0}.field2))")
fun doSomethingWithDto(dto: MyDTO)
}
Solutions tried so far
Annotate DTO as if it were a Node entity
Based on the following found in the reference docs https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom-queries.parameters
Mapped entities (everything with a #Node) passed as parameter to a
function that is annotated with a custom query will be turned into a
nested map.
#Node
data class MyDTO(
#Id
val field1: String,
val field2: String
)
Replace {0} with $0 in custom query
Based on the following found in the reference docs https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom-queries.parameters
You do this exactly the same way as in a standard Cypher query issued
in the Neo4j Browser or the Cypher-Shell, with the $ syntax (from
Neo4j 4.0 on upwards, the old {foo} syntax for Cypher parameters has
been removed from the database).
...
[In the given listing] we are referring to the parameter by its name.
You can also use $0 etc. instead.
#Repository
interface MyRepository : PagingAndSortingRepository<MyEntity, String> {
// ConverterNotFoundException is thrown when this method is called
#Query("MATCH (e:MyEntity {attr1: $0.field1}) " +
"CREATE (e)-[l:LINK]->(n:OtherEntity {attr2: $0.field2))")
fun doSomethingWithDto(dto: MyDTO)
}
Details
spring-boot-starter: v2.4.10
spring-data-neo4j: v6.0.12
neo4j-java-driver: v4.1.4
Neo4j server version: v3.5.29
RTFM Custom conversions ...
Found the solution myself. Hopefully someone else may benefit from this as well.
Solution
Create a custom converter
import mypackage.model.*
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.neo4j.driver.Value
import org.neo4j.driver.Values
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair
import java.util.HashSet
class DtoToNeo4jValueConverter : GenericConverter {
override fun getConvertibleTypes(): Set<ConvertiblePair>? {
val convertiblePairs: MutableSet<ConvertiblePair> = HashSet()
convertiblePairs.add(ConvertiblePair(MyDTO::class.java, Value::class.java))
return convertiblePairs
}
override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor?): Any? {
return if (MyDTO::class.java.isAssignableFrom(sourceType.type)) {
// generic way of converting an object into a map
val dataclassAsMap = jacksonObjectMapper().convertValue(source as MyDTO, object :
TypeReference<Map<String, Any>>() {})
Values.value(dataclassAsMap)
} else null
}
}
Register custom converter in config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.neo4j.core.convert.Neo4jConversions
import org.springframework.core.convert.converter.GenericConverter
import java.util.*
#Configuration
class MyNeo4jConfig {
#Bean
override fun neo4jConversions(): Neo4jConversions? {
val additionalConverters: Set<GenericConverter?> = Collections.singleton(DtoToNeo4jValueConverter())
return Neo4jConversions(additionalConverters)
}
}
It's ridiculous that the framework would force you to write a custom converter for this. I made a #Transient object in my overridden User class for a limited set of update-able user profile fields, and I'm encountering the same error. I guess I will just have to break up the object into its component String fields in the method params to get around this problem. What a mess.
#Query("MATCH (u:User) WHERE u.username = :#{#username} SET u.firstName = :#{#up.firstName},u.lastName = :#{#up.firstName},u.intro = :#{#up.intro} RETURN u")
Mono<User> update(#Param("username") String username,#Param("up") UserProfile up);
No converter found capable of converting from type [...UserProfile] to type [org.neo4j.driver.Value]

Updating a hibernate entity with CrudRepository

I'm currently writing my first spring boot Kotlin application and am trying to create a rest API with JPA persistence. The basics are going fine but I'm struggling with updating a model on a patch endpoint (#patchMapping).
I want to adhere to proper rest standards and for that reason I'm hitting the patch endpoint with #PatchMapping("/company/{id}").
I would like to be able to call the CrudRepository in a way like this.
#PatchMapping("/company/{id}")
fun update(#PathVariable id: Long, #RequestBody updateRequest: Company) : Company {
return repository.update(updateRequest, id)
}
but it appears as if the spring way to do it is to pass the id of the object you're going to update within the requestBody? e.g.
repository.save(updateRequest)
which then auto merges the object. But this conflicts with any sane rest convention...
is there an integrated solution available for what I want to achieve? I'd like to refrain from writing my own logic as I'd hoped spring to have this functionality.
Do you need something like this?
#RestController
class Controller(private val service: CompanyService) {
#PatchMapping("/company/{id}")
fun update(#PathVariable id: Long, #RequestBody company: Company): Company {
return service.updateCompany(company, id)
}
}
#Service
class CompanyService (private val repository: CompanyRepository) {
#Transactional
fun updateCompany(company: Company, id: Long): Company {
val companyToUpdate = repository.findOne(id)
companyToUpdate.setSomething(company.getSomething)
raturn companyToUpdate;
}
}
interface CompanyRepository : CrudRepository<Company, Long>

Resources