Exposed 0.57.0 Help

Entity definition

An Entity in Exposed maps a database table record to a Kotlin object. This ensures type safety and allows you to work with database records just like regular Kotlin objects, taking full advantage of Kotlin's language features.

When using the DAO approach, IdTable needs to be associated with an Entity, because every database record in this table needs to be mapped to an Entity instance, identified by its primary key.

Defining an Entity class

You define an entity instance by creating a class. In the following example, StarWarsFilmEntity is the entity class linked to the table StarWarsFilmsTable:

package org.example.entities import org.example.tables.StarWarsFilmsTable import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID class StarWarsFilmEntity(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<StarWarsFilmEntity>(StarWarsFilmsTable) var sequelId by StarWarsFilmsTable.sequelId var name by StarWarsFilmsTable.name var director by StarWarsFilmsTable.director }

Entity type

The entity type determines how the Entity class interacts with the table’s primary key. Since StarWarsFilmsTable is an IntIdTable, the StarWarsFilmsEntity class extends from IntEntity, which indicates that the id and primary key of StarWarsFilmsTable is of type Int.

The following entity types are supported:

IntEntity

Base class for an entity instance identified by an id comprised of a wrapped Int value.

LongEntity

Base class for an entity instance identified by an id comprised of a wrapped Long value.

UIntEntity

Base class for an entity instance identified by an id comprised of a wrapped UInt value.

ULongEntity

Base class for an entity instance identified by an id comprised of a wrapped ULong value.

UUIDEntity

Base class for an entity instance identified by an id comprised of a wrapped UUID value.

CompositeEntity

Base class for an entity instance identified by an id comprised of multiple wrapped values.

EntityClass

The EntityClass in the companion object block is responsible for managing Entity instances, such as creating, querying, and deleting records. It also maintains the relationship between the entity class (StarWarsFilmEntity in this example) and the database table (StarWarsFilmsTable).

Each entity type is supported by a corresponding EntityClass, which accepts the following parameters:

table

The IdTable containing rows mapped to entities managed by this class.

entityType

(Optional) The expected type of the Entity class. This can be omitted if it is the class of the type argument provided to this EntityClass instance.

entityCtor

(Optional) The function that instantiates an Entity using the provided EntityID. If not provided, reflection is used to determine the primary constructor on the first access (which may affect performance).

In the example above, IntEntityClass is specified in the companion object:

companion object : IntEntityClass<StarWarsFilmEntity>(StarWarsFilmsTable)

This setup enables you to use functions provided by IntEntityClass to manage instances of StarWarsFilmEntity effectively.

Properties

Each column in the table is represented as a property in the entity class, where the by keyword ensures the data is fetched or updated from the corresponding column when accessed.

var sequelId by StarWarsFilmsTable.sequelId var name by StarWarsFilmsTable.name var director by StarWarsFilmsTable.director

Once the entity class is defined, instances of this class allow you to manipulate individual records from the corresponding table. This could involve creating a new record, retrieving a row based on its primary key, updating values, or deleting records.

Immutable entities

For defining entities that are immutable, Exposed provides the additional ImmutableEntityClass and ImmutableCachedEntityClass.

The ImmutableCachedEntityClass uses an internal cache to store entity loading states by the associated database. This ensures that entity updates are synchronized with this class as the lock object.

To create an immutable entity, use either ImmutableEntityClass or ImmutableCachedEntityClass as a companion object in your entity class. For example, here’s how to define a CityEntity class:

package org.example.entities import org.example.tables.CitiesTable import org.jetbrains.exposed.dao.ImmutableEntityClass import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.id.EntityID class CityEntity(id: EntityID<Int>) : IntEntity(id) { val name by CitiesTable.name companion object : ImmutableEntityClass<Int, CityEntity>(CitiesTable) }
  • Properties are defined using val instead of var. This enforces immutability, as val properties cannot be reassigned after initial assignment.

  • The primary function of the entity class in this context is to query data from the associated table, not to modify it. Therefore, inserts can only be performed using the DSL insert() method and updates can be done either through the DSL update() or the ImmutableEntityClass.forceUpdateEntity() methods.

Class method overrides

You can use override methods in both the Entity class and the EntityClass companion object to extend functionality or manage entity behavior.

For example, here’s how to override the delete() method in a User entity:

package org.example.entities import org.example.tables.UsersTable import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID class UserEntityWithOverride(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<UserEntityWithOverride>(UsersTable) var name by UsersTable.name var city by CityEntity referencedOn UsersTable.cityId override fun delete() { println("Deleting user $name with ID: $id") super.delete() } }

In this example, a custom message is printed before the delete() function of the superclass (IntEntity) completes the deletion.

Field transformations

As databases typically store only basic types, such as integers and strings, it's not always convenient to keep the same simplicity on Data Access Object (DAO) level.

For example, you might need to parse JSON from a VARCHAR column, or retrieve values from a cache based on data from the database. In such cases, the preferred approach is to use column transformations.

Example: Defining an Unsigned Integer field

Suppose that you want to define an unsigned integer field on an entity, but Exposed doesn't have such column type yet. You can achieve this by using the following implementation:

package org.example.entities import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable object TableWithUnsignedInteger : IntIdTable() { val uint = integer("uint") } class EntityWithUInt(id: EntityID<Int>) : IntEntity(id) { var uint: UInt by TableWithUnsignedInteger.uint.transform({ it.toInt() }, { it.toUInt() }) companion object : IntEntityClass<EntityWithUInt>(TableWithUnsignedInteger) }

The transform function accept two lambdas that convert values to and from the original column type. In this case, you make sure to store only UInt instances in the uint field.

Although it is still possible to insert or update values with negative integers via DAO, this approach assures a cleaner business logic.

Memoized transformations

If your transformation function involves complex operations that impact performance, you can use memoizedTransform to cache the result of the transformation.

package org.example.entities import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.IntIdTable import java.util.Base64 object TableWithText : IntIdTable() { val text = varchar("text", length = 2048) } class EntityWithBase64(id: EntityID<Int>) : IntEntity(id) { var base64: String by TableWithText.text .memoizedTransform( wrap = { Base64.getEncoder().encodeToString(it.toByteArray()) }, unwrap = { Base64.getDecoder().decode(it).toString() } ) companion object : IntEntityClass<EntityWithBase64>(TableWithText) }

With memorized transformation, the value is unwrapped only once and then cached for future reads. This cache remains valid for the lifetime of the entity. If the transaction's entity cache is cleared or the entity is reloaded in a new transaction (creating a new cache without the existing value), the value will be wrapped again. However, if the original entity is kept alive outside of the transaction, the cached value persists to avoid re-wrapping.

Last modified: 05 December 2024