Defining the data model

Models

Metadata

xref:immutability.adoc

ID and Indexes

A model may have one or more named indexes, to allow you to search and/or order by a specific value.

A model must have one unique ID, which can be of any type. This ID defines the default ordering of the models inside the collections. In essence, the ID works exactly like an index, except that it is unnamed. You can use UUID.randomUUID() if your model does not have a unique value.

Indexes and IDs can be composite, which means that they can contain multiple values. A composite index allows you to:

  • Get models ordered by first value, then second, then third, then…​

  • Look for all models with the first value, then second, then third, then…​

With annotations

When targeting only the JVM, you can simply use annotations:

A simple model
data class User(
    @Id val uid: String,
    val firstName: String,
    @Index("lastName") val lastName: String
)
When using @Id or @Index, Kodein-DB converts String values to byte array using the ASCII charset. Therefore, only ASCII characters are allowed.

Using this configuration, when getting all users by index "lastName", they will be ordered first by lastName, then by uid. If you want the results to be ordered by lastName then firstName (then uid), you can use a composite index:

Same model with composite index
data class User(
    @Id val uid: String,
    val firstName: String,
    val lastName: String
) {
    @Index("name") fun nameIndex() = listOf(lastName, firstName)
}

With the model

The model itself can define its metadata by implementing either the Metadata or HasMetadata interface:

Model is metadata
data class User(
    override val id: String, (1)
    val firstName: String,
    val lastName: String
) : Metadata {
    override fun indexes() = indexSet("lastName" to listOf(lastName, firstName)) (2)
}
1 The id property override is mandatory
2 The indexes function override is optional (no index by default)
Model has metadata
data class User(
    val id: String,
    val firstName: String,
    val lastName: String
) : HasMetadata {
    override fun getMetadata(db: ModelDB, vararg options: Options.Write) =
            Metadata(id, "lastName" to listOf(lastName, firstName))
}

With an extractor

If you don’t own the models, or if you don’t want to mark them for Kodein-DB, you can use register a MetadataExtractor when you open the database:

Registering a metadata extractor
val db = DB.open("path/to/db",
    MetadataExtractor {
        when (it) {
            is User -> Metadata(it.id, "lastName" to listOf(it.lastName, it.firstName))
            else -> error("Unknown model $it")
        }
    }
)

Using ID as an index

If we consider the User model we have just defined, we have defined the ID to be a UUID, meaning that the order in which they will be stored and retrieved is completely random.
Because the ID must be unique, we cannot use the name to be the ID. However, we can create a composite ID. Consider this updated model:

Model with a composite ID
data class User(
    val uid: String,
    val firstName: String,
    val lastName: String
) : Metadata {
    override val id get() = listOf(lastName, firstName, uid)
}

Because uid is unique, the tuple (lastName, firstName, uid) is unique (if only because it contains uid). Therefore, the id property is always unique, but the order in which the models will be stored are defined first by lastName, then by firstName, then only by id.

While using a composite ID can be very useful, it makes the creation of key from ID values more complex.

Key & References

If a model contains another model, it will be serialized into the same document. If you need to reference another document, then you need to store a Key:

A model with a reference to another model
data class User(
    override val id: String,
    val name: Name, (1)
    val address: Key<Address> (2)
) : Metadata {
    override fun indexes() = indexSet("lastName" to listOf(name.last, name.first))
}
1 Will be included as part of this model’s document.
2 References another model with its own document.

Polymorphism

The problem

By default, Kodein-DB inserts each model in the document collection that corresponds to its real type.

Considering the following insertions:

Multiple insertions
open class Person(@Id val name: String)
class Child(name: String, val parents: List<Key<Person>>): Person(name)

val janeKey = db.put(Person("Jane"))
val johnKey = db.put(Person("John"))

val parents = listOf(janeKey, johnKey)
db.put(Child("Jill", parents))
db.put(Person("Jack", parents))

Using the preceding code, there will be two different collections, one Person, one Adult, meaning if you were to look for all Person models, you would only get Jane & John.

Children are person too (even when they keep asking you when’s the end of this documentation…​) so, you probably want to put every Child model into the Person collection. To do that, you need to enable polymorphism: the fact that a collection can hold multiple types of models.

JVM only annotation

The simpler way to define a polymorphic document is to use the @Polymorphic annotation. However, as usual for annotations, it only works for the JVM.

Children are Persons
@Polymorphic(Person::class) (1)
class Child(name: String, val parents: List<Key<Person>>): Person(name)
1 This @Polymorphic annotation instructs Kodein-DB to put Child models into the Person collection.

Type Table

In Kodein-DB, the Type Table is responsible for defining which model type belongs to which collection.

Using a Type Table is compatible with multiplatform!

You can define a TypeTable when opening the database:

Defining a Type Table
val db = DB.open("path/to/db",
    TypeTable {
        root<Person>() (1)
            .sub<Child>() (2)
    }
)
1 Defines the root collection Person.
2 Defines that all Child models will be put in the Person collection.