views:

278

answers:

3

EDIT: Rewrote the question. Added bounty as its important for me. The final hint with which i can get findByAttributes to work (without reimplementing it in subclasses) will get my points.

In my app i'm doing typesafe database queries with the new JPA2 Criteria Query. Therefore I have a trait DAO which should be (re)usable for ALL entities in my application. So this is how the outline the current trait i'm using looks like (which works):

trait DAO[T, K](implicit m: Manifest[T]) {
  @PersistenceContext 
  var em:EntityManager = _

  lazy val cb:CriteriaBuilder = em.getCriteriaBuilder

  def persist(entity: T)
  def update(entity: T)
  def remove(entity: T)
  def findAll(): ArrayList[T]

  // Pair of SingularAttribute and corresponding value
  // (used for queries for multiple attributes)
  type AttributeValuePair[A] = Pair[SingularAttribute[T, A], A]

  // Query for entities where given attribute has given value
  def findByAttribute[A](attribute:AttributeValuePair[A]):ArrayList[T]

  // Query for entities with multiple attributes (like query by example)
  def findByAttributes[A](attributes:AttributeValuePair[_]*):ArrayList[T] 
}

In a concrete DAO, i'm extending this trait like this, setting the type and implementing the methods (removed all except the most important method):

class UserDAO extends DAO[User, Long] {
  override type AttributeValuePair[T] = Pair[SingularAttribute[User, T], T]

  override def findByAttributes[T](attributes:AttributeValuePair[_]*):ArrayList[User] = {
    val cq = cb.createQuery(classOf[User])
    val queryRoot = cq.from(classOf[User])
    var criteria = cb.conjunction
    for (pair <- attributes) 
      criteria = cb.and(cb.equal(queryRoot.get(pair._1), pair._2 ))
    cq.where(Seq(criteria):_*)
    val results = em.createQuery(cq).getResultList
    results.asInstanceOf[ArrayList[User]]
  }
}

BTW, findByAttributes is really nice to use. Example:

val userList = userEJB.findByAttributes(
  User_.title -> Title.MR, 
  User_.email -> "[email protected]"
)

I realized, that findByAttributes is so generic, that its the same in ALL classes of my app that implement the DAO. The only thing that changes is the Type used in the method. So in another class wich inherits DAO, its

def findByAttributes[T](attributes:AttributeValuePair[_]*):ArrayList[Message] = {
  val cq = cb.createQuery(classOf[Message])
  val queryRoot = cq.from(classOf[Message])
  var criteria = cb.conjunction
  for (pair <- attributes) 
    criteria = cb.and(cb.equal(queryRoot.get(pair._1), pair._2 ))
  cq.where(Seq(criteria):_*)
  val results = em.createQuery(cq).getResultList
  results.asInstanceOf[ArrayList[User]]
}

So i created a new abstract class named SuperDAO that should contain the implemented generic methods, so that i don't have to re-implement them in every subclass. After some help from Landei (thanks), the (most important part of my) current implementation of my SuperDAO looks like this

abstract class SuperDAO[T, K](implicit m: Manifest[T]) {
  @PersistenceContext
  var em:EntityManager = _

  lazy val cb:CriteriaBuilder = em.getCriteriaBuilder

  type AttributeValuePair[A] = Pair[SingularAttribute[T, A], A]

  def findByAttributes(attributes:AttributeValuePair[_]*):ArrayList[T] = {
    val cq = cb.createQuery(m.erasure)
    val queryRoot = cq.from(m.erasure)
    var criteria = cb.conjunction
      for (pair <- attributes) { 
        criteria = cb.and(
          cb.equal(
            // gives compiler error
            queryRoot.get[SingularAttribute[T,_]](pair._1)
          )
          ,pair._2
        )
      }
    cq.where(Seq(criteria):_*)
    val results = em.createQuery(cq).getResultList
    results.asInstanceOf[ArrayList[T]]
  }

So the current problem is that the line with queryRoot.get produces the following error:

overloaded method value get with alternatives:   
(java.lang.String)javax.persistence.criteria.Path
[javax.persistence.metamodel.SingularAttribute[T, _]] <and>
(javax.persistence.metamodel.SingularAttribute[_ >: Any, 
javax.persistence.metamodel.SingularAttribute[T,_]])
javax.persistence.criteria.Path
[javax.persistence.metamodel.SingularAttribute[T, _]]  
cannot be applied to 
(javax.persistence.metamodel.SingularAttribute[T,_$1])

Whats meant with $1???

If needed: SingularAttribute Javadoc

EDIT @Landei:

Changing the method signature to

def findByAttributesOld[A](attributes:AttributeValuePair[A]*):ArrayList[T] = {

And the queryRoot.get to

queryRoot.get[A](pair._1.asInstanceOf[SingularAttribute[T,A]])

Results in the (much shorter!) error:

overloaded method value get with alternatives:  
(java.lang.String)javax.persistence.criteria.Path[A] <and>   
(javax.persistence.metamodel.SingularAttribute[_ >: Any,     A])
javax.persistence.criteria.Path[A]  cannot be applied to 
(javax.persistence.metamodel.SingularAttribute[T,A])

The solution of @Sandor Murakozi seems to work. Have to test it a bit. But i would also appreciate a shorter solution, if its possible at all!

+2  A: 

This should (?) work:

abstract class DAO[T, K <: Serializable](implicit m: Manifest[T]) {
 ...

def findByAttributes[T](attributes:AttributeValuePair[_]*):ArrayList[T] = {
  val cq = cb.createQuery(m.erasure)
  val queryRoot = cq.from(m.erasure)
  var criteria = cb.conjunction
  for (pair <- attributes) 
    criteria = cb.and(cb.equal(queryRoot.get(pair._1), pair._2 ))
  cq.where(Seq(criteria):_*)
  val results = em.createQuery(cq).getResultList
  results.asInstanceOf[ArrayList[T]]
}

}

[Edit]

Aaargh!1!11!!!!

I think you need to write findByAttributes(...), not findByAttributes[T](...), else that T shadows the T of the DAO class (which is the "right one"). I'm not sure that this solves your problem, but as it is, it is wrong.

[Edit1]

I didn't read the API careful enough. I think you want to use this Version of get.

So we would have to provide only the second type parameter of the SingularAttribute. The problem is that this would be the same as the one in AttributeValuePair[_]. I honestly don't know how to preceed here. You could try

def findByAttributes[A](attributes:AttributeValuePair[A]*):ArrayList[T] = {...

or

queryRoot.get[A](pair._1.asInstanceOf[SingularAttribute[T,A]])

If this doesn't work, we get at least some interesting error messages, which may give us a hint :-)

Landei
Thanks. Added a related edit to my question.
ifischer
There are four versions of get in javax.persistence.criteria.Path (which is a superclass of Root), so you can try to give the compiler a hint:... queryRoot.get[SingularAttribute[T,_]](pair._1)...
Landei
Thanks! But still doesn't work. Added another edit.
ifischer
See my [Edit]...
Landei
Thanks for your help so far. And another edit...
ifischer
See my [Edit1]...
Landei
+2  A: 

This one compiles without errors. However, I did not try to run it, so you may get some exceptions (e.g. from queryRoot.asInstanceOf[Root[T]], I have a little bit bad feeling about it):

  def findByAttributes(attributes:AttributeValuePair[_]*):ArrayList[T] = {
    val cq = cb.createQuery(m.erasure)
    val queryRoot = cq.from(m.erasure)
    var criteria = cb.conjunction
      for (pair <- attributes) { 
        criteria = pred(pair, cb, queryRoot.asInstanceOf[Root[T]])
      }
    cq.where(Seq(criteria):_*)
    val results = em.createQuery(cq).getResultList
    results.asInstanceOf[ArrayList[T]]
  }


  def pred[A](pair: AttributeValuePair[A], 
      cb: CriteriaBuilder, 
      queryRoot: Root[T]): Predicate = 
    cb.and(cb.equal(queryRoot.get(pair._1),pair._2))

BTW in SuperDAO.findByAttributes the parentheses/params of cb.equal seems to be a bit mixed up. It looks OK in the other method.

About the _$1 type: I think when you say SingularAttribute[T,_] it will be a so called existential type. It is a shorthand for SingularAttribute[T,X] forSome { type X }. So the _ means that we don't really know what X is, but for sure there is a fixed type there. As you can have several existential types the compiler just calls them _$1, _$2 and so on. They are synthetically created names instead of X-es.
Existential types are used mostly when you use Java generics with wildcard or raw types. In these cases some tricks (like introducing an extra method with it's own type param) may be needed for proper type checking.

Sandor Murakozi
Impressive. Seems to work so far! Will do some testing...
ifischer
Cool. I hope it will work. One thing that came to my mind was that maybe you could use the type given by m.erasure instead of T inside of the method (or in another extracted from it). This way you might be able to avoid the ugly asInstanceOf, which is not 100% correct from the typing point of view, just Java probably does not care about it because of erasure.
Sandor Murakozi
+2  A: 

EDIT: Added comments as requested by @ifischer

I think your main problem is that you lose valuable type information by just passing m.erasure as this returns Class[_] instead of Class[T] what you actually want here. Doing a cast before the rest will save you some nasty stuff.

Also the unbound wildcards used in JPA 2.0 are a bit annoying as you need to jump some hoops to get around them.

As it does not make much sense to query for no attributes I pulled the first attribute out of the *-parameter. This also means that you do not need to start with conjunction.

I shortened some names so that the code fits in the box without line breaks:

// import java.util.list as JList, so it does not shadow scala.List
import java.util.{List => JList}

abstract class SuperDAO[T <: AnyRef, K](implicit m: Manifest[T]) {

  @PersistenceContext
  var em: EntityManager = _

  // pretend that we have more type info than we have in the Class object.
  // it is (almost) safe to cast the erasure to Class[T] here
  def entityClass = m.erasure.asInstanceOf[Class[T]]

  lazy val cb: CriteriaBuilder = em.getCriteriaBuilder

  // Type alias for SingularAttributes accepted for this DAOs entity classes
  // the metamodel will only ever provide you with Attributes of the form
  // SingularAttribute<? super X,E>, where X is the entity type (as your
  // entity class may extend from another) and E is the element type.
  // We would actually like to use a contravariant definition of the first
  // type parameter here, but as Java has no notion of that in the definition
  // side, we have to use an existential type to express the contravariance
  // similar to the way it would be done in Java.
  type Field[A] = (SingularAttribute[_ >: T,A],A)

  // As we need at least one attribute to query for, pull the first argument out
  // of the varargs.
  def findByAttributes(attribute: Field[_], attributes: Field[_]*): JList[T] = {
    val cq = cb.createQuery(entityClass)
    val root = cq.from(entityClass)

    // shorthand for creating an equal predicate as we need
    // that multiple times below
    def equal(a: Field[_]) = cb.equal(root.get(a._1), a._2)

    // the Seq of Predicates to query for:
    def checks = Seq(
      // if there is only one argument we just query for one equal Predicate
      if (attributes.isEmpty) equal(attribute)

      // if there are more, map the varargs to equal-Predicates and prepend
      // the first Predicate to them. then wrap all of them in an and-Predicate
      else cb.and(equal(attribute) +: attributes.map(equal) : _*)
    )

    // as we already casted the entityClass we do not need to cast here
    em.createQuery(cq.where(checks : _*)).getResultList
  }
}
Moritz
Thanks. Unfortunately, i could only do a small test, but in the moment it seems like it works. Since it is the cleanest solution, ill give you the 100 points.
ifischer
@Moritz: thanks again for this solution. I'm still impressed ;) Could you do me a favor and make some comments and explanations into the code? especially the method signature, Field, checks and JList. Would help me a lot for a better understanding. I'm writing a thesis about this stuff, so i have to fully understand it
ifischer
@ifischer thanks for accepting it. I added some comments. Feel free to ask if there are still parts that are unclear.
Moritz
@Moritz: Did you mean to write "...you lose valuable type information..." instead of "...you use valuable type information..."?
Randall Schulz
@Randall Of course I meant lose... Fixed that.
Moritz