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
DataIntegrityViolationException
s 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.