Skip to content
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

Feature request: add the ability to copy query beans #3296

Closed
Incanus3 opened this issue Dec 16, 2023 · 3 comments · Fixed by #3304
Closed

Feature request: add the ability to copy query beans #3296

Incanus3 opened this issue Dec 16, 2023 · 3 comments · Fixed by #3304
Assignees
Milestone

Comments

@Incanus3
Copy link
Contributor

Rationale

It happens to us quite often that we'd like to reuse some base part of a query built using query beans and then construct several queries on top of it using additional chaining methods. The problem is, the generated chaining methods (e.g. name.eq("Tom")) are all destructive - they mutate the underlying Query in place and there doesn't seem to be a way to copy the current "version" of the query bean.

Example

val adults = QPerson().age.gt(18)
val adultEuropeans = adults.continent.eq("Europe")
// this won't work as expected as it also includes the "where continent = 'Europe'" part
val adultAmericans = adults.continent.eq("Americas")

Implementation notes

I see Query already has a copy() method and TQRootBean already has a constructor variant taking Query, so (I might be completely wrong here of course) it might even be as simple as generating a similar constructor variant for each query bean along with a copy() method which would 1) call copy() on the query and 2) call the new constructor variant with this copied query. Then we could use this as

val adults = QPerson().age.gt(18)
val adultEuropeans = adults.copy().continent.eq("Europe")
val adultAmericans = adults.copy().continent.eq("Americas")continent = 'Europe'" part
@Incanus3
Copy link
Contributor Author

Incanus3 commented Dec 16, 2023

Actually, we have an even stronger motivation for wanting this. We like to create a wrapper query-building classes for our queries like

class EaObjectPropertyQuery(
    private val database: Database,
    private override val ebeanQuery: QEaObjectProperty = QEaObjectProperty(database),
) : DbQuery<EaObjectProperty, QEaObjectProperty>(EaObjectProperty::class) {
    fun withId(id: Long): EaObjectPropertyQuery =
        chainQuery { where().id.eq(id) }

    fun withName(name: String): EaObjectPropertyQuery =
        chainQuery { where().property.eq(name) }

    fun withValue(value: String): EaObjectPropertyQuery =
        chainQuery { where().value.eq(value) }

    fun withObject(eaObject: EaObject): EaObjectPropertyQuery =
        chainQuery { where().eaObject.eq(eaObject) }

    fun withObjectIn(objects: Collection<EaObject>): EaObjectPropertyQuery =
        chainQuery { where().objectId.isIn(objects.map { it.id }) }

    fun withObjectIn(objectQuery: EaObjectQuery): EaObjectPropertyQuery =
        chainQuery { where().objectId.isIn(objectQuery.ebeanQuery.select(QEaObject._alias.id).query()) }

    private fun chainQuery(chain: QEaObjectProperty.() -> QEaObjectProperty) =
        // FIXME: this doesn't clone the query, it would be nice if we could call `ebeanQuery.copy()` here
        EaObjectPropertyQuery(database, chain(ebeanQuery))
}

abstract class DbQuery<EntityType : Any, QueryType : TQRootBean<EntityType, QueryType>>(
    private val entityClass: KClass<EntityType>,
) {
    protected abstract val ebeanQuery: QueryType

    override fun findOne(): EntityType? {
        try {
            return ebeanQuery.findOne()
        } catch (error: NonUniqueResultException) {
            throw FoundMoreThanOne(entityClass, error)
        }
    }

    override fun findList(): List<EntityType> {
        return ebeanQuery.findList()
    }

    override fun exists(): Boolean {
        return ebeanQuery.exists()
    }

    override fun count(): Int = ebeanQuery.findCount()

    override fun forEach(action: (EntityType) -> Unit) {
        ebeanQuery.findEach(action)
    }

    override fun delete() {
        ebeanQuery.delete()
    }
}

Then we can use this as EaObjectPropertyQuery(database).withObject(eaObject).withName("property1").findOne(). In this simple case it might look as a redundant layer of abstraction, but 1) we like to have a layer that shields the rest of the codebase from the ORM so that it doesn't need to know about its internals and more importantly 2) some of our query methods are much more complex.

The problem here (as stated in the comment in EaObjectPropertyQuery.chainQuery()) is that there's no way to copy the querybean-based queries, so the only way to reuse part of the queries built using these wrappers is to construct them anew.

@rbygrave
Copy link
Member

it might even be as simple as generating a similar constructor variant for each query bean along with

Yes, that should work.

@Incanus3
Copy link
Contributor Author

it might even be as simple as generating a similar constructor variant for each query bean along with

Yes, that should work.

That would be perfect. Thank you for considering this.

rbygrave added a commit that referenced this issue Jan 15, 2024
@rbygrave rbygrave linked a pull request Jan 15, 2024 that will close this issue
rbygrave added a commit that referenced this issue Jan 15, 2024
@rbygrave rbygrave self-assigned this Jan 15, 2024
@rbygrave rbygrave added this to the 13.25.3 milestone Jan 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants