Persistent Entity with type parameter


(Omid) #1

How should one define a persistent entity with type parameters, something like:

import akka.Done
import com.lightbend.lagom.scaladsl.persistence.{PersistentEntity, PersistentEntityRegistry}
import com.lightbend.lagom.scaladsl.persistence.PersistentEntity.ReplyType

trait MappingCommand[T] extends ReplyType[T]
case class Update[F,T](key:F, value:T) extends MappingCommand[Done]
case class RemoveKey[F](key:F) extends MappingCommand[Done]
case object GetMappings extends MappingCommand[MappingState[_,_]]

trait MappingEvent
case class Updated[F,T](key:F, value:T) extends MappingEvent
case class KeyRemoved[F](key:F) extends MappingEvent

case class MappingState[From, To](dict: Map[From, List[To]])

class MappingEntity[F,T] extends PersistentEntity {

  override type Command = MappingCommand[_]
  override type Event = MappingEvent
  override type State = MappingState[F, T]

  override def initialState: MappingState[F, T] = MappingState(Map.empty)

  override def behavior: Behavior = {
    Actions().onReadOnlyCommand[GetMappings.type , MappingState[F, T]] {
      case (GetMappings, ctx, state) => ctx.reply(state)
    }
    .onCommand[Update[F, T], Done] {
      case (Update(key: F, value: T), ctx, _) =>
        ctx.thenPersist(Updated(key, value)) { _ => ctx.reply(Done) }
    }.onEvent {
      case (Updated(key: F, value: T), state) =>
        state.copy(
          dict =
            if (state.dict.contains(key)) state.dict.updated(key, state.dict(key) :+ value)
            else state.dict.updated(key, List(value)))
    }.onCommand[RemoveKey[F], Done] {
      case (RemoveKey(key: F), ctx, _) =>
        ctx.thenPersist(KeyRemoved(key)) { _ => ctx.reply(Done) }
    }.onEvent {
      case (KeyRemoved(key: F), state) =>
        state.copy(dict = state.dict - key)
    }
  }
}

(Tim Moore) #2

Hi @omidb, are you still having a problem with this?

It’s not totally clear to me what the question is. Is the code not compiling, or you’re not sure how to use it? Can you go into more detail about what you tried and what problems you had?


(Omid) #3

Hi @TimMoore,

This code won’t compile. It is really hard to use type parameters for a persistent entity. Also, it’s really hard to compose them. Here, I’m trying to create a generic Map entity that can contain a mapping from a key to a list of values. How can one implement this?

Thanks


(Tim Moore) #4

What error do you get from the compiler?


(Omid) #5

This is the error message:

[error] ..boom..\impl\MappingEntity.scala:29: type arguments [mypackage.impl.GetMappings.type,mypackage.impl.MappingState[F,T]] do not conform to method onReadOnlyCommand's type parameter bounds [C <: MappingEntity.this.Command with com.lightbend.lagom.scaladsl.persistence.PersistentEntity.ReplyType[Reply],Reply]
[error]     Actions().onReadOnlyCommand[GetMappings.type , MappingState[F, T]] {
[error]                                ^
[error] ..boom..\impl\MappingEntity.scala:30: value reply is not a member of Any
[error]       case (GetMappings, ctx, state) => ctx.reply(state)
[error]                                             ^
[error] ..boom..\impl\MappingEntity.scala:34: value thenPersist is not a member of Any
[error]         ctx.thenPersist(Updated(key, value)) { _ => ctx.reply(Done) }
[error]             ^
[error] ..boom..\impl\MappingEntity.scala:34: value reply is not a member of Any
[error]         ctx.thenPersist(Updated(key, value)) { _ => ctx.reply(Done) }
[error]                                                         ^
[error] ..boom..\impl\MappingEntity.scala:37: value copy is not a member of Any
[error]         state.copy(
[error]               ^
[error] ..boom..\impl\MappingEntity.scala:38: not found: value dict
[error]           dict =
[error]           ^
[error] ..boom..\impl\MappingEntity.scala:43: value thenPersist is not a member of Any
[error]         ctx.thenPersist(KeyRemoved(key)) { _ => ctx.reply(Done) }
[error]             ^
[error] ..boom..\impl\MappingEntity.scala:43: value reply is not a member of Any
[error]         ctx.thenPersist(KeyRemoved(key)) { _ => ctx.reply(Done) }
[error]                                                     ^
[warn] ..boom..\impl\MappingEntity.scala:45: abstract type pattern F is unchecked since it is eliminated by erasure
[warn]       case (KeyRemoved(key: F), state) =>
[warn]                             ^
[error] ..boom..\impl\MappingEntity.scala:46: value copy is not a member of Any
[error]         state.copy(dict = state.dict - key)
[error]               ^
[error] ..boom..\impl\MappingEntity.scala:46: not found: value dict
[error]         state.copy(dict = state.dict - key)
[error]                    ^

(Tim Moore) #6

The types you’re using for replies in the command handlers need to match the types you’ve defined for the command class.

For example, you have:

case object GetMappings extends MappingCommand[MappingState[_,_]]

but then the command handler is:

.onReadOnlyCommand[GetMappings.type , MappingState[F, T]]

The second type parameter for onReadOnlyCommand needs to match the reply type of the command.

If you want to parameterize the reply type, then GetMappings will need to be defined as a parameterized class:

case class GetMappings[F, T]() extends MappingCommand[MappingState[F,T]]
//...
.onReadOnlyCommand[GetMappings[F, T] , MappingState[F, T]] {
  case (GetMappings(), ctx, state) => ctx.reply(state)
}

Otherwise, the reply type for the command line handler should use wildcards in the same way the command object is defined:

.onReadOnlyCommand[GetMappings.type , MappingState[_, _]]

I think the other type errors are cascading from there.

You might also reconsider the design. Usually, a persistent entity is modelling a domain entity. It might be better to use the generic mapping as an internal implementation detail of the entity (for example in the state) and to use concrete entity types that model the domain of your application.