views:

82

answers:

2

I'm using Scala to do typesafe JPA2 Criteria Queries. Therefore i have a Java MetaModel Class (the only Java in my code, the rest is Scala -> pure Scala problem), which holds my model attributes:

@StaticMetamodel(User.class)
public class User_ {
  public static volatile SingularAttribute<User, Long> id;
  public static volatile SingularAttribute<User, String> name;
}

To do a query for one single attribute, i have this function:

def findByAttribute[T](
  attribute:SingularAttribute[User, T], value:T):ArrayList[User] = 
{
  ...
}

Which i can call like this:

userEJB.findByAttribute(User_.name, "John")

Now i'm trying to create a query function, with which i can query for multiple attributes at once, and therefore i want to use a Map of SingularAttributes as a parameter for my function:

// Map is of type scala.collection.immutable.Map
def findByAttributes[T](
  attributes:Map[SingularAttribute[User, T], T]):ArrayList[User] = 
{
  ...
}

Ok, so the function should work... But how can i call it??? Say for example i want to query with a Map like this:

User_.name -> "Doe"
User_.id -> 5

So my first approach of defining this Map in Scala and passing it to findByAttributes is this:

val criteria = Map(User_.name -> "Doe", User_.id -> 5)
// Produces Compiler Error
val users = userEJB.findByAttributes(criteria)

Unfortunately, the compiler isn't satisfied when passing searchFor to the findByAttributes-function, producing the error below:

no type parameters for method findByAttributes: (attributes: 
Map[javax.persistence.metamodel.SingularAttribute[net.teachernews.model.User,
T],T])
java.util.ArrayList[net.teachernews.model.User] exist so that it can be applied to 
arguments (scala.collection.immutable.Map[javax.persistence.metamodel.
SingularAttribute[
net.teachernews.model.User, _ 
>: java.lang.Long with java.lang.String 
<: java.lang.Comparable[_ 
>: java.lang.Long with java.lang.String 
<: java.lang.Comparable[_ >: java.lang.Long with java.lang.String 
<: java.io.Serializable] with java.io.Serializable] 
with java.io.Serializable],Any])  --- because --- 
argument expression's type is not compatible with formal parameter type;  
found   : 
scala.collection.immutable.Map[javax.persistence.metamodel.SingularAttribute[
net.teachernews.model.User, _ 
>: java.lang.Long with java.lang.String 
<: java.lang.Comparable[_ 
>: java.lang.Long with java.lang.String    
<: java.lang.Comparable[_ >: java.lang.Long with java.lang.String 
<: java.io.Serializable] with java.io.Serializable] with java.io.Serializable],
Any]  
required: Map[javax.persistence.metamodel.SingularAttribute[
net.teachernews.model.User,?T],?T]

That's the most complicated generic problem i ever had. A bit too high for my skill ;) Anyone knows how i can build the right map type that i can pass to the function? Is it even possible, or can't the compiler infer the type anymore in my case? Or am i using the wrong datastructure?

+2  A: 

When you declare the map as follows

val criteria = Map(User_.name -> "Doe", User_.id -> 5)

the compiler will infer a somewhat weird type:

scala.collection.immutable.Map[SingularAttribute[User, _ >: Long with String], Any]

Try it out yourself in Scala's REPL!

The problem here is that the compiler infers Any as the common type of String and Int which is actually true. So, you're losing any information about the actual values in the map. And of course the key also isn't really what you would want.

This means that Map is obviously not the right type. You could try to use a n-tuple instead:

((User_.name -> "Doe"), (User_.id -> 5))

So, all type information will be stored correctly. Of course, you will then have to create several findByAttributes functions (one for 1-tuple, one for 2-tuple, 3-tuple and so on ...). This is a bit tedious, but it's the best solution I could think of now.

Michel Krämer
Unfortunately, the parameter has to be of variable length, because i want to use this function for all of my entities, without having to modify it. And the user shouldn't have to pay attention in which order the parameters are passed. Isn't there another type as an option? Thanks for the hints, helped me understand the problem.
ifischer
+1  A: 

I haven't fully worked out the details, but I think something like

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

def findByAttributeValuePair[T](p:AttributeValuePair[T]):ArrayList[User] =
   findByAttribute(p._1, p._2)

def findByAttributes(attributes:AttributeValuePair[_]*):ArrayList[User] = 
{
   ...
}

might work. Then a call would look like

findByAttributes(User_.name -> "Doe", User_.id -> 5)

Edit: The findByAttributeValuePair method may be necessary to get the call to findByAttribute to type-check, judging from my fiddling in the Scala 2.8 REPL.

Aaron Novstrup
Wow. I knew that it has to be possible somehow! Just tested it, seems to work fine. Types are correctly checked. Do you know if a function like my findByAttributes, with all the type checking, is possible in Java at all? Would be a good pro-Scala argument...
ifischer
In this case, the Java version wouldn't be much different. It would just be more verbose, and you'd have to find or write a Pair class since it's not included in the Java library. The calls would have to be a bit uglier, using a factory method to perform the type checking and construct the pairs:findByAttributes(pair(User_.name, "Doe"), pair(User_.id, 5))
Aaron Novstrup