4
votes

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 
}
2

2 Answers

5
votes

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:

object FixedScalaAnnotations {
  type Id = com.packagecontainingid.Id @field //replace by the right package
  type Column = com.packagecontainingcolumn.Column @field
}

And then import it in your case class and use it instead of the original:

import FixedScalaAnnotations._

@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)

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/

1
votes

@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 class

Solution: I had to make IdBaseEntity a trait instead and everything worked great

Here is the hierarchy

BaseEntity.scala

import java.util.Date

trait BaseEntity {
  val createdOn: Option[Date] = None
  val modifiedOn: Option[Date] = None
}

IdBaseEntity.scala

trait IdBaseEntity[T <: java.io.Serializable] extends BaseEntity {
  val id: Option[T] = None
}

FixedScalaAnnotations.scala

import scala.annotation.meta.field

object FixedScalaAnnotations {
  type Id = javax.persistence.Id @field
  type Column = javax.persistence.Column @field
}

Organization.scala

import java.util.{Date, UUID}
import FixedScalaAnnotations._
import javax.persistence.Entity

@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]