Entity Relationship

Tables in a database are often related to each other. With raw SQL queries you can retrieve these relationships with JOINs. You want the same kind of flexibility while using entities but without having to resort to writing raw queries. Since entities in Ozone are strongly typed, it only makes sense to have the relationships be strongly typed as well. Ozone and Ktorm makes it easy to retrieve an entity along with its entity relatives without making you worry about left and right joins.

One To One

The simplest of all the relations is one-to-one. For example, a User could have one Profile and one profile belongs to only one User. So a one-to-one relationship is best described by hasOne and belongsTo names. In our example, a User hasOne Profile and inversely, a Profile belongsTo one User.

While designing a database schema, only one of the tables would have a column that points to an id of the other "foreign" table. This key is called a foreign key and generally, this column is named foreignTable_id such as "user_id".

The table that has the foreign key defined in its table gets to be on the belongsTo side of the relationship. Think of it as a pet dog with a collar tag that says "I belong to this awesome owner with this phone number". This is true for not just one-to-one but also one-to-many relationships.

On the other side of the relationship, the table that does not have a foreign key pointing to the other table gets to be on the hasOne side of the relationship.

Understanding the differences between belongsTo and hasOne is very important as it is a common source of confusion, especially among beginners.

Let's now see an example of two entities with a one-to-one relationship. We'll start with plain entities without any relationship and then modify them to connect them in the next steps.


interface User : Entity<User> {
    val id: Long
    var name: String

    companion object : Entity.Factory<User>()
}

object Users : OzoneTable<User>("users") {
    val id by bigIncrements("id").bindTo { it.id }
    val name by string("name").bindTo { it.name }
}


interface Profile : Entity<Profile> {
    val id: Long
    var handle: String
    var userId: Long
    var isActive: Boolean

    companion object : Entity.Factory<Profile>()
}

object Profiles : OzoneTable<Profile>("profiles") {
    val id by bigIncrements("id").bindTo { it.id }
    val handle by string("handle").bindTo { it.handle }
    val isActive by boolean("s_active").bindTo { it.isActive }
    val userId by long("user_id").bindTo { it.userId }
}

belongsTo

belongsTo relationship can be mentioned by replacing bindTo with belongsTo() method in the table itself. In our example, one profile belongs to one user:


interface Profile : Entity<Profile> {
    val id: Long
    var handle: String
    var isActive: Boolean

    // We don't want just the id but the entity itself

    /** v̶a̶r̶ ̶u̶s̶e̶r̶I̶d̶:̶ ̶L̶o̶n̶g̶ **/
    var user: User

    companion object : Entity.Factory<Profile>()
}

object Profiles : OzoneTable<Profile>("profiles") {
    val id by bigIncrements("id").bindTo { it.id }
    val handle by string("handle").bindTo { it.handle }
    val isActive by boolean("s_active").bindTo { it.isActive }

    // Add belongsTo() at the end passing the name of the table and the
    // actual property of this entity you want to bind it to

    /** v̶a̶l̶ ̶u̶s̶e̶r̶I̶d̶ ̶b̶y̶ ̶l̶o̶n̶g̶(̶"̶u̶s̶e̶r̶_̶i̶d̶"̶)̶.̶b̶i̶n̶d̶T̶o̶ ̶{̶ ̶i̶t̶.̶u̶s̶e̶r̶I̶d̶ ̶}̶ **/
    val userId by long("user_id").belongsTo(Users) { it.user }
}

Now, whenever a profile instance is fetched from the database, a left join will be used to fetch its user as well.


val profile = Profiles.findById(5)
val user = profile?.user
assertNotNull(user)

hasOne

On the other side of belongsTo is hasOne. This table doesn't have a column referring to the other table. To bind to a "virtual" entity, it needs to define a property with a custom getter and use hasOne() method.


interface User : Entity<User> {
    val id: Long
    var name: String

    val profile: Profile?
        get() = hasOne(Profiles)

    companion object : Entity.Factory<User>()
}

You don't have to do anything in the table object but only in the entity interface. This makes sense if you think about it — there is nothing in the actual database table to describe this relationship so we have to define a property "on the fly".

/info/hasOne caches the related entity so even if you call this property multiple times, only one database trip is made.

hasOne Relationship Conventions

The secret sauce of Ozone's hasOne() relationship bindings is some educated guesses on how you have probably designed the schema. You can override this convention as well if you have defined your table differently.

  • hasOne(table: T, foreignKey: String?, localKey: String?, cacheKey: String?)

    • table: The table object to which the relative entity belongs to. This is required.

    • foreignKey: The foreign key of the relative table. This is by default derived as: entityClassName + "_id" where entityClassName is the lowercased name of this entity class. So, if a User hasOne Profile, this foreign key would be user_id in the Profiles table.

    • localKey: The name of the column that the foreignKey points to. By default, it is id. This is the "local" key of this entity not the entity that has defined the foreignKey i.e. not Profile entity's id but User entity's id.

    • cacheKey: The key to use for caching it locally so that you can refer to it without making another database trip. Consider changing this value only if the key clashes with something else.

Overriding hasOne Expression

If you want complete control over retrieving the related entity, you can use the alternative hasOneBy() method and pass a predicate.


interface User : Entity<User> {
    val id: Long
    var name: String
    val profile: Profile?
        get() = hasOneBy(Profiles) {
            it.userId eq id
        }

    companion object : Entity.Factory<User>()
}

One To Many

One-to-many is very similar to one-to-one relationship. Similar to one-to-one relationship, an entity would belong to another entity if it has a key in its table that points to the primary key of another table. Nothing is different here — the key is still called a foreign_key and since it has the key, this entity would declare it to be belonging to another entity, just like one-to-one.

Let's say we want to model a department where it can have many employees and each employee belongs to only one department.

The belongsTo side of relationship should look familiar:


interface Employee : Entity<Employee> {
    val id: Long
    var fullName: String
    var role: String
    var department: Department

    companion object : Entity.Factory<Employee>()
}

object Employees : OzoneTable<Employee>("employees") {
    val id by bigIncrements("id").bindTo { it.id }
    val fullName by string("full_name").bindTo { it.fullName }
    val role by string("role").bindTo { it.role }
    val departmentId by long("department_id").belongsTo(Departments) { it.department }
}

hasMany

On the other side of the relationship, since multiple entities can now belong to it, it would have to create a "virtual" property that would get a list of entities owned by it. Since it doesn't have anything in its table referring to the other table, nothing is there to be done in the entity's table. To convey this relationship, you can use hasMany() method passing the name of the related entity.


interface Department : Entity<Department> {
    val id: Long
    var name: String
    val employees
        get() = hasMany(Employees)

    companion object : Entity.Factory<Department>()
}

object Departments : OzoneTable<Department>("departments") {
    val id by bigIncrements("id").bindTo { it.id }
    val name by string("name").bindTo { it.name }
}

hasMany Relationship Conventions

hasMany works by making some educated guesses about the schema of your table. You can override these values if you want.

  • hasMany(table: T, foreignKey: String?, localKey: String?, cacheKey: String?)

    • table: The table object to which the relative entity belongs to. This is required.

    • foreignKey: The foreign key of the relative table. This is by default derived as: entityClassName + "_id" where entityClassName is the lowercased name of this entity class. So, if a Department hasMany Employees, this foreign key would be department_id in the Employees table.

    • localKey: The name of the column that the foreignKey points to. By default, it is id. This is the "local" key of this entity not the entity that has defined the foreignKey i.e. not Department entity's id but Employee entity's id.

    • cacheKey: The key to use for caching it locally so that you can refer to it without making another database trip. Consider changing this value only if the key clashes with something else.

Overriding hasMany Expression

If you want to define how to retrieve the related entities, you can use the alternative hasManyBy() method and pass a predicate.


interface Department : Entity<Department> {
    val id: Long
    var name: String
    val employees
        get() = hasManyBy(Employees) {
            it.departmentId eq id
        }

    companion object : Entity.Factory<Department>()
}

Customizing Relationship Constraints

When fetching a related entity, you can further customize the query expression by passing a lambda as the last parameter to one of hasOne() and hasMany() methods.

Let's say you only want to retrieve the profile if it is active.


interface User : Entity<User> {
    val id: Long
    var name: String

    val profile: Profile?
        get() = hasOne(Profiles) { it.isActive eq true }

    companion object : Entity.Factory<User>()
}

Similarly, you can further constraint the hasMany() query by passing a lambda predicate.

Let's say you only want to retrieve employees if they are also managers.


interface Department : Entity<Department> {
    val id: Long
    var name: String
    val employees
        get() = hasMany(Employees) {
            it.role eq "manager"
        }

    companion object : Entity.Factory<Department>()
}