Defining the data model

Models

Metadata

Please read Immutability requirement first!

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 as long as it is usable as Value. 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
)

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() = mapOf("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 register a MetadataExtractor when you open the database:

Registering a metadata extractor
val db = DB.open("path/to/db",
    MetadataExtractor.forClass<User> {
        Metadata(it.id, "lastName" to listOf(it.lastName, it.firstName))
    }
)

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 either the ID of this other document, or a Key (which is the ID of this other document serialized):

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() = mapOf("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.

IDs and indexes values

While models are stored in the database as serialized blobs, IDs and indexes are stored as Value.

A Value is a serializable entity where the serialized bytes define the ordering of IDs and indexes.

Types usable as Value

By default, the Kodein-DB understands the following types to be used as values: ByteArray, ReadBuffer, Boolean, Short, Int, Long, Char, CharSequence (such as String), Kodein-Memory UUID (not java.util.UUID, Key).

In order to keep the database efficient and indexes ordering semantic, Char and CharSequence are stored as UTF-8 characters (i.e. using a variable number of byte per character). You should be careful when using non-ascii characters as their ordering will not always be natural.
For example, the French language uses the accented letter 'é'. UTF ordering means that "céleri" will be sorted after "cuisse". You should therefore unaccent index values.

Adding new types as value

You can configure Kodein-DB to accept more types as values (and therefore use these types in IDs and indexes). You simply need to add a ValueConverter to the database when creating it:

a sample ValueConverter for KotlinX LocalDateTime:
val db = DB.default.open("path/to/db",
    ValueConverter.forClass<LocalDateTime> {
        Value.of(it.toInstant(TimeZone.UTC).epochSeconds)
    }
)
A ValueConverter just needs to know how to serialize a type to a Value. Values are never deserialized.

Multiple index values

IndexValues is a special type. If an index has a value of type IndexValues, then each of its containing value will be considered a separate value for this index. With it, you can define multiple value for the same index.

Multiple values are not the same as composite values! In fact, if an index has multiple value, each of them may be composite.

Defining the same value for multiple index has the following effects:

  • The model will appear for different search using the same index

  • The model may appear multiple times when performing a search that hits multiple values of the same index.

Let’s say we define a simple book model:

A simple book data class:
data class Book(
    val name: String,
    val author: String,
    val keywords: List<String>
)

Here keywords is a list of keyword a user might search for when searching for this book.

Let’s make this a model for our database:

A simple book model:
data class Book(
    override val id: UUID,
    val name: String,
    val author: String,
    val keywords: List<String>
) : Metadata {
    override fun indexes() = mapOf(
        "keywords" to IndexValues(keywords)
    )
}

Now you can search by index "lastName" with any keyword, and the book will appear in the Cursor if it contains the keyword.

When using multiple index, the model may appear multiple times when using find<Type>().byIndex(name) or find<Type>().byIndex(name, value, isOpen = true).
In the previous example, a cursor created with find<Book>().byIndex("keywords") will return the same book as many times as it has keywords.

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(Child("Jack", parents))

Using the preceding code, there will be two different collections, one Person, one Child, 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.