New ORM Framework for Kotlin
The article introduces a Kotlin API for some ORMs to simplify database operations by providing a lightweight and intuitive interface.
Join the DZone community and get the full member experience.
Join For FreeIf you have an aversion to new frameworks, don't even read this. For other kind readers, please note that here I'm going to present a proposal for an API for modeling database queries in a declarative style with strong Kotlin type checking primarily. Only some classes around entities are implemented; the database connection is missing for now. In the project, I tried to evaluate my experience and vision so far. However, not all ideas presented in this paper are completely new. Some of them I drew from the Ujorm framework, and the entity concept was inspired by the Ktorm framework. But the code is new. The prototype results from a mosaic that has been slowly pieced together and has now taken a form that is hopefully worth presenting.
If you are not discouraged by the introduction, I will skip the general talk about ORM and let you get to the code samples. The demonstration examples use two relational database tables. This is an employee/department (unspecified organization) relationship where each employee may (or may not) have a supervisor. Both tables are described by entities from the following class diagram:
Suppose we want to create a report containing the unique employee number, the employee's name, the department name, and the supervisor's name (if any). We are only interested in departments with a positive identifier and a department name starting with the letter "D." We want to sort the report by department name (descending) and then by employee name (ascending). How could a query (SELECT) based on these entities look in the presented API?
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, // DB relation by the inner join
employees.superior + employees.name) // DB relation by the left outer join
.where((employees.department + departments.id GE 1)
AND (employees.department + departments.name STARTS "D"))
.orderBy(
employees.department + departments.name ASCENDING false,
employees.name ASCENDING true)
.toList()
The use of a DSL in a database query probably doesn't surprise anyone today. However, the chaining of the entity attribute model (hereafter, property-descriptor) is worthy of attention. Combining them creates a new composite property descriptor that implements the same interface as its atomic parts. The query filter (WHERE) is described by an object constructed from elementary conditions into a single binary tree. Composite property descriptors provide information from which SQL query sessions between database tables are also derived. This approach can cover perhaps the most common SQL queries, including recursive queries. But certainly not all of them. For the remaining ones, an alternative solution must be used. A rough design is in the project tests.
Let's focus next on the employee entity:
@Entity
interface Employee {
var id: Int
var name: String
var higherEducation: Boolean
var contractDay: LocalDate
var department: Department
var superior: Employee?
}
Entity is an interface with no other dependencies. The advantage is that the interface can get by (in ORM) without binary code modification. However, to get the object, you must use a factory method that supplies the implementation. An alternative would be to extend some generic class (provided by the framework), which I found more invasive. The metamodel pair object provides the factory method for creating new objects.
Each entity here needs a metamodel that contains information about its type and attributes that provides some services. The attributes of the metamodel are the property mentioned above descriptors of the pair entities. Note that the same property() method is used to create the property descriptors it doesn't matter if it is a session description (an attribute on another entity). The only exception is where the attribute (entity) type accepts NULL. The positive news is that the compiler will report the misuse (of the shorter method name) at compile time. An example of the employee entity metamodel is attached:
open class Employees : EntityModel<Employee>(Employee::class) {
val id = property { it.id }
val name = property { it.name }
val higherEducation = property { it.higherEducation }
val contractDay = property { it.contractDay }
val department = property { it.department }
val superior = propertyNullable { it.superior }
}
Annotations will declare the specific properties of the columns (database tables) on the entities so that the metamodel classes can be generated once according to their entity. The entity data is stored (internally) in an object array. The advantage is fewer memory requirements compared to an implementation based on the HashMap class.
The next example demonstrates the creation of new objects and their storage in the database (INSERT).
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)
The MyDatabase class (which provides the metamodel) is the only one here (the singleton design pattern). Still, it can generally be any object our application context provides (for example). If we wanted to use a service (cloning an entity object, for example), we could extend (that provider) with the AbstractEntityProvider class and use its resources. An example of the recommended registration procedure (metamodel classes), along with other examples, can be found in the project tests.
Conditions
A condition (or also a criterion) is an object we encountered when presenting a SELECT statement. However, you can also use a condition on its own, for example, to validate entity values or filter collections. If the library provided support for serializing it to JSON text format (and back), the range of uses would probably be even more expansive. To build the following conditions, we start from the metamodel already stored in the employees variable.
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)
If we have an employee object in the employee variable, the employee criterion can be tested with the following code:
expect(crn4(employee)).equals(true) // Valid employee
expect(crn5(employee)).equals(false) // Invalid employee
On the first line, the employee met the criterion; on the second line, it did not. If needed (during debugging or logging), the content of the conditions can be visualized in the text; examples are attached:
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)""")
Other Interesting Things
The property descriptor may not only be used to model SQL queries but can also participate in reading and writing values to the object. The simplest way is to extend the entity interface with the PropertyAccessor interface. If we have an employee object, code can be used to read it:
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]
The explicit declaration of variable data types is for presentation purposes only, but in practice, they are redundant and can be removed. Writing variables to an object is similar:
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
Please note that reading and writing values are done without overriding also for NULLABLE values. Another interesting feature is the support for reading and writing values using composite property descriptors. Just for the sake of argument, I assure you that for normal use of the object, it will be more convenient to use the standard entity API declared by the interface.
The sample above copies its attributes to variables and back. If we wanted to clone an object, we could use the following construct (shallow copy):
val target: Employee = MyDatabase.utils().clone(source)
No reflection methods are called during data copying, which is allowed by the class architecture used. More functional usage examples can be found in the project tests on GitHub.
Why?
Why was this project created? In the beginning, it was just an effort to learn the basics of Kotlin. Gradually a pile of disorganized notes in the form of source code was created, and it was only a matter of time before I came across language resources that would also lead to a simplified Ujorm framework API. Finding ready-made ORM libraries in Kotlin made me happy. However, of the two popular ones, I couldn't pick one that suited me better. I missed interesting features of one with the other and vice versa. In some places, I found the API not intuitive enough to use; in others, I ran into complications with database table recursion. A common handicap was (for my taste) the increased error rate when manually building the entity metamodel. Here one can certainly counter that entities can be generated from database tables. In the end, I organized my original notes into a project, cleaned up the code, and added this article. That is perhaps all that is relevant.
Conclusion
I like the integration with the core of a ready-made ORM framework; probably the fastest would be the integration with Ujorm. However, I am aware of the risks associated with any integration, and I can't rule out that this project won't eventually find any real use in the ORM field. The prototype is freely available under the Apache Commons 2 license. Thank you for your constructive comments.
Opinions expressed by DZone contributors are their own.
Comments