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)
@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
@IdClass
es in scala and fall back to java only for those
entities that require nested annotations while you wait for 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 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
}
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]
@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]
@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]
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 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, 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 thepersistence.xml
.
The other three allow the deployment of unmodified classes, so I
restricted my tests to these.
The Results
The way the persistence 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 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()].
@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)
}
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
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 theant.properties
file.
History
The first version contained@BeanProperty
annotations on
all fields, which are nice in general, but unnecessary for JPA.