Persistent Properties

Persistent class can have simple properties and links to other persistent classes implemented by property delegates.

class XdUser(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdUser>()

    var login by xdRequiredStringProp(unique = true, trimmed = true) { login() }
    var visibleName by xdRequiredStringProp(trimmed = true)
    var banned by xdBooleanProp()
    var created by xdRequiredDateTimeProp()
    var lastAccessTime by xdDateTimeProp()
    val groups by xdLink0_N(XdGroup::users)
    val sshPublicKeys by xdChildren0_N(XdSshPublicKey::user)

    override val presentation: String
        get() = login
}

class XdSshPublicKey(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdSshPublicKey>()
    
    var user: XdBaseXdUser by xdParent(XdUser::sshPublicKeys)
    var data by xdBlobStringProp()
    var fingerPrint by xdRequiredStringProp(unique = true)
}

class XdGroup(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdGroup>()

    var name by xdRequiredStringProp(unique = true) { containsNone("<>/") }
    var parentGroup: XdGroup by xdParent(XdGroup::subGroups)
    val subGroups by xdChildren0_N(XdGroup::parentGroup)
    val users by xdLink0_N(XdUser::groups, onDelete = CLEAR, onTargetDelete = CLEAR)

    override val presentation: String
        get() = name
}

Simple properties

Methods that create delegates for simple properties accept optional parameters dbName and constraints. By default Xodus-DNQ uses Kotlin-property name to name the property in Xodus database. Parameter dbName helps to override this.

Parameter constraints is a closure that has PropertyConstraintsBuilder as a receiver. Using this parameter you can set up property constraints that will be checked before transaction flush. Xodus-DNQ defines several useful constraints for String and Number types, but it is simple to defined your own constraints.

Methods to create delegates for required simple properties have parameter unique: Boolean. By default its value is false. If its value is true, Xodus-DNQ will check on flush uniqueness of property value among instances of the persistent class.

Byte

xdByteProp — optional byte property
// Optional non-negative Byte property with database name `age`.
var age by xdByteProp { min(0) }
xdRequiredByteProp — required byte property
// Unique required Byte property with database name `id`. 
var id by xdRequiredByteProp(unique = true) 
xdNullableByteProp — nullable byte property
// Non-negative nullable Byte property with database name `salary`.
var salary by xdNullableByteProp { min(0) }  

Short

xdShortProp — optional short property
// Optional non-negative Short property with database name `age`.
var age by xdShortProp { min(0) }
xdRequiredShortProp — required short property
// Unique required Short property with database name `id`. 
var id by xdRequiredShortProp(unique = true) 
xdNullableShortProp — nullable short property
// Non-negative nullable Short property with database name `salary`.
var salary by xdNullableShortProp { min(0) }  

Int

xdIntProp — optional integer property
// Optional non-negative Int property with database name `age`.
var age by xdIntProp { min(0) }

// Optional Int property with database name `grade`.
var rank by xdIntProp(dbName = "grade")
xdRequiredIntProp — required integer property
// Required non-negative Int property with database name `age`.
var age by xdRequiredIntProp { min(0) }  

// Required Int property with database name `grade`.
var rank by xdRequiredIntProp(dbName = "grade")

// Unique required Int property with database name `id`. 
var id by xdRequiredIntProp(unique = true) 
xdNullableIntProp — nullable integer property
// Non-negative nullable Int property with database name `salary`.
var salary by xdNullableIntProp { min(0) }  

Long

xdLongProp — optional long property
// Optional non-negative Long property with database name `salary`.
var salary by xdLongProp() { min(0) }  
xdRequiredLongProp — required long property
// Unique required Long property with database name `id`.
var id by xdRequiredLongProp(unique = true) 
xdNullableLongProp — nullable long property
// Non-negative nullable Long property with database name `salary`.
var salary by xdNullableLongProp { min(0) }  

Float

xdFloatProp — optional float property
// Optional non-negative Float property with database name `salary`.
var salary by xdFloatProp() { min(0) }  
xdRequiredFloatProp — required float property
// Unique required Float property with database name `seed`.
var seed by xdRequiredFloatProp(unique = true) 
xdNullableFloatProp — nullable float property
// Non-negative nullable Float property with database name `salary`.
var salary by xdNullableFloatProp { min(0) }  

Double

xdDoubleProp — optional double property
// Optional non-negative Double property with database name `salary`.
var salary by xdDoubleProp() { min(0) }  
xdRequiredDoubleProp — required double property
// Unique required Double property with database name `seed`.
var seed by xdRequiredDoubleProp(unique = true) 
xdNullableDoubleProp — nullable double property
// Non-negative nullable Double property with database name `salary`.
var salary by xdNullableDoubleProp { min(0) }  

Boolean

xdBooleanProp — boolean property
// Optional Boolean property with database name `anonymous`.
var isGuest by xdBooleanProp(dbName = "anonymous") 
xdNullableBooleanProp — nullable boolean property
// Nullable Boolean property with database name `isFemale`.
var isFemale by xdNullableBooleanProp() 

String

xdStringProp — optional string property
// Optional nullable String property with database name `lastName`.
var lastName by xdStringProp(trimmed = true)
xdRequiredStringProp — required string property
// Required unique String property with database name `uuid`.
var uuid by xdRequiredStringProp(unique=true)

Joda DateTime

xdDateTimeProp — optional Joda DateTime property
// Optional nullable DateTime property with database name `createdAt`.
var createdAt by xdDateTimeProp()
xdRequiredDateTimeProp — required Joda DateTime property
// Required not-null DateTime property with database name `createdAt`.
var createdAt by xdRequiredDateTimeProp()

Blob

xdBlobProp — optional blob property
// Optional nullable InputStream property with database name `image`.
var image by xdBlobProp()
xdRequiredBlobProp — required blob property
// Required not-null InputStream property with database name `image`.
var image by xdRequiredBlobProp()

String Blob

xdBlobStringProp — optional string blob property
// Optional nullable String property with database name `description`.
var description by xdBlobStringProp()
xdRequiredBlobStringProp — required string blob property
// Required not-null String property with database name `description`.
var description by xdRequiredBlobStringProp()

String List

xdSetProp — set of comparables property
// Set of strings property with database name `tags`.
var tags by xdSetProp<XdPost, String>()

Simple property constraints

Property constraints are checked on transaction flush. Xodus-DNQ throws ConstraintsValidationException if some of them fail. Method getCauses() of ConstraintsValidationException returns all actual DataIntegrityViolationExceptions corresponding to data validation errors that happen during the transaction flush.

try {
    store.transactional {
        // Do some database update           
    }
} catch(e: ConstraintsValidationException) {
    e.causes.forEach {
        e.printStackTrace()
    }
}

Regex

Checks that string property value matches regular expression.

var javaIdentifier by xdStringProp {
    regex(Regex("[A-Za-z][A-Za-z0-9_]*"), "is not a valid Java identifier")
}

Email

Checks that string property value is a valid email. Optionally accepts custom regular expression to verify email.

var email by xdStringProp { email() }

None of characters

Checks that string property value contains none of the specified characters.

var noDash by xdStringProp { containsNone("-") }

Letters only

Checks that string property value contains only letter characters.

var alpha by xdStringProp { alpha() }

Digits only

Checks that string property value contains only digit characters.

var number by xdStringProp { numeric() }

Digits and letters only

Checks that string property value contains only digit and letter characters.

var base64 by xdStringProp { alphaNumeric() }

URI

Checks that string property value is a valid URI.

var uri by xdStringProp { uri() }

URL

Checks that string property value is a valid URL.

var url by xdStringProp { url() }

String length

Checks that length of string property value falls into defined range.

var badPassword by xdStringProp { length(min = 5, max = 10) }

Require if

Checks that property value is defined if provided closure returns true. Triggers only if the property is changed.

var main by xdStringProp()
var dependent by xdLongProp { requireIf { main != null } }

Min value

Checks that number property value is more or equals than given value.

var timeout by xdIntProp { min(1000) }

Max value

Checks that number property value is less or equals than given value.

var timeout by xdIntProp { max(10_000) }

Is after

Checks that DateTime property value is after given value.

var afterDomini by xdDateTimeProp { isAfter({ domini }) }

Is before

Checks that DateTime property value is before given value.

var beforeChrist by xdDateTimeProp { isBefore({ domini }) }

Future

Checks that DateTime property value is a moment in the future.

var future by xdDateTimeProp { future() }

Past

Checks that DateTime property value is a moment in the past.

var past by xdDateTimeProp { past() }

Custom Property Constraints

You can define your own property constrains in the same way built-in constraints are defined. You need to defined an extension method for PropertyConstraintBuilder that builds and adds your constraint.

fun PropertyConstraintBuilder<*, String?>.cron(
        message: String = "is not a valid cron expression"
) {
    constraints.add(object : PropertyConstraint<String?>() {

        /**
         * Is called on flush to check if new value of property matches constraint. 
         */
        override fun isValid(value: String?): Boolean {
            // It's better to ignore empty values in your custom checks. 
            // Otherwise check for required property may fail twice 
            // as undefined value and as invalid cron expression. 
            return value == null || value.isEmpty() || try {
                CronExpression(value)
                true
            } catch (e: Exception) {
                false
            }
        }

        /**
         * Is called on check failure to build an exeption error message  
         */
        override fun getExceptionMessage(propertyName: String, propertyValue: String?): String {
            val errorMessage = try {
                CronExpression(propertyValue)
                ""
            } catch (e: Exception) {
                e.message
            }

            return "$propertyName should be valid cron expression but was $propertyValue: $errorMessage"
        }

        override fun getDisplayMessage(propertyName: String, propertyValue: String?) = message
    })
}

There are three types of links: directed links, bi-directed links and aggregation links.

Most of the methods that create delegates for links accept optional parameter dbPropertyName. By default Xodus-DNQ uses Kotlin-property name to reference the link in Xodus database. Parameter dbPropertyName helps to override this.

On delete policy

Most of the methods that create delegates for links accept optional parameters onDelete and onTargetDelete. This parameters defines what Xodus-DNQ should do with the link on this entity delete or on the link target delete. Available options are

FAIL Fail transaction if entity is deleted but link still points to it.
CLEAR Clear link to deleted entity.
CASCADE If entity is delete and link still exists, then delete entity on the opposite link end as well.
FAIL_PER_TYPE Fail transaction with a custom message if entity is deleted but link still points to it. One message per entity type.
FAIL_PER_ENTITY Fail transaction with a custom message if entity is deleted but link still points to it. One message per entity.

Unidirectional assosiations

xdLink0_1<XdSource, XdTarget> –– unidirectional [0..1] association
var directedOptionalLink by xdLink0_1(XdTarget, onTargetDelete = OnDeletePolicy.CLEAR)
xdLink1<XdSource, XdTarget> — unidirectional [1] association
var directedRequiredLink by xdLink1(XdTarget)
xdLink0_N<XdSource, XdTarget> — unidirectional [0..N] association
val users by xdLink0_N(XdUser)
xdLink1_N<XdSource, XdTarget> — unidirectional [1..N] association
val users by xdLink1_N(XdUser)

Bidirectional associations

For bidirectional associations Xodus-DNQ maintains both ends of the links. For example, if there is a bidirectional link between XdUser::groups and XdGroup::users, and you add some group to user.groups.add(group) Xodus-DNQ will automatically add user to group.users.

xdLink0_1<XdSource, XdTarget> — bidirectional [0..1] association
val group by xdLink0_1(XdGroup::users)
xdLink1 — bidirectional [1] association
val group by xdLink1(XdGroup::users)
xdLink0_N<XdSource, XdTarget> — bidirectional [0..N] association
val groups by xdLink0_N(XdGroup::users)
xdLink1_N<XdSource, XdTarget> — bidirectional [1..N] association
val groups by xdLink1_N(XdGroup::users)

Aggregations

Aggregations or parent-child association are auxiliary type of links with some predefined behavior.

  1. If persistent class of an entity has at least one parent link defined, it is considered as a child entity and it should have exactly one parent on flush.
  2. On parent delete all its children are deleted as well.
xdChild0_1<XdParent, XdChild> — parent end [0..1] of aggregation association
val profile by xdChild0_1(XdUser::profile)
xdChild1<XdParent, XdChild> — parent end [1] of aggregation association
val profile by xdChild1(XdUser::profile)
xdChildren0_N<XdParent, XdChild> — parent end [0..N] of aggregation association
val subGroups by xdChildren0_N(XdGroup::parentGroup)
xdChildren1_N<XdParent, XdChild> — parent end [1..N] of aggregation association
val contacts by xdChildren1_N(XdContact::user)
val user by xdParent(XdUser::contacts)
val parentGroup by xdMultiParent(XdGroup::subGroups)
val parentOfRootGroup by xdMultiParent(XdRoot::rootGroup)

Extension properties

Bidirectional link can also point to Kotlin extension property on one side.

class XdUser(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdUser>()
    var login by xdRequiredStringProp(unique = true)
}

class SecretInfo(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<Secret>()
    var info by xdBlobStringProp()
    var user: XdUser by xdLink1(XdUser::secret)
}

// this association will be authomatically registered in XdModel together with SecretInfo type
var XdUser.secret by xdLink1(SecretInfo::user)

Other properties

Kotlin extension properties can be used for simple properties and links. If extension properties use database constraints then they should be registered in XdModel with plugins.

class XdUser(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdUser>()
    var login by xdRequiredStringProp(unique = true)
}

var XdUser.name by xdRequiredStringProp()
var XdUser.boss by xdLink1(XdSuperUser)

XdModel.withPlugins(
        SimpleModelPlugin(listOf(XdUser::name, XdUser::boss))
)

isDefined and getSafe

Required properties and links of cardinality 1 have non-null types in Kotlin. But for new entities that were not committed yet, the properties can be not defined yet. To check if a property has a value one can use method isDefined. To get a value of such a property safely there is a method getSafe.

class XdUser(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdUser>()
    var login by xdRequiredStringProp(unique = true)
}

fun `isDefined returns false for undefined properties`() {
    store.transactional {
        val user = XdUser.new()
        assertEquals(false, user.isDefined(XdUser::login))
    }
}

fun `isDefined returns true for defined properties`() {
    store.transactional {
        val user = XdUser.new { login = "zeckson" }
        assertEquals(true, user.isDefined(XdUser::login))
    }
}

fun `getSafe returns null for undefined properties`() {
    store.transactional {
        val user = XdUser.new()
        assertEquals(null, user.getSafe(XdUser::login))
    }
}

fun `getSafe returns property value for defined properties`() {
    store.transactional {
        val user = XdUser.new { login = "zeckson" }
        assertEquals("zeckson", user.getSafe(XdUser::login))
    }
}

Unique Indices

Xodus-DNQ can check uniqueness constraints for composite keys. To enable it composite key index should be defined.

class XdAPI(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdAPI>() {
        override val compositeIndices = listOf(
                listOf(XdAPI::service, XdAPI::key)
        )
    }

    var key by xdRequiredStringProp()
    var service by xdLink1(XdService)    
}

It is also possible to define an index with a single property to make it unique. Technically two following code blocks do the same thing.

class XdAPI(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdAPI>()

    var key by xdRequiredStringProp(unique=true)
    var name by xdRequiredStringProp(unique=true)
}
class XdAPI(entity: Entity) : XdEntity(entity) {
    companion object : XdNaturalEntityType<XdAPI>() {
        override val compositeIndices = listOf(
                listOf(XdAPI::key),
                listOf(XdAPI::name)
        )
    }

    var key by xdRequiredStringProp()
    var name by xdRequiredStringProp()
}

Explicit indices for a single property can be useful when you want to make some property of a parent class to be unique for instances of a child class.