Using JPA With Scala

With the java persistence API JPA, you should be able to add a few standardized annotations to classes that represent the entities of your application and get an easy to use, vendor neutral persistence layer that you can use with different persistence providers without changing your code. On this page I test how far you can get with different engines if you write your code in scala. As it turns out, your scala JPA entities are vendor neutral if and only if your vendor is eclipselink.
So how does the current (as of march 2009) crop of persistence providers fare with entities implemented in scala? Here is the executive summary:
Apache OpenJPA 1.2.0 EclipseLink 1.1.0 Hibernate 3.4.0.GA
Single Table FAIL OK OK
One-To-Many FAIL OK OK
Many-To-Many FAIL OK OK
Composite PK FAIL OK FAIL

Why or Why Not to Use Scala for JPA Entites

Writing JPA entities in scala has some advantages and some disadvantages. The major disadvantage at the time is that scala up to versions 2.7.x does not support nested annotations, so you cannot use any of the JPA annotations that represent collections of related annotations (like @JoinColumns or @NamedQueries) or have a complex structure like @SqlResultSetMapping. But you can get quite far by designing you entity classes in a way that will give you sensible table definitions with the default naming conventions.

One of the main advantages -- apart from the general fact that scala code can be very succinct -- becomes visible if you have to deal with composite primary keys. Using composite primary keys can be quite tedious in java, because you have to implement an @IdClass for the key that is serializable and overrides equals and hashCode. In java this amounts to approximately a whole page of boilerplate code for every composite key. But the astute reader might have noticed that the requirements for an @IdClass sound awfully similar to the properties of a scala case class, and indeed, they are. In scala, all this boilerplate boils down to

case class CompositeKey(var some_id: Int, var other_id: Int)
Of course, if you have many composite keys, you will also most probably have foreign key relationships on them for which you will want to use @JoinColumns, so scala is a mixed blessing here. But since scala 2.7 and up supports mixed language projects, you can still write most of your code including all the @IdClasses in scala and fall back to java only for those entities that require nested annotations while you wait for [Ext. Link]Lukas Rytz to fix named arguments, or define the things you cannot express with annotations in an orm.xml file.

Some useful tips on how to deal with this are to be found on the [Ext. Link]Lift JPA page.

The Tests

For this comparison I used four simple scenarios that I tested with the latest stable version of every JPA implementation I could find. The first test persists a single entity into a single table as a test of basic functionality:
package jpatest

import _root_.javax.persistence._

object Project {
  def find(id: Int)(implicit em: EntityManager) : Project =
    em.find(classOf[Project], id).asInstanceOf[Project]
}

@Entity
@Table{ val name="projects" }
class Project(name: String) {

  def this() = this("")

  @Id
  @GeneratedValue{ val strategy=GenerationType.IDENTITY }
  var id : Int = _

  @Version
  var version : Int = _

  var title : String = name
}
The second test adds a milestone entity and a simple on-to-many relationship from projects to milestones. The code added to the project entity class is (the targetEntity is only necessary to work around a bug in 2.7 that will be fixed in 2.8):
  @OneToMany{val mappedBy = "project",
             val cascade = Array(CascadeType.ALL),
	     val targetEntity =  classOf[Milestone2],
	     val fetch = FetchType.LAZY }
  var milestones : java.util.List[Milestone2] =
    new java.util.Vector[Milestone2]
The third test gets rid of the milestones again and instead adds a many-to-many relationship between projects and users. Both classes get an attribute like this:
  @ManyToMany{val cascade = Array(CascadeType.ALL),
	      val targetEntity =  classOf[User3],
	      val fetch = FetchType.LAZY }
  var users : java.util.List[User3] =
    new java.util.Vector[User3]
The fourth test models this many-to-many relationship explicitly, so that we can add additional attributes like the role a user has within a projekt to it. The changes to the user and project entity class are trivial, instead of a many-to-many relationship to the other entity, both have a one-to-many relationship to the role entity:
  @OneToMany{val cascade = Array(CascadeType.ALL),
	     val targetEntity =  classOf[Role4],
	     val fetch = FetchType.LAZY }
  var users : java.util.List[Role4] =
    new java.util.Vector[Role4]
The interesting things happen in the role entity class, which has a composite primary key as described above consisting of the user and project primary key, as well as many-to-one relationships to both entities. The trick here is to make the primary key fields immutable, otherwise JPA will generate double mappings for the PK component and the many-to-one relationship that represent the same relationship to the other entity.
package jpatest

import _root_.javax.persistence._

case class Role4Key(var user_id: Int,
		    var proj_id: Int)

@Entity
@IdClass(classOf[Role4Key])
@Table{ val name="roles4" }
class Role4(name: String) {

  def this() = this("")

  @Id
  @Column{val name ="user_id", val nullable=false,
	  val updatable=false, val insertable=false}
  var user_id: Int = _

  @Id
  @Column{val name ="proj_id", val nullable=false,
	  val updatable=false, val insertable=false}
  var proj_id: Int = _

  @Version
  var version : Int = _

  var title : String = name

  @ManyToOne{val cascade = Array(CascadeType.ALL),
	     val targetEntity = classOf[User4],
	     val fetch = FetchType.LAZY }
  @JoinColumn{val name = "user_id"}
  var user: User4 = _

  @ManyToOne{val cascade = Array(CascadeType.ALL),
	     val targetEntity = classOf[Project4],
	     val fetch = FetchType.LAZY }
  @JoinColumn{val name = "proj_id"}
  var project: Project4 = _
}

The Candidates

I found five free JPA implementions (thanks to [Ext. Link]hints from the mailing lists): DataNucleus requires to run some tool to modify the class files before deployment which required more ant-wrangling than I wanted to invest for this test. (And, as others have found out since, [Ext. Link]the DataNucleus bytecode enhancer has problems with scalac generated bytecode.) Ebean seems to require programmaic configuration, so I could not test it by just switching out the persistence.xml. The other three allow the deployment of unmodified classes, so I restricted my tests to these.

The Results

The way the persistence [Ext. Link]annotations end up in the class files produced by the scala compiler seems to confuse OpenJPA. Technically there is nothing wrong with the way OpenJPA behaves here, the [Ext. Link]behaviour of JPA in the presence of confusing annotations is undefined and the problems are stricly scala problems. But still, all I ever got out of OpenJPA in any of my tests were exceptions like

TEST FAILED - Runner.testJPA_1: Type "jpatest.Project3" attempts
to use both field and property access. Only one access method is
permitted. Field access is used on the following fields: [private
java.util.List jpatest.Project3.users, private int
jpatest.Project3.version, private int jpatest.Project3.proj_id,
private int jpatest.Project3.proj_id]. Property access is used on
the following methods: [public void
jpatest.Project3.version_$eq(int), public java.util.List
jpatest.Project3.users(), public int jpatest.Project3.proj_id(),
public int jpatest.Project3.proj_id(), public void
jpatest.Project3.users_$eq(java.util.List), public void
jpatest.Project3.proj_id_$eq(int), public void
jpatest.Project3.proj_id_$eq(int), public int
jpatest.Project3.version()].
Eclipselink and Hibernate performed the first three test without any problems. The first difference appeared in the test with the composite primary key. Eclipselink mastered this test without problems. Hibernate first complained about the @IdClass, unlike eclipselink it insists on a default constructor for this class too, like for a proper entity class. This is easily remedied, just change it to:
case class Role4Key(var user_id: Int, var proj_id: Int) {
  def this() = this(0,0)
}
But with this change the real problems just start. You get a long trace of nested exceptions, with the root cause beeing
Caused by: java.sql.SQLException: Invalid argument in JDBC call:
  parameter index out of range: 5
        at org.hsqldb.jdbc.Util.sqlException(Unknown Source)
        at org.hsqldb.jdbc.Util.sqlException(Unknown Source)
        ... lots more
This happens probably when trying to add the sixth parameter to this four parameter query, which is the last one hibernate logs:
Hibernate: insert into roles4 (proj_id, title, user_id, version)
           values (?, ?, ?, ?)

The Code

Here is a tar-archive with the code. To use it, you must download the JPA implementations, install scalatest and fix the paths in the ant.properties file.

History

The first version contained @BeanProperty annotations on all fields, which are nice in general, but unnecessary for JPA.
Florian Hars <florian@hars.de>, 2009-05-22 (orig: 2009-03-25)