Skip to content

DATAMONGO-2138 - Type-safe Kotlin query extension #622

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

TjeuKayim
Copy link
Contributor

@TjeuKayim TjeuKayim commented Nov 22, 2018

https://jira.spring.io/browse/DATAMONGO-2138

I'm suggesting to add Kotlin extensions for "type-safe queries". This will prevent typos in field names, and improve the readability of a query.

New Query Syntax

We can achieve type safe queries with Kotlin's Property References.

mongoOperations.find<Book>(
  Query(Book::name isEqualTo "Moby-Dick")
)
// Nested Properties (i.e. refer to "book.author")
Book::author / Author::name regex "^H"

To-Do

  • Discuss changes in type hierarchy: leave it as is
  • Agree on typed query syntax.
  • Implement all Criteria features.
  • Update documentation.

@pivotal-issuemaster
Copy link

@TjeuKayim Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.

@pivotal-issuemaster
Copy link

@TjeuKayim Thank you for signing the Contributor License Agreement!

@odrotbohm odrotbohm requested a review from mp911de November 22, 2018 21:22
@odrotbohm
Copy link
Member

I am very reluctant to introduce changes in the type signature to cater a Kotlin extension. We don't want to add complexity for our Java developers (still more than 90% of our user base) that doesn't provide any obvious benefit. So far, the extensions we added didn't require those kinds of changes and I'd love to see that stay this way.

@TjeuKayim
Copy link
Contributor Author

You’re right. We should change as less Java as possible, and just add Kotlin extensions. I’m just trying to fit the new class TypedQuery inside the type hierarchy of Query, and it’s not that trivial to find an elegant solution.
If that fails, we just have to overload methods that use Query with TypedQuery using Kotlin Extensions.

@TjeuKayim TjeuKayim force-pushed the issue/DATAMONGO-2138 branch from b009401 to fc5e59b Compare November 23, 2018 05:56
@TjeuKayim
Copy link
Contributor Author

I am finally convinced that is it better not to change the signature of Query. Sometimes, it's hard to let go your idea.

@mp911de
Copy link
Member

mp911de commented Nov 23, 2018

How would an example look like when querying nested properties? (class Book(val author: Author), class Author(val name: String), query for book.author.name)

@TjeuKayim
Copy link
Contributor Author

I'm not sure how we should handle nested properties.
KMongo has a special "/" operator for that.
https://github.com/Litote/kmongo/blob/master/kmongo-kdoc/docs/typed-queries.md#nested-properties

@TjeuKayim
Copy link
Contributor Author

We can also consider 'typed updates'.

operations.updateFirst(Book::name isEqualTo "Moby-Dick", set(Book::price, 11))

@TjeuKayim
Copy link
Contributor Author

At first, I wanted to restricts the KProperty to members of the Entity class, but it requires much coding to make that work. Every use of Query as parameter will need an Kotlin extension, and the type parameter should be passed through to Criteria.
Maybe we should keep it simple, and trust the developers to use the right type. The concept of typed queries and updates will be more feasible then.

@mp911de
Copy link
Member

mp911de commented Nov 24, 2018

Let's focus on find operations right now and make sure that we can query properties across the document – basically let's support the same criteria features as we do with the regular Query. We can add update operations later on.

@TjeuKayim TjeuKayim force-pushed the issue/DATAMONGO-2138 branch from 09c3cca to c6a49cd Compare November 25, 2018 20:26
@TjeuKayim
Copy link
Contributor Author

This version has typed criteria builder functions for gt, lt, ne, size, all, gte, lte, mod, nin, isEqualTo and inValues. They can be chained using typedCriteria().

@TjeuKayim
Copy link
Contributor Author

Let's discuss the syntax for typed queries.

  1. The simplest implementation would just use the existing Criteria builder.

    Criteria(Book::name).eq("Moby-Dick")
        .and(Book::price).lt(1200)

    This requires the least coding, just some extension functions for Criteria.

  2. We could use infix functions.

    criteria(
        Book::name isEqualTo "Moby-Dick",
        Book::price lt 1200
    )

    This is a very clean and good looking syntax. However, the functions have to be imported. This could led to conflicting function names.

  3. Or we use lambdas with receiver.

    criteria {
        Book::name isEqualTo "Moby-Dick"
        Book::price lt 1200
    }

    The lambda's receiver has infix member functions to build typed criteria. The use of those functions will be restricted to the lambda parameter, and you don't need to import them.

What do you think?

@mp911de
Copy link
Member

mp911de commented Nov 26, 2018

API usage should impose as little constraints as possible on client code in terms of technical constraints. Importing functions bears the risk of clashing with existing application code/external code.

Examples 2 and 3 read naturally and if option 2 could clash with other imported functions, then option 3 seems the way to go. Reading criteria within a closure easily associates the AND nature of criteria combination.

We should also considerOR queries. Maybe something like:

criteria {
    Book::name isEqualTo "Moby-Dick"
    or {
        Book::price lt 1200
        Book::price gt 240
    }
}

@TjeuKayim
Copy link
Contributor Author

Looks nice! I was also imagining such "or" queries.
I will go for option 3 then.

@TjeuKayim
Copy link
Contributor Author

All Criteria features are now supported by my TypedCriteriaBuilder.
Can you review my code @mp911de?

@mp911de
Copy link
Member

mp911de commented Nov 28, 2018

Pretty decent work, I did a more in-depth review pass and found only minor issues such as formatting and we typically use AssertJ for assertions. Maybe @sdeleuze can have also a look once you're done with changes.

We'd like to include the change in the upcoming milestone release 2.2 M1 in early December. Once this PR is merged, we could have a look on typed updates.

@TjeuKayim
Copy link
Contributor Author

Thank you for the positive feedback! 😃

@sdeleuze
Copy link
Contributor

I will have look Monday asap back for vacation if that timing is ok from a Spring Data release perspective.

@TjeuKayim
Copy link
Contributor Author

Pretty decent work, I did a more in-depth review pass and found only minor issues such as formatting and we typically use AssertJ for assertions.

What specific formatting rules do you mean @mp911de?
I tried to apply the coding standards, but those are written for Java.
Most Kotlin files in the project use tabs, but some ones use spaces, like ReactiveAggregationOperationExtensions.kt.

Also, should I start test methods with a line break?

@sdeleuze
Copy link
Contributor

sdeleuze commented Dec 3, 2018

Thanks @TjeuKayim for this contribution.

I think there are issues in the API design that we need to fix before merging that PR. For example I am wondering if we could improve logical operators since I am not super fond of the following syntax:

or(
    { Book::price lt 1200 }
    { Book::price gt 240 }
 )

or({ Book::price lt 1200 })

I think the root issue is that the API is not composable (KProperty extensions return Unit), and I am not sure we need to use lambdas with receiver for that use case, since criteria are built from a KProperty (they are usually used to propose a set of top level functions to the developer via autocomplete).

What about just using KProperty extensions that returns Criteria and modify or and and to be infix extensions of Criteria is order to allow to translate

Criteria("title").isEqualTo("Moby-Dick")
    .orOperator(Criteria("price").lt(1200), Criteria("price").gt(240))

To (Book::title isEqualTo "Moby-Dick") or (Book::price lt 1200 and Book::price.gt 240)

That seems closer to the Java API principle, while leveraging Kotlin capabilities to provide a more readable syntax.

Any thought?

@odrotbohm
Copy link
Member

I don't think the two statements are the same. This:

Criteria("title").isEqualTo("Moby-Dick")
    .orOperator(Criteria("price").lt(1200), Criteria("price").gt(240));

is probably meant to be:

(Book::title isEqualTo "Moby-Dick") and (Book::price lt 1200 or Book::price.gt 240)

Note the swapped and and or.

@TjeuKayim
Copy link
Contributor Author

TjeuKayim commented Dec 3, 2018

Alright then. Let's use top level functions, like before bc12a92.
The builder functions will return a TypedCriteria object that stores the operation and lazily implements CriteriaDefinition.

@TjeuKayim
Copy link
Contributor Author

I did a quick refactor to change the syntax.

mongoOperations.find<Book>(Query(
  Book::name isEqualTo "Moby-Dick"
))
typedCriteria(
  Book::name isEqualTo "Moby-Dick",
  orOperator(
    Book::price lt 1200,
    Book::price gt 240
  )
)

@TjeuKayim
Copy link
Contributor Author

I think these expressions should match:

(Book::title isEqualTo "Moby-Dick") and ((Book::price lt 1200) or (Book::price gt 240))

Criteria().andOperator(
	Criteria("title").isEqualTo("Moby-Dick"),
	Criteria().orOperator(Criteria("price").lt(1200), Criteria("price").gt(240))
)

@mp911de
Copy link
Member

mp911de commented Dec 3, 2018

andOperator adds another indirection, so combination through AND on the top-level should reflect

Criteria("title").isEqualTo("Moby-Dick")
    .orOperator(Criteria("price").lt(1200), Criteria("price").gt(240));

andOperator is useful when specifying constraints for the same field multiple times (e.g. $and: [ { price: { $ne: 1.99 } }, { price: { $exists: true } } ])

@TjeuKayim
Copy link
Contributor Author

Do you mean that and() and andOperator() should be two different functions? That makes sense.

@sdeleuze
Copy link
Contributor

sdeleuze commented Dec 3, 2018

@TjeuKayim Do you think it could be possible to make KProperty extensions directly returning Criteria instances and remove TypedCriteria, typedCriteria() and typedQuery()?

@TjeuKayim
Copy link
Contributor Author

TjeuKayim commented Dec 7, 2018

Merge a subset of the changes we've seen so far and produce some documentation.

That's probably a good idea.
This feature request is for typed queries. So instead of plain strings, this pull request should add extensions to use Kotlin's Property references. Support for nested properties is needed. Also some extensions to use it within the Criteria builder (where(KProperty) & Criteria.and(KProperty).
I will push a new version with just those changes, and create a follow-up ticket later.

@TjeuKayim TjeuKayim force-pushed the issue/DATAMONGO-2138 branch from 10737a7 to 29f4bca Compare December 7, 2018 07:05
@mp911de
Copy link
Member

mp911de commented Dec 7, 2018

I've got a new idea. What if we stick more to the original Criteria syntax. So instead of KProperty extensions, we can make new where() and and() functions that take a KPropery and delegate to their Java counterpart. Other methods of criteria can be extended with infix Kotlin functions.

We should stick to (Book::title isEqualTo "Moby-Dick") and (Book::price lt 1200).
The proposed where(Book::name) isEqualTo "Moby-Dick" and Book::price lt 1200 format is less consistent and less readable.

@sdeleuze
Copy link
Contributor

sdeleuze commented Dec 7, 2018

@mp911de I agree with your last comment and your proposal to deal with that in 2 steps.

@sdeleuze
Copy link
Contributor

sdeleuze commented Dec 7, 2018

@TjeuKayim Please ping me when I can review the PR updated according to @mp911de feedback, I would like to have a look before the merge.

@TjeuKayim
Copy link
Contributor Author

TjeuKayim commented Dec 7, 2018

I also prefer this syntax (Book::title isEqualTo "Moby-Dick") and (Book::price lt 1200).

What other features should be included in this pull request?
Just the infix KProperty extensions? Without and, nor and or.

Accidentally hit the "Close" button on my smartphone

@TjeuKayim TjeuKayim closed this Dec 7, 2018
@TjeuKayim TjeuKayim reopened this Dec 7, 2018
@mp911de
Copy link
Member

mp911de commented Dec 7, 2018

Just the infix KProperty extensions? Without and, nor and or.

Exactly. Please also squash your commits. This makes it easier to merge.

@TjeuKayim TjeuKayim force-pushed the issue/DATAMONGO-2138 branch from a5111dc to 447ccb2 Compare December 7, 2018 15:15
@TjeuKayim
Copy link
Contributor Author

@sdeleuze, you can review the PR if you like.
I also squashed my commits.

How can you set asciidoctor's output to HTML? (I'm using the Maven plugin)

@mp911de
Copy link
Member

mp911de commented Dec 7, 2018

How can you set asciidoctor's output to HTML? (I'm using the Maven plugin)

Not sure I follow. Do you want to generate the reference documentation? If so, then run Maven with the distribute profile in the project root:

$ mvn install -Pdistribute

You can find the generated documentation in target/site/reference/html/index.html.

@TjeuKayim
Copy link
Contributor Author

Yes, I wanted to generate the reference documentation, but used the wrong maven command. With the distribute profile, it’s working. Thank you helping.

Where should I add the documentation for this feature? I was thinking of a new subheading below “Additional Query Options”, and calling it “Typed Queries for Kotlin”.

@TjeuKayim TjeuKayim force-pushed the issue/DATAMONGO-2138 branch from e348980 to 6907000 Compare December 9, 2018 08:59
@sdeleuze
Copy link
Contributor

sdeleuze commented Dec 9, 2018

Thanks @TjeuKayim, the latest commit is ok for me.

@mp911de
Copy link
Member

mp911de commented Dec 9, 2018

Awesome work @TjeuKayim. Let's move Typed Queries with Kotlin directly after Fluent Template API. We will do the final review tomorrow and merge your changes to master so it can be shipped with Moore M1.

@TjeuKayim
Copy link
Contributor Author

TjeuKayim commented Dec 9, 2018

Great tot hear that. 😃
I will be available tomorrow from about 11:00 CET.

@TjeuKayim TjeuKayim force-pushed the issue/DATAMONGO-2138 branch from 6907000 to 796a282 Compare December 10, 2018 10:33
@TjeuKayim
Copy link
Contributor Author

I placed the documentation after Fluent Template API.

@TjeuKayim TjeuKayim force-pushed the issue/DATAMONGO-2138 branch from 796a282 to a8d8996 Compare December 10, 2018 10:56
@TjeuKayim TjeuKayim changed the title DATAMONGO-2138 - Typed Queries for Kotlin DATAMONGO-2138 - Type-safe Kotlin query extension Dec 10, 2018
Copy link
Member

@mp911de mp911de left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot. We'll take this pull request from here.

mp911de pushed a commit that referenced this pull request Dec 10, 2018
We now support type-safe queries using Kotlin's DSL capabilities by accepting property references. Property references map to property paths and are translated to Criteria objects.

mongoOperations.find<Book>(
  Query(Book::title isEqualTo "Moby-Dick")
)

mongoOperations.find<Book>(
  Query(Book::title exists true)
)

mongoOperations.find<Book>(
 Query(Criteria().andOperator(
  Book::price gt 5,
  Book::price lt 10)
))

Original pull request: #622.
mp911de added a commit that referenced this pull request Dec 10, 2018
Rename NestedProperty to KPropertyPath to reflect the underlying concept in alignment with our own PropertyPath type. Rename nestedFieldName(…) method to asString(…) to align with Kotlin method terminology. Reformat.

Slightly reword documentation. Add Type-safe Queries for Kotlin to What's New section.

Original pull request: #622.
@mp911de
Copy link
Member

mp911de commented Dec 10, 2018

That's merged and polished now. Thanks a lot for this excellent pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants