0

I would like to create something similar to Ruby's ActiveRecord Scopes using Kotlin Exposed.

For example I would like to break the following query up so that the first part acts like a scope.

This query returns what I want.

val m1 = Measurement.wrapRows(Measurements.innerJoin(Runs).select {
        ((exists(Tags.select { Tags.run.eq(Runs.sequelId) and Tags.name.eq("region") and Tags.value.eq("default") })) or notExists(Tags.select {
            Tags.run.eq(Runs.sequelId) and Tags.name.eq("region")
        })) and Measurements.name.eq("someName")

I would like to use this part as a scope:

val q1 = Measurements.innerJoin(Runs).select {
    ((exists(Tags.select { Tags.run.eq(Runs.sequelId) and Tags.name.eq("region") and Tags.value.eq("default") })) or notExists(Tags.select {
        Tags.run.eq(Runs.sequelId) and Tags.name.eq("region")
    }))
}

and then be able to refine the query using the q1 "scope"

so something like this:

val q2 = q1.having { Measurements.name.eq("someName")  } // which does not work

Ultimately I would like to push this down into either the Measurements object or the Measurement class so I can do something like this

Measurement.withDefaultRegion.where( Measurements.name.eq("someName")
nPn
  • 16,254
  • 9
  • 35
  • 58

1 Answers1

1

I was able to get what I wanted by adding a couple of functions to the model's companion object.

The first one provides the "scope"

fun defaultRegion() :Op<Boolean> {
    return Op.build {(exists(Tags.select { Tags.run.eq(Runs.sequelId) and Tags.name.eq("region") and Tags.value.eq("default") })) or notExists(Tags.select {
        Tags.run.eq(Runs.sequelId) and Tags.name.eq("region")
    })}
}

The second function does the query using the scope and any refinements passed in and returns a "collection" of objects.

fun withDefaultRegionAnd( refinedBy: (SqlExpressionBuilder.()->Op<Boolean>)) : SizedIterable<Measurement> {
    return Measurement.wrapRows(Measurements.innerJoin(Runs).select(Measurement.defaultRegion() and SqlExpressionBuilder.refinedBy() ))
}

At the client level I can simply do this:

val measurements = Measurement.withDefaultRegionAnd { Measurements.name.eq("someName") }

Here are the nearly table object and entity classes:

object Measurements : IntIdTable("measurements") {

    val sequelId = integer("id").primaryKey()
    val run = reference("run_id", Runs)
    // more properties
}

class Measurement(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<Measurement>(Measurements) {
        fun defaultRegion() :Op<Boolean> {
            return Op.build {(exists(Tags.select { Tags.run.eq(Runs.sequelId) and Tags.name.eq("region") and Tags.value.eq("default") })) or notExists(Tags.select {
                Tags.run.eq(Runs.sequelId) and Tags.name.eq("region")
            })}
        }
        fun withDefaultRegionAnd( refinedBy: (SqlExpressionBuilder.()->Op<Boolean>)) : SizedIterable<Measurement> {
            return Measurement.wrapRows(Measurements.innerJoin(Runs).select(Measurement.defaultRegion() and SqlExpressionBuilder.refinedBy() ))
        }
    }

    var run by Run referencedOn Measurements.run
    var name by Measurements.name
    // more properties
}
nPn
  • 16,254
  • 9
  • 35
  • 58