Skip to content

a complete dynamic object relational mapper that is able to define schemas during runtime while offering complete query possibilities similar to jpa including a GraphQL endpoint with almost identical scope

License

Notifications You must be signed in to change notification settings

coolsamson7/dorm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Java CI with Maven

dORM

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.

What's the problem anyway...

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.

Sample

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
}

Solution design

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 key
  • PROPERTY a table that will store single attributes of an entity
  • RELATIONS a bridge table expressing object relations

The property table defines the columns

  • TYPE the id of the entity structure
  • ENTITY the id of the corresponding entity
  • NAME 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 value
  • INT_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

Performance

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?

GraphQL

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
    }
}

Reference

Check the Wiki for detailed information.

About

a complete dynamic object relational mapper that is able to define schemas during runtime while offering complete query possibilities similar to jpa including a GraphQL endpoint with almost identical scope

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published