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
}
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.0.// Optional non-negative Byte property with database name `age`.
var age by xdByteProp { min(0) }
Byte.0.unique is true then Xodus-DNQ will check on flush uniqueness
of the property value among instances of the persistent class.// Unique required Byte property with database name `id`.
var id by xdRequiredByteProp(unique = true)
Byte?.null.// Non-negative nullable Byte property with database name `salary`.
var salary by xdNullableByteProp { min(0) }
Short.0.// Optional non-negative Short property with database name `age`.
var age by xdShortProp { min(0) }
Short.0.unique is true then Xodus-DNQ will check on flush uniqueness
of the property value among instances of the persistent class.// Unique required Short property with database name `id`.
var id by xdRequiredShortProp(unique = true)
Short?.null.// Non-negative nullable Short property with database name `salary`.
var salary by xdNullableShortProp { min(0) }
Int.0.// 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")
Int.0.unique is true then Xodus-DNQ will check on flush uniqueness
of the property value among instances of the persistent class.// 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)
Int?.null.// Non-negative nullable Int property with database name `salary`.
var salary by xdNullableIntProp { min(0) }
Long.0L.// Optional non-negative Long property with database name `salary`.
var salary by xdLongProp() { min(0) }
Long.0L.unique is true then Xodus-DNQ will check on flush uniqueness
of the property value among instances of the persistent class.// Unique required Long property with database name `id`.
var id by xdRequiredLongProp(unique = true)
Long?.null.// Non-negative nullable Long property with database name `salary`.
var salary by xdNullableLongProp { min(0) }
Float.0F.// Optional non-negative Float property with database name `salary`.
var salary by xdFloatProp() { min(0) }
Float.0F.unique is true then Xodus-DNQ will check on flush uniqueness
of the property value among instances of the persistent class.// Unique required Float property with database name `seed`.
var seed by xdRequiredFloatProp(unique = true)
Float?.null.// Non-negative nullable Float property with database name `salary`.
var salary by xdNullableFloatProp { min(0) }
Double.0.0.// Optional non-negative Double property with database name `salary`.
var salary by xdDoubleProp() { min(0) }
Double.0.0.unique is true then Xodus-DNQ will check on flush uniqueness
of the property value among instances of the persistent class.// Unique required Double property with database name `seed`.
var seed by xdRequiredDoubleProp(unique = true)
Double?.null.// Non-negative nullable Double property with database name `salary`.
var salary by xdNullableDoubleProp { min(0) }
Boolean.false.// Optional Boolean property with database name `anonymous`.
var isGuest by xdBooleanProp(dbName = "anonymous")
Boolean?.null.// Nullable Boolean property with database name `isFemale`.
var isFemale by xdNullableBooleanProp()
String?.null.trimmed: Boolean enables string trimming on value set, i.e. when
you assign a value to such property all leading and trailing spaces are removed.// Optional nullable String property with database name `lastName`.
var lastName by xdStringProp(trimmed = true)
String.RequiredPropertyUndefinedException on get.null. So empty string does not pass require check.trimmed: Boolean enables string trimming on value set, i.e. when
you assign a value to such property all leading and trailing spaces are removed.unique is true then Xodus-DNQ will check on flush uniqueness
of the property value among instances of the persistent class.// Required unique String property with database name `uuid`.
var uuid by xdRequiredStringProp(unique=true)
org.joda.time.DateTime?.null.// Optional nullable DateTime property with database name `createdAt`.
var createdAt by xdDateTimeProp()
org.joda.time.DateTime.RequiredPropertyUndefinedException on get.unique is true then Xodus-DNQ will check on flush uniqueness
of the property value among instances of the persistent class.// Required not-null DateTime property with database name `createdAt`.
var createdAt by xdRequiredDateTimeProp()
InputStream?.null.XdQuery by this property.// Optional nullable InputStream property with database name `image`.
var image by xdBlobProp()
InputStream.RequiredPropertyUndefinedException on get.XdQuery by this property.// Required not-null InputStream property with database name `image`.
var image by xdRequiredBlobProp()
String?.null.XdQuery by this property.// Optional nullable String property with database name `description`.
var description by xdBlobStringProp()
String.RequiredPropertyUndefinedException on get.XdQuery by this property.// Required not-null String property with database name `description`.
var description by xdRequiredBlobStringProp()
Set.emptySet().// Set of strings property with database name `tags`.
var tags by xdSetProp<XdPost, String>()
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()
}
}
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")
}
Checks that string property value is a valid email. Optionally accepts custom regular expression to verify email.
var email by xdStringProp { email() }
Checks that string property value contains none of the specified characters.
var noDash by xdStringProp { containsNone("-") }
Checks that string property value contains only letter characters.
var alpha by xdStringProp { alpha() }
Checks that string property value contains only digit characters.
var number by xdStringProp { numeric() }
Checks that string property value contains only digit and letter characters.
var base64 by xdStringProp { alphaNumeric() }
Checks that string property value is a valid URI.
var uri by xdStringProp { uri() }
Checks that string property value is a valid URL.
var url by xdStringProp { url() }
Checks that length of string property value falls into defined range.
var badPassword by xdStringProp { length(min = 5, max = 10) }
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 } }
Checks that number property value is more or equals than given value.
var timeout by xdIntProp { min(1000) }
Checks that number property value is less or equals than given value.
var timeout by xdIntProp { max(10_000) }
Checks that DateTime property value is after given value.
var afterDomini by xdDateTimeProp { isAfter({ domini }) }
Checks that DateTime property value is before given value.
var beforeChrist by xdDateTimeProp { isBefore({ domini }) }
Checks that DateTime property value is a moment in the future.
var future by xdDateTimeProp { future() }
Checks that DateTime property value is a moment in the past.
var past by xdDateTimeProp { past() }
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.
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. |
XdTarget?.null.onDelete defines what should happen to the entity on the opposite end when this entity is deleted.
CLEAR (default) — nothing.CASCADE — entity on the opposite end is deleted as well.onTargetDelete defines what should happen to this entity when the entity on the opposite end is deleted.
FAIL (default) — transaction fails, i.e. link should be cleared before target entity delete.CLEAR — link is cleared.CASCADE — this entity is deleted as well.var directedOptionalLink by xdLink0_1(XdTarget, onTargetDelete = OnDeletePolicy.CLEAR)
XdTarget.RequiredPropertyUndefinedException on get.onDelete defines what should happen to the entity on the opposite end when this entity is deleted.
CLEAR (default) — nothing.CASCADE — entity on the opposite end is deleted as well.onTargetDelete defines what should happen to this entity when the entity on the opposite end is deleted.
FAIL (default) — transaction fails, i.e. link should be cleared before target entity delete.CASCADE — this entity is deleted as well.var directedRequiredLink by xdLink1(XdTarget)
XdMutableQuery<XdTarget>.XdTarget.emptyQuery().onDelete defines what should happen to the entities on the opposite end when this entity is deleted.
CLEAR (default) — nothing.CASCADE — entity on the opposite end is deleted as well.onTargetDelete defines what should happen to this entity when one of the entities on the opposite end is deleted.
FAIL (default) — transaction fails, i.e. association with the deleted entity should be removed first.CLEAR — association with the deleted entity is removed.CASCADE — this entity is deleted as well.val users by xdLink0_N(XdUser)
XdMutableQuery<XdTarget>.XdTarget.emptyQuery().onDelete defines what should happen to the entities on the opposite end when this entity is deleted.
CLEAR (default) — nothing.CASCADE — entity on the opposite end is deleted as well.onTargetDelete defines what should happen to this entity when one of the entities on the opposite end is deleted.
FAIL (default) — transaction fails, i.e. association with the deleted entity should be removed first.CLEAR — association with the deleted entity is removed.CASCADE — this entity is deleted as well.val users by xdLink1_N(XdUser)
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.
XdTarget?.null.onDelete defines what should happen to the entity on the opposite end when this entity is deleted.
FAIL (default) — transaction fails, i.e. link should be cleared before this entity delete.CLEAR — link is cleared.CASCADE — entity on the opposite end is deleted as well.onTargetDelete defines what should happen to this entity when the entity on the opposite end is deleted.
FAIL (default) — transaction fails, i.e. link should be cleared before target entity delete.CLEAR — link is cleared.CASCADE — this entity is deleted as well.val group by xdLink0_1(XdGroup::users)
XdTarget.RequiredPropertyUndefinedException on get.onDelete defines what should happen to the entity on the opposite end when this entity is deleted.
FAIL (default) — transaction fails, i.e. link should be cleared before this entity delete.CLEAR — link is cleared.CASCADE — entity on the opposite end is deleted as well.onTargetDelete defines what should happen to this entity when the entity on the opposite end is deleted.
FAIL (default) — transaction fails, i.e. link should be cleared before target entity delete.CASCADE — this entity is deleted as well.val group by xdLink1(XdGroup::users)
XdMutableQuery<XdTarget>.XdTarget.emptyQuery().onDelete defines what should happen to the entities on the opposite end when this entity is deleted.
FAIL (default) — transaction fails, i.e. association should be deleted before this entity delete.CLEAR — association is cleared.CASCADE — entities on the opposite end are deleted as well.onTargetDelete defines what should happen to this entity when one of the entities on the opposite end is deleted.
FAIL (default) — transaction fails, i.e. association with the deleted entity should be removed first.CLEAR — association with the deleted entity is removed.CASCADE — this entity is deleted as well.val groups by xdLink0_N(XdGroup::users)
XdMutableQuery<XdTarget>.XdTarget.emptyQuery().onDelete defines what should happen to the entities on the opposite end when this entity is deleted.
FAIL (default) — transaction fails, i.e. association should be deleted before this entity delete.CLEAR — association is cleared.CASCADE — entities on the opposite end are deleted as well.onTargetDelete defines what should happen to this entity when one of the entities on the opposite end is deleted.
FAIL (default) — transaction fails, i.e. association with the deleted entity should be removed first.CLEAR — association with the deleted entity is removed.CASCADE — this entity is deleted as well.val groups by xdLink1_N(XdGroup::users)
Aggregations or parent-child association are auxiliary type of links with some predefined behavior.
XdChild?.null.val profile by xdChild0_1(XdUser::profile)
XdChild.RequiredPropertyUndefinedException on get.val profile by xdChild1(XdUser::profile)
XdMutableQuery<XdChild>.XdTarget.emptyQuery().val subGroups by xdChildren0_N(XdGroup::parentGroup)
XdMutableQuery<XdChild>.XdTarget.emptyQuery().val contacts by xdChildren1_N(XdContact::user)
XdParent.RequiredPropertyUndefinedException on get.val user by xdParent(XdUser::contacts)
XdTarget?.null.val parentGroup by xdMultiParent(XdGroup::subGroups)
val parentOfRootGroup by xdMultiParent(XdRoot::rootGroup)
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)
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))
)
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))
}
}
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.