Kotlin interface default methods are considered query methods [DATACMNS-1223]
Ben Madore opened DATACMNS-1223 and commented
If i had an interface with method:
public List<Person> findByFirstNameAndLastNameAndAgeGreaterThan(String firstName, String lastName, Int age);
Can I create an alias of this, possibly setting some default values, by using default methods? e.g.
public List<Person> findAdults(String firstName, String lastName) {
return this.findByFirstNameAndLastNameAndAgeGreaterThan(firstName, lastName, 18);
}
I'm told that this may work in 2.x (though i don't have a Spring 5 project to try this on) but it definitely does not work in 1.11.8 as it results in org.springframework.data.mapping.PropertyReferenceException: No property Adults found for type Person!.
If it does not work in 2.0, it seems like it would be a nice addition... if it DOES work in 2.0 it would be nice to have documented. Well, I think either way, having documentation about the expected behaviour of default methods in Repository classes would be a nice thing to have.
Affects: 2.0.2 (Kay SR2)
Reference URL: https://stackoverflow.com/questions/49190066/how-can-i-use-kotlin-default-methods-with-spring-data-repository-interfaces/49190067#49190067
Issue Links:
- DATAJPA-1489 Support for custom query implementation methods with Kotlin interfaces ("is duplicated by")
2 votes, 5 watchers
Oliver Drotbohm commented
What you describe already works (and has for quite a while even, so it should work in current Ingalls releases) if you actually turn the method you list into a default method, i.e. you're declaration is missing the default keyword. There's an example showing this here but I've checked our reference docs and it doesn't actually mention that feature. So I'll turn this one into a request to improve the documentation so that it's easier to find out about that
Ben Madore commented
Good point on the default keyword, i was trying to simplify our actual code into a more general case, and mistakenly left off the intended default. The actual code that failed for us using 1.11.8 though is kotlin, so i suppose it could be doing something strange underneath the covers.
One thing about the example you linked to is that the default method name does only contain valid Entity properties, I assume this is not required, it just is the case in that example?
Solely for reference the actual kotlin code that was failing for us was:
fun findByEmailContainingIgnoreCaseOrFirstNameContainingIgnoreCaseOrLastNameContainingIgnoreCase(email: String,
firstName: String,
lastName: String,
pageable: Pageable): Page<Account>
fun search(email: String, firstName: String, lastName: String, pageable: Pageable) =
findByEmailContainingIgnoreCaseOrFirstNameContainingIgnoreCaseOrLastNameContainingIgnoreCase(email, firstName, lastName, pageable)
and the error is: org.springframework.data.mapping.PropertyReferenceException: No property search found for type Account!
Oliver Drotbohm commented
I am not a Kotlin expert, but are you sure the latter method is actually discoverable as default method? In the 1.x branch we use this piece of code to detect default methods and that seems to work for Java default methods. Might be worth just trying to call that method with a Method instance obtained from your Kotlin declared method to sort out the obvious. In 2.x we just call method.isDefault() to detect that.
If you have a dead simple sample project just trying these two methods (probably simplified as well) I can most certainly have a look.
Ben Madore commented
So... I did a bit more digging, and yep, you're right, it's a kotlin problem. They dont' actually generate default interface methods they generate static methods... even when you target java 8 :(
Interestingly enough multiple people in the tracking ticket specifically mention spring data :-D https://youtrack.jetbrains.com/issue/KT-4779
Ben Madore commented
Not sure if there's an easy way to extend your isDefaultMethod() check to account for that, assuming not, might be something worth calling out in any new documentation created to document default methods
Oliver Drotbohm commented
If I understand the referenced ticket correctly, it's not even static methods but an interface method plus some method on a default implementation. I'll need to resort to Mark Paluch expertise to judge whether there's a way we can find out about this by reflection
Ben Madore commented
Yes, you're right, sounds like they generate an interface and a default impl (which isn't visible to java-side, so they always have to implement the "default" method).
I got the static bit i got from the response of someone when I asked for an answer in kotlin slack... i admittedly didn't initially read through the ticket in enough detail to realize i should have corrected it.
Mark Paluch commented
That's correct, Kotlin generates a regular interface method. Consider following Kotlin interface declaration:
interface KotlinUserRepository : Repository<User, String> {
fun findById(username: String): User
fun search(username: String) = findById(username)
}
Then the Kotlin compiler creates the following, corresponding Java code:
public interface KotlinUserRepository extends Repository {
User findById(String username);
User search(String username);
@Metadata(…)
public static final class DefaultImpls {
public static User search(KotlinUserRepository $this, String username) {
Intrinsics.checkParameterIsNotNull(username, "username");
return $this.findById(username);
}
}
}
As far as I see, there is no public reflection API to determine whether an method (KFunction) is a default method (that delegates to InterfaceName$DefaultImpls#methodName(…))) or whether it's a regular method. The proposed temporary workaround of @JvmDefault is a patch, not a solution.
Furthermore, these methods are callable from Java code and from that perspective we would run into a non-existent query method.
Technically, we could compensate for Kotlin by mimicking what the Kotlin compiler does: Attempt to lookup DefaultImpls#methodName(…)) and forward the call to the default implementation. I think that's something we should not do:
- We have no public API to figure out whether it's a default method or not
- We would heavily rely on Kotlin internals over which we don't have control and open a can of worms
- We don't know how this topic evolves
From that perspective, I vote to do nothing on our end. Either declare your Kotlin repository interface without Kotlin's default methods or create these directly in Java by using regular default methods – you can leverage @NonNull annotations to indicate (non-)nullability
Ben Madore commented
Mark,
I agree with your technical assessment of "Do Nothing" for the time being - though I think it would be quite helpful to mention this somewhere in the documentation, considering at least a few other people have commented on the Kotlin issue considering Spring 5+ in general is treating Kotlin as a first class citizen.
cheers!
Rui Miguel Pereira Figueira commented
As a workaround:
@Repository
interface InternalKotlinUserRepository : Repository<User, String> {
fun findById(username: String): User
}
@NoRepositoryBean
@Component
class KotlinUserRepository(repo: InternalKotlinUserRepository) : Repository<User, String> by repo {
fun search(username: String) = findById(username)
}
Basically, KotlinUserRepository is not an actual repository (we mark it as @NoRepositoryBean) but delegates into our actual Spring Data repository InternalKotlinUserRepository using Kotlin delegation. This way, we can put our "default" methods in KotlinUserRepository without worring about Spring Data to try to map them into JPA queries.
Rui Miguel Pereira Figueira commented
Kotlin now supports default methods on interfaces using a @JvmDefault, but it is still an experimental feature. So, it is another workaround
Given the fact that Kotlin provides a way to provide proper default methods, we can close this ticket here.