In Akka Persistence, the data is cached in the service memory (state), and the back-end stores some persistent event logs, so it is impossible to use DSL like SQL for paging query. Using Akka Streams and Actor, we can achieve the effect of paging query by coding, and the paging query is still step-by-step parallel
EventSourcedBehavior
CQRS model is implemented in EventSourcedBehavior of Akka Persistence. Command processing and event processing are decoupled by commandHandler and eventHandler. commandHandler processes the incoming command and returns an event. You can choose to persist the event. If the event needs to be persisted, the event will be passed to eventHandler for processing. After the event is processed, eventHandler will return a "new" state (or directly return to the original state without updating).
def apply[Command, Event, State]( persistenceId: PersistenceId, emptyState: State, commandHandler: (State, Command) => Effect[Event, State], eventHandler: (State, Event) => State): EventSourcedBehavior[Command, Event, State]
modeling
In terms of the database table modeling we are used to, we will have the following table:
create table t_config ( data_id varchar(64), namespace varchar(64) not null, config_type varchar(32) not null, content text not null, constraint t_config_pk primary key (namespace, data_id) ); create index t_config_idx_data_id on t_config (data_id);
ConfigManager actor can be regarded as t config table. Its entityId is namespace. The State holds the primary key values of all records (ConfigManagerState), which is equivalent to t config IDX data ID index of T config table.
Confidentity actors can be regarded as records stored in the t ﹣ config table, and each actor instance is a row of records. Its entityId consists of namespace + data ID, which is equivalent to the T config PK composite primary key of the T config table. Here we define two EventSourcedBehavior:
- ConfigManager: has a list of all configuration ID s and saves them as State in EventSourcedBehavior
- Confidentity: owns each configuration data and saves it as a State in EventSourcedBehavior
Realization
Here is the code of ConfigManager and confidentity. Next, I will explain how to implement paging query.
ConfigManager
object ConfigManager { sealed trait Command extends CborSerializable sealed trait Event extends CborSerializable sealed trait Response extends CborSerializable final case class Query(dataId: Option[String], configType: Option[String], page: Int, size: Int) extends Command final case class ReplyCommand(in: AnyRef, replyTo: ActorRef[Response]) extends Command private final case class InternalResponse(replyTo: ActorRef[Response], response: Response) extends Command case class ConfigResponse(status: Int, message: String = "", data: Option[AnyRef] = None) extends Response final case class ConfigManagerState(dataIds: Vector[String] = Vector()) extends CborSerializable val TypeKey: EntityTypeKey[Command] = EntityTypeKey("ConfigManager") } import ConfigManager._ class ConfigManager private (namespace: String, context: ActorContext[Command]) { private implicit val system = context.system private implicit val timeout: Timeout = 5.seconds import context.executionContext private val configEntity = ConfigEntity.init(context.system) def eventSourcedBehavior(): EventSourcedBehavior[Command, Event, ConfigManagerState] = EventSourcedBehavior( PersistenceId.of(TypeKey.name, namespace), ConfigManagerState(), { case (state, ReplyCommand(in, replyTo)) => replyCommandHandler(state, replyTo, in) case (_, InternalResponse(replyTo, response)) => Effect.reply(replyTo)(response) }, eventHandler) private def processPageQuery( state: ConfigManagerState, replyTo: ActorRef[Response], in: Query): Effect[Event, ConfigManagerState] = { val offset = if (in.page > 0) (in.page - 1) * in.size else 0 val responseF = if (offset < state.dataIds.size) { Source(state.dataIds) .filter(dataId => in.dataId.forall(v => v.contains(dataId))) .mapAsync(20) { dataId => configEntity.ask[Option[ConfigState]](replyTo => ShardingEnvelope(dataId, ConfigEntity.Query(in.configType, replyTo))) } .collect { case Some(value) => value } .drop(offset) .take(in.size) .runWith(Sink.seq) .map(items => ConfigResponse(IntStatus.OK, data = Some(items))) } else { Future.successful(ConfigResponse(IntStatus.NOT_FOUND)) } context.pipeToSelf(responseF) { case Success(value) => InternalResponse(replyTo, value) case Failure(e) => InternalResponse(replyTo, ConfigResponse(IntStatus.INTERNAL_ERROR, e.getLocalizedMessage)) } Effect.none } }
ConfigEntity
object ConfigEntity { case class ConfigState(namespace: String, dataId: String, configType: String, content: String) sealed trait Command extends CborSerializable sealed trait Event extends CborSerializable final case class Query(configType: Option[String], replyTo: ActorRef[Option[ConfigState]]) extends Command final case class ConfigEntityState(config: Option[ConfigState] = None) extends CborSerializable val TypeKey: EntityTypeKey[Command] = EntityTypeKey("ConfigEntity") } import ConfigEntity._ class ConfigEntity private (namespace: String, dataId: String, context: ActorContext[Command]) { def eventSourcedBehavior(): EventSourcedBehavior[Command, Event, ConfigEntityState] = EventSourcedBehavior(PersistenceId.of(TypeKey.name, dataId), ConfigEntityState(), commandHandler, eventHandler) def commandHandler(state: ConfigEntityState, command: Command): Effect[Event, ConfigEntityState] = command match { case Query(configType, replyTo) => state.config match { case None => Effect.reply(replyTo)(None) case Some(config) => val resp = if (configType.forall(v => config.configType.contains(v))) Some(config) else None Effect.reply(replyTo)(resp) } } }
The configmanager ා processpagequery function implements most of the paging query logic (some logic needs to be processed by confidentity).
val offset = if (in.page > 0) (in.page - 1) * in.size else 0 val responseF = if (offset < state.dataIds.size) { // process paging } else { Future.successful(ConfigResponse(IntStatus.OK, data = Some(Nil))) }
Here, first obtain the actual paging data offset offset, and then judge the size of the dataIds saved in the ConfigManager state. If the offset is less than state.dataIds.size, we will perform paging logic, otherwise we will directly return an empty list to the front end.
Source(state.dataIds) .filter(dataId => in.dataId.forall(v => v.contains(dataId))) .mapAsync(20) { dataId => configEntity.ask[Option[ConfigState]](replyTo => ShardingEnvelope(s"$namespace@$dataId", ConfigEntity.Query(in.configType, replyTo))) } .collect { case Some(value) => value } .drop(offset) .take(in.size) .runWith(Sink.seq) .map(items => ConfigResponse(IntStatus.OK, data = Some(items)))
This Akka Streams stream is the main implementation of paging processing. If it is SQL, it is similar to:
select * from t_config where data_id like '%"in.dataId"%' offset "offset" limit "in.size"
mapAsync performs 20 concurrent asynchronous operations in the flow execution process, and delegates each matching configeintity (entityId generated by s"$namespace@$dataId") to query the config ﹐ type field. In this way, the complete SQL statement is similar to:
select * from t_config where data_id like '%"in.dataId"%' and change_type = "in.changeType" offset "offset" limit "in.size"
Confidentity's query logic for the change ﹐ type part is as follows:
case Query(configType, replyTo) => state.config match { case None => Effect.reply(replyTo)(None) case Some(config) => val resp = if (configType.forall(v => config.configType.contains(v))) Some(config) else None Effect.reply(replyTo)(resp) }
If the in.config type is empty, you don't need to judge the change ﹐ type field, but you can directly return Some(config). The SQL statement is similar at this time:
select * from t_config where data_id like '%"in.dataId"%' and true offset "offset" limit "in.size"
Tip here is a tip. For the judgment of Option[T] field, the. forall method is directly used. It is equivalent to:
option match { case Some(x) => p(x) case None => true }
Summary
The full code can be found here https://github.com/yangbajing/yangbajing-blog/tree/master/src/main/scala/blog/persistence/config Find it.