A very early prototype of the Kotlin library for modelling filters of domain objects. The solution was inspired by the Ujorm key-value framework, but this code is completely new. Topical areas of use are:
- dynamic validations
- alerting
- modelling conditions for ORM
The ultimate goal of this project is to provide a programming interface (in Kotlin)
for building a database query model using DSL ready API, which allows type-safe building of the SQL queries at compile time.
A unique feature is the possibility of chaining the properties of the related entities and the simple rules for metamodel building.
An entity here is an interface that does not depend on any parent.
At runtime, a virtual implementation is created from the interface that is based on storing values in the object array.
Compared to storing data in the Map
object, this is a more memory-efficient and probably faster solution.
Let's have a simple domain model of employees and their departments, where every employee can have a superior.
Then the database query (SELECT
) can look like this:
fun comprehensiveDatabaseSelect() {
val employees: Employees = MyDatabase.employees // Employee metamodel
val departments: Departments = MyDatabase.departments // Department metamodel
val result: List<Employee> = MyDatabase.select(
employees.id,
employees.name,
employees.department + departments.name, // Required relation by the inner join
employees.superior + employees.name, // Optional relation by the left outer join
employees.department + departments.created)
.where (employees.department + departments.id GE 1
AND (employees.department + departments.name STARTS "D"))
.orderBy(employees.department + departments.created ASCENDING false)
.toList()
expect(result).toHaveSize(1)
expect(result.first().department.name)
.toEqual("Development")
}
and an INSERT
for example:
internal fun insertRows() {
val development: Department = MyDatabase.departments.new {
name = "Development"
created = LocalDate.of(2020, 10, 1)
}
val lucy: Employee = MyDatabase.employees.new {
name = "Lucy"
contractDay = LocalDate.of(2022, 1, 1)
superior = null
department = development
}
val joe: Employee = MyDatabase.employees.new {
name = "Joe"
contractDay = LocalDate.of(2022, 2, 1)
superior = lucy
department = development
}
MyDatabase.save(development, lucy, joe)
}
@Entity
interface Employee {
var id: Int
var name: String
var contractDay: LocalDate
var department: Department
var superior: Employee?
}
/** Model of the entity can be a generated class in the feature */
open class Employees : EntityModel<Employee>(Employee::class) {
val id = property { it.id }
val name = property { it.name }
val contractDay = property { it.contractDay }
val department = property { it.department }
val superior = propertyNullable { it.superior }
}
/** Initialize, register and close the entity model. */
val MyDatabase.employees by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
MyDatabase.add(Employees().close<Employees>())
}
New objects are created using the factory method, which provides the interface a necessary implementation. The values of domain object attributes can be accessed using the interface API, but also through the property descriptors. Here are some more examples of basic usage of this domain model.
val employees = MyDatabase.employees // Employee metamodel
val departments = MyDatabase.departments // Department metamodel
val employee: Employee = employees.new { // Create new employee object
id = 11
name = "John"
contractDay = LocalDate.now()
department = createDepartment(2, "D")
}
// Read and Write values by property descriptors:
val id: Int = employee[employees.id]
val name: String = employee[employees.name]
val contractDay: LocalDate = employee[employees.contractDay]
val department: Department = employee[employees.department]
val superior: Employee? = employee[employees.superior]
val departmentName: String = employee[employees.department + departments.name]
employee[employees.id] = id
employee[employees.name] = name
employee[employees.contractDay] = contractDay
employee[employees.department] = department
employee[employees.superior] = superior
employee[employees.department + departments.name] = departmentName
// Composed properties:
employee[employees.department + departments.id] = 3
employee[employees.department + departments.name] = "C"
expect(employee.department.id).toEqual(3)
expect(employee.department.name).toEqual("C")
expect(employee[employees.department + departments.id]).toEqual(3)
expect(employee[employees.department + departments.name]).toEqual("C")
// Criterion conditions:
val crn1 = employees.name EQ "Lucy"
val crn2 = employees.id GT 1
val crn3 = (employees.department + departments.id) LT 99
val crn4 = crn1 OR (crn2 AND crn3)
val crn5 = crn1.not() OR (crn2 AND crn3)
expect(crn1(employee)).equals(false) // Invalid employee
expect(crn4(employee)).equals(true) // Valid employee
expect(crn1.toString()).toEqual("""Employee: name EQ "Lucy"""")
expect(crn2.toString()).toEqual("""Employee: id GT 1""")
expect(crn3.toString()).toEqual("""Employee: department.id LT 99""")
expect(crn4.toString()).toEqual("""Employee: (name EQ "Lucy") OR (id GT 1) AND (department.id LT 99)""")
expect(crn5.toString()).toEqual("""Employee: (NOT (name EQ "Lucy")) OR (id GT 1) AND (department.id LT 99)""")
Common classes of the framework (released: 2022-11-26)
An example implementation of this demo project (released: 2022-11-26)
- Building remote attribute models (via relationships) is not supported yet (including reading and writing values of POJO)
- API cleaning
- Serialize a
Criterion
object intoJSON
format and parsing the result back to the object. - The domain object model should be generated according to the original POJO objects in feature
- Integrate the model filters with the some ORM framework (Ujorm ?)
For more information see links to source code of API tests: