Being reactive
Listeners and subscriptions
Kodein-DB supports the reactive pattern. You can use Kodein-DB as a data event hub, so that you can react to the addition or suppression of documents.
A listener is responsible for reacting to an operation.
Once you have registered it, you can get a subscription Closeable
, which will stop the listener from being called if you close
it.
Using the DSL
You can easily register a listener using the ad-hoc DSL:
db.on<User>().register { (1)
}
db.onAll().register { (2)
}
1 | Registers a listener on the User collection. |
2 | Registers a global listener to the entire database. |
A DSL listener can access its own subscription (this can be useful if you want the listener to cancel its own subscription after reacting to a certain event) in the context of the callbacks:
db.on<User>().register {
didPut { user ->
if (whatever) this.subscription.close()
}
}
Using the DBListener
interface
You can have one of your classes implement the DBListener
interface and then register it:
class UserListener : DBListener<User> {}
class GlobalListener : DBListener<Any> {}
val uSub = db.on<User>().register(UserListener()) (1)
val aSub = db.onAll().register(GlobalListener()) (2)
1 | Registers a listener on the User collection. |
2 | Registers a global listener to the entire database. |
A class listener receives its own subscription (this can be useful if you want the listener to cancel its own subscription after reacting to a certain event) just after registration:
class UserListener : DBListener<User> {
lateinit var subscription: Closeable
override fun setSubscription(subscription: Closeable) {
this.subscription = subscription
}
}
Before an operation
simple check
You can use the event system to act before an operation.
Any exception thrown in a will* callback cancels the operation (or batch of operation) and prevents subsequent callbacks to be called.
|
Reacting before an operation can be useful to ensure that the operation satisfies certain prerequisites, or to throw an exception to interrupt the operation if it isn’t.
db.on<User>().register {
willPut { user ->
check(user.name.isNotBlank()) { "User firstName and lastName must not be blank" }
}
willDelete {
val pictureCount = db.find<Picture>().byIndex("userKey", key).entries().count()
check(pictureCount == 0) { "User has pictures, delete them first" }
}
}
class UserListener : DBListener<User> {
override fun willPut(model: User, typeName: ReadMemory, metadata: Metadata, options: Array<out Options.Write>) {
check(model.name.isNotBlank()) { "User firstName and lastName must not be blank" }
}
override fun willDelete(key: Key<User>, getModel: () -> User?, typeName: ReadMemory, options: Array<out Options.Write>) {
val pictureCount = db.find<Picture>().byIndex("userKey", key).entries().count()
check(pictureCount == 0) { "User has pictures, delete them first" }
}
}
Accessing the deleted model
You may have noticed in the preceding example that the willDelete
callback do not access the deleted model.
That’s because it is not given to the willDelete
DSL callback.
Because the deletion of a document uses its key, and not its model, you need to instruct the system to get the document before deleting it.
-
Using the DSL, use the
willDeleteIt
method:DSL delete check with modeldb.on<User>().register { willDeleteIt { user -> val pictureCount = db.find<Picture>().byIndex("userId", user.id).entries().count() check(pictureCount == 0) { "User has pictures, delete them first" } } }
-
Using the class method, call the
getModel
function argument:DSL delete check with modelclass UserListener : DBListener<User> { override fun willDelete(key: Key<User>, getModel: () -> User?, typeName: ReadMemory, options: Array<out Options.Write>) { val user = getModel() val pictureCount = db.find<Picture>().byIndex("userId", user.id).entries().count() check(pictureCount == 0) { "User has pictures, delete them first" } } }
After an operation
simple reaction
You can react after an operation, this can be useful:
-
Locally if you want to keep or a local state (such as a UI) up to date:
-
Globally if you want to keep a global state (such as the database itself) up to date.
Any exception thrown from a did* callback will not prevent other listeners to be called.
Kodein-DB ensures that all did* listeners are called when an operation has suceeded.
|
db.on<User>().register {
didPut { user -> ui.add(user) }
didDelete { ui.reload() }
}
class UserListener : DBListener<User> {
override fun didPut(model: User, key: Key<User>, typeName: ReadMemory, metadata: Metadata, size: Int, options: Array<out Options.Write>) {
ui.add(model)
}
override fun didDelete(key: Key<User>, model: User?, typeName: ReadMemory, options: Array<out Options.Write>) {
ui.reload()
}
}
Note that all arguments of the listener’s methods are available in the DSL in the this
context.
You can use DSL delete reaction with model
|
Accessing the deleted model
You may have noticed in the preceding example that the didDelete
callback do not access the deleted model.
That’s because it is not given to the didDelete
DSL callback, and will probably be null in the didDelete
class method.
Because the deletion of a document uses its key, and not its model, you need to instruct the system to get the document before deleting it.
-
Using the DSL, simply use the
didDeleteIt
method:DSL delete reaction with modeldb.on<User>().register { didDeleteIt { user -> ui.remove(user) } }
-
Using the class method, call the
getModel
function argument inwillDelete
:DSL delete reaction with modelclass UserListener : DBListener<User> { override fun willDelete(key: Key<User>, getModel: () -> User?, typeName: ReadMemory, options: Array<out Options.Write>) { getModel() } override fun didDelete(key: Key<User>, model: User?, typeName: ReadMemory, options: Array<out Options.Write>) { ui.remove(model) } }
Informing listeners
Sometimes, you need to pass some context to the listener(s). Things like "Where is the operation coming from?" or "Why is this operation happening?". In short, you may need to inform your listeners about context.
For example, you may want to know if you are creating a new User
, or updating one.
Doing so is easy.
First, create a class that will hold the context and have it implement Options.Write
:
enum class UserContext : Options.Write {
NEW, UPDATE
}
Next, recover it from your listener:
db.on<User>().register {
didPut {
val context = options.filterIsInstance<UserContext>().firstOrNull()
when (context) {
UserContext.NEW -> { /* insertion */ }
UserContext.UPDATE -> { /* update */ }
null -> { /* unknown */ }
}
}
}
Finally, don’t forget to add the context option when you perform the operation:
db.put(newUser, UserContext.NEW)
Local reactions
You may need to attach a callback to a specific operation or batch of operation.
For that, Kodein-DB provides the Anticipate
and React
options.
Regular
You can easily add a check that will run before an operation is performed (this is especially usefull for a batch):
db.put(newUser,
Anticipate { println("Will put a user!") },
React { println("Did put a user!") }
)
db.newBatch().use { batch ->
batch.addOptions(
Anticipate { println("Will write batch!") },
React { println("Did write batch!") }
)
}