So here's my dilemma: I have a domain model with a bunch of case classes in scala such as User
and Organization
. Within my data access layer (dao, repository, etc) I am using astyanax(a java library from netflix) and it's entity persister to save the object to a cassandra column family.
Here is some sample code for my cassandra/astyanax-backed DAOs (yes i know I need to do something more scala-ish, but I'm still learning =))
After reading through this long-winded description, I'm basically looking to see why annotated vals in the param list won't work when java does getDeclaredAnnotations()
on the Field
I would hate to have to go back and refactor everything so I can use the persister which makes it really simple to save an entity (i.e. manager.put(entity)
). If I want to keep using case classes so I can work with more immutable style scala and Lens
from scalaz, I'll then have to update the DAO and do all the persisting manually which can really kill time.
So, if anyone knows something that I'm not seeing, please let me know! Thanks in advance for taking the time to read through this.
Scenario 1 - Case Class
Astyanax fails to pick up the annotation @Id on val
@Entity case class Organization(@Id @Column(name = "id") override val id: Option[UUID] = None, @Column(name = "created_on") override val createdOn: Option[Date] = None, @Column(name = "modified_on") override val modifiedOn: Option[Date] = None, @Column(name = "name") name: Option[String] = None, @Column(name = "is_paid_account") isPaidAccount: Boolean = false) extends IdBaseEntity[UUID](id, createdOn, modifiedOn)
Scenario 2 - Class with companion object or class without companion object
Astyanax fails to pick up the @Id annotation on val
@Entity class Organization(@Id @Column(name = "id") override val id: Option[UUID] = None, @Column(name = "created_on") override val createdOn: Option[Date] = None, @Column(name = "modified_on") override val modifiedOn: Option[Date] = None, @Column(name = "name") name: Option[String] = None, @Column(name = "is_paid_account") isPaidAccount: Boolean = false) extends IdBaseEntity[UUID](id, createdOn, modifiedOn) object Organization { def apply(id: Option[UUID] = None, createdOn: Option[Date] = None, modifiedOn: Option[Date] = None, name: Option[String] = None, isPaidAccount: Boolean = false) = new Organization(id, createdOn, modifiedOn, name, isPaidAccount) }
Scenario 3 - Case class or class with val defined inside block
This works fine because it picks up theId
as being annotated with @Id
, but I don't want do do this because IdBaseEntity
already defines and id val
and defeats the whole purpose of inheritance and being able to pass id
to the superclass
@Entity case class Organization(@Id @Column(name = "id") override val id: Option[UUID] = None, @Column(name = "created_on") override val createdOn: Option[Date] = None, @Column(name = "modified_on") override val modifiedOn: Option[Date] = None, @Column(name = "name") name: Option[String] = None, @Column(name = "is_paid_account") isPaidAccount: Boolean = false) extends IdBaseEntity[UUID](id, createdOn, modifiedOn) { @Id @Column(name = "id") val theId: Option[UUID] = id }
The Data access portion
Way down in manager you will see a call to build()
. Astyanax examines the class that was passed in to withEntityType()
, which in this case is classOf[Organization]
Every one of my scenarios fails except #3 when I have a val declared inside the class block instead of the parameter list for the case class or a regular class/regular class with companion object. Astyanax says that there is know member of that class that is annotated with @Id
so it throws an exception. Before I dig any further, I figured I would ask the community about the nuances of annotation a scala class and sending it to a java library that does reflection. The source is nothing special. In fact here are the relevant lines where things fail: https://github.com/Netflix/astyanax/blob/master/astyanax-entity-mapper/src/main/java/com/netflix/astyanax/entitystore/EntityMapper.java#L89-120
class CassandraOrganizationDAO extends BaseCassandraDAO[Organization, UUID](Astyanax.context) with OrganizationDAO { val ColumnFamilyOrganizations: ColumnFamily[UUID, String] = new ColumnFamily[UUID, String]( "organizations", TimeUUIDSerializer.get(), StringSerializer.get(), ByteBufferSerializer.get()) val ColumnFamilyOrganizationMembers: ColumnFamily[UUID, UUID] = new ColumnFamily[UUID, UUID]( "organization_members", TimeUUIDSerializer.get(), TimeUUIDSerializer.get(), DateSerializer.get()) val manager: EntityManager[Organization, UUID] = new DefaultEntityManager.Builder[Organization, UUID]() .withEntityType(classOf[Organization]) .withKeyspace(getKeyspace()) .withColumnFamily(ColumnFamilyOrganizations) .build() // the rest of the class is omitted }
@Mik378 provided great help in dealing with JPA annotations and this seems to work for people with stuff like Hibernate. However, in my case, I'm using Astyanax and it may be that it's just one of its nuances so I'll post the solution that worked for me given what I learned from @Mike378 above.
IMPORTANT: I couldn't get anything to work when
IdBaseEntity
was an abstract class or classSolution: I had to make IdBaseEntity a trait instead and everything worked great
Here is the hierarchy
BaseEntity.scala
IdBaseEntity.scala
FixedScalaAnnotations.scala
Organization.scala
I came across a similar issue, where my annotation were never taken in account in my constructor's fields. Indeed, I used SpringData and it was impossible to directly map annotation (like
@Indexed
) on a field.To enable annotations in your case class's constructor, you should first create this kind of class:
And then import it in your case class and use it instead of the original:
Be sure the original packages aren't used.
Here a related article dealing with JPA: http://blog.fakod.eu/2010/07/14/constructor-arguments-with-jpa-annotations/