dOrm is a complete dynamic orm ( Object Relational Mapper ), that doesn't require to setup custom table structures per entity but is able to map dynamic entities on a database based on a couple of technical tables only. In addition to the server side api a GraphQL interface has been implemented with identical scope.
Before we describe the solution, let's figure out what the problem is....
Typical applications use the normal ORMs that persist entities on table structures that are known at compile time predefined in the underlying dbms. While this is fine for most cases, there are some situations, where we need to configure entities dynamically. Think of a custom workflow definition ( e.g. with Camunda ) that wants to persist some complex internal state, which is not known upfront.
Exactly this problem is solved by the current implementation.
Let's look a simple example first:
Assuming we have an injected ObjectManager
which is responsible for transaction and object lifecycle management, we are able to
specify an entity by defining the attributes including type and type constraint information
personDescriptor = manager.type("person")
.add(attribute("name").type(string().length(100))) // string with length constraint
.add(attribute("age").type(int().greaterThan(0)))
.add(relation("parents").target("person").multiplicity(Multiplicity.ONE_OR_MANY).inverse("children"))
.add(relation("children").target("person").multiplicity(Multiplicity.ZERO_OR_MANY).inverse("parents"))
...
.register()
With this structural information - which is also persisted in the database - we can create and access DataObject
instances, that carry the payload information
manager.begin()
try {
val parent = manager.create(personDescriptor)
// set some values by the custom get and set operators
person["name"] = "Andi"
person["age"] = 58
val child = manager.create(personDescriptor)
// set some values by the custom get and set operators
child["name"] = "Nika"
child["age"] = 14
// add relation
parent.relation("children").add(child)
}
finally {
manager.commit() // will create!
}
Let's query all persons persisted so far...
val queryManager = manager.queryManager()
manager.begin()
try {
val person = queryManager.from(personDescriptor)
val query = queryManager
.create()
.select(person)
.from(person)
val queryResult = query.execute().getResultList()
val name = queryResult[0]["name"]
// let's modify values
queryResult[0]["age"] = 30 // better!
}
finally {
manager.commit() // will update all dirty objects
}
Since the objects are managed by the ObjectManager
we can update values easily. The manager will know about any changes and will
persist them at the end of the transaction.
Projections are possible as well, as seen here
val queryManager = manager.queryManager()
manager.begin()
try {
val person = queryManager.from(personDescriptor)
val children = person.join("children")
val query = queryManager
.create()
.select(person.get("age"), person.get("name")
.where(eq(children.get("name"), "Nika"))
val tupleResult = query.execute().getResultList()
val name = tupleResult[0][1]
}
finally {
manager.commit()
}
In addition to a criteria api like query, we are of course able to specify hql like queries as well:
val queryManager = manager.queryManager()
manager.begin()
try {
val query = manager.query<DataObject>("SELECT p.name FROM person AS p JOIN p.children as children WHERE children.name = :name")
val queryResult = query.executor()
.set("name", "NIka")
.execute()
.getResultList()
...
}
finally {
manager.commit() // will update
}
The solution is pretty straight forward. Entities are stored as a combination of three technical tables
ENTITY
a table referencing the entity definition and a generated primary keyPROPERTY
a table that will store single attributes of an entityRELATIONS
a bridge table expressing object relations
The property table defines the columns
TYPE
the id of the entity structureENTITY
the id of the corresponding entityNAME
the property name
and a number of columns that are able to store payload data with respect to the supported low-level data types
STRING_VALUE
a string valueINT_VALUE
a int value ( stores boolean values well )DOUBLE_VALUE
a floating point value
In order to model relations, the property table has a reflexive relation that expresses relationships stpored in a bridge table.
As the definition of an entity is known, the engine will know which attributes are stored in which columns.
Let's look at a simple query, that will read a single person.
select
p.PROPERTY,
p.ENTITY,
p.DOUBLE_VALUE,
p.INT_VALUE,
p.STRING_VALUE,
p.TYPE
from
PROPERTY p
where
p.ENTITY=?
After reading the result set, the engine will create the appropriate DataObject
instance and store the appropriate values in the correct places.
If we talk about queries, that code gets a little bit more complicated. Querying for an integer attribute "age" with the operator "=" and value 58 will result in something like
select
p1_0.ATTRIBUTE,
p1_0.ENTITY,
p1_0.DOUBLE_VALUE,
p1_0.INT_VALUE,
p1_0.STRING_VALUE,
p1_0.TYPE
from
PROPERTY p1_0
where
p1_0.ENTITY in (
(select distinct p2_0.ENTITY
from PROPERTY p2_0
where
p2_0.TYPE="person"
and p2_0.NAME="age"
and p2_0.INT_VALUE=58))
order by
p1_0.ENTITY
Of course, the performance and storage requirements are not as good as if we would map on static tables, since
- we have a lot a attribute rows
- indexes are much bigger
- we require a subselect for every condition
Let's look at a simple benchmark, that
- creates objects
- rereads all objects
- filters objects ( that will match all objects )
- filters and projects to a single attribute
- updates a single property flushing all objects
The test was repeated with 2 scenarios executing 2000 times each.
- JPA entity with 10 properties
- DORM object with 1 properties
- DORM object with 10 properties
The results ( avg time per object in ms ) are based on a H2 database ( on my old macbook :-) ).
Test | JPA | DORM(1) | DORM(10) |
---|---|---|---|
Create | 0.067 | 0.077 | 1.015 |
Read | 0.0405 | 0.217 | 0.4875 |
Filter | 0.019 | 0.07 | 0.3705 |
Filter & Project | 0.003 | 0.0505 | 0.0655 |
Update | 0.0635 | 0.2575 | 0.4495 |
As you can see, the biggest difference is the create test, since it has to create a row per property. Reading is surpisingly fast, even though a lot of rows need to be read and processed. Update is almost even, since only a single property was changed. The difference would grow again, the more properties are touched.
Still, not bad, huh?
In addition to the supplied Kotlin API a GraphQL server is available giving access to all CRUD and Query possibilities. The internal schema is created dynamically based on the internal registry.
For the already mentioned Person
object, it will create a schema based on the following technical types and inputs
type OperationResult {
count: Int
}
input FloatFilter {
eq: Float
ge: Float
gt: Float
le: Float
lt: Float
ne: Float
}
input IntFilter {
eq: Int
ge: Int
gt: Int
le: Int
lt: Int
ne: Int
}
input StringFilter {
eq: String
ne: String
}
input BooleanFilter {
eq: Boolean
ne: Boolean
}
resulting in
type Person {
id: Int
name: String
age: Int
children: [Person]
father: Person
}
input PersonInput {
id: Int
name: String
age: Int
children: [PersonInput]
father: PersonInput
}
input PersonFilter {
and: [PersonFilter]
or: [PersonFilter]
id: IntFilter
name: StringFilter
age: IntFilter
father: PersonFilter
}
type Query {
Person(where: PersonFilter): [Person]
}
type Mutation {
createPerson(input: PersonInput): Person
deletePersons(where: PersonFilter): OperationResult
updatePerson(input: PersonInput): Person
updatePersons(input: PersonInput, where: PersonFilter): [Person]
}
A query - already showing a join - will look like:
query sampleQuery {
Person (
where: {
father: {name: {eq: "Andi"}}
}
) {
id
name
father {
id
name
}
}
}
An bulk update:
mutation {
updatePersons(
where: {
id: {eq: 1}
},
input: { age: 59, name: "Andi" }) {
id
name
}
}
Check the Wiki for detailed information.