Stand: 2013-08-28

Play 2 mit Scala und JPA/Hibernate

Von Haus aus ist die Scala-Variante von Play 2 nicht für die Kombination mit JPA (bzw. Hibernate) vorgesehen. Wie Play, Scala und JPA zusammenspielen zeigt dieser Artikel.

Inhalt

Transaktionen im Controller

Statt mit undurchsichtigen ThreadLocal-Objekten oder aspektorientierter Programmierung (AOP) zu arbeiten, reicht in Scala ein Lambda-Ausdruck, der innerhalb einer Transaktion ausgeführt wird. Ein weiterer Vorteil davon ist, dass man sehr genau steuern kann, wo eine Transaktion beginnen und enden soll. In einem Controller sieht eine Transaktionsklammer mit meinem Ansatz so aus:

def post(id: Long) = Action {
  Transaction { implicit entityManager =>
    val item = ItemRepository.findById(id)
    item.foreach(_.incrementClicks())
    Ok()
  }
}

Der entityManager wird als implizites Objekt in den Lambdaausdruck gereicht, damit man ihn nicht überall in den Parameterlisten wiederholen muss. Trotzdem ist dieser implizite Parameter expliziter als ein ThreadLocal-Objekt oder AOP.

Die zugehörige Repository-Methode in dem Beispiel hat dementsprechend zwei Parameterlisten, wovon eine implizit ist und den entityManager entgegen nimmt:

def findById(id: Long)(implicit entityManager: EntityManager): Option[Item] =
  Option(entityManager.find(classOf[Item], id))

Dieses Verfahren ist für alle Methoden erforderlich, die Zugriff auf den EntityManager benötigen.

Transaktionen bereitstellen

Folgender Code realisiert das Objekt, das die Transkation bereitstellt und den übergebenen Lambdaausdruck ausführt:

package util

import javax.persistence.{EntityManager, Persistence}

object Transaction {

  val emf = Persistence.createEntityManagerFactory("defaultPersistenceUnit")

  def apply[A](action: EntityManager => A): A = {
    val entityManager = emf.createEntityManager()
    def transaction = entityManager.getTransaction
    transaction.begin()
    try {
      val result = action(entityManager)
      if (transaction.isActive) // Transaktion kann bereits explizit zurückgerollt sein
        transaction.commit()
      result
    }
    catch {
      case exception: Throwable => 
        if (transaction != null && transaction.isActive)
            transaction.rollback()
        throw exception
    }
    finally 
      entityManager.close()
  }
  
  def rollback()(implicit entitymanager: EntityManager) {
    entitymanager.getTransaction().rollback()
  }
}

Entitätsklassen mit Scala und JPA

Bisher ging alles ziemlich glatt, aber an ein paar Stellen merkt man doch, dass JPA aus der Java-Welt kommt. Wenn Scalas Klassenparameter mit dem Schlüsselwort var auch gleich ein Feld anlegen, ist dort die BeanProperty-Annotation erforderlich, weil der Scala-Compiler Felder sonst anders anlegt, als Hibernate es erwartet.

@Entity
class Item(
  @BeanProperty
  @ManyToMany(cascade=Array(CascadeType.PERSIST), fetch = FetchType.EAGER) 
  var tags: Set[Tag]
)

Das Beispiel zeigt mit der Verwendung von java.util.HashSet noch eine weitere Besonderheit, die man bei der Kombination von Scala und JPA beachten muss: JPA bzw. Hibernate sind nur für Java Collections ausgelegt und wissen deshalb nicht, was sie mit Scala Collections anfangen sollen. Entweder muss man sich also Adapter schreiben, die Hibernate den Umgang mit Scala Collections beibringen, oder man verwendet eben die Java Collections.

pesistence.xml

JPA wird über die Datei conf/META-INF/persistence.xml konfiguriert, die etwa so aussehen kann:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
    http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">
             
    <persistence-unit name="defaultPersistenceUnit" transaction-type="RESOURCE_LOCAL">
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <properties>
        <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
        
        <!-- Mit der folgenden Zeile kann automatisch ein DB-Schema erzeugt werden. -->
        <!-- <property name="hibernate.hbm2ddl.auto" value="create-drop"/> -->
        
        <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
        <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost:5432/example"/>
        <property name="javax.persistence.jdbc.user" value="userx"/>
        <property name="javax.persistence.jdbc.password" value="topsecret"/>
        <property name = "hibernate.show_sql" value="false" />
      </properties>
    </persistence-unit>
    
</persistence>

Benötigte Bibliotheken

Zu guter Letzt fehlen noch die Bibliotheken, die die Play-Anwendung benötigt, um JPA verwenden zu können. Trage dazu in die Datei Build.scala folgende Bibliotheken ein:

val appDependencies = Seq(
    "postgresql" % "postgresql" % "9.1-901.jdbc4",
    "org.hibernate" % "hibernate-core" % "4.1.9.Final",
    "org.hibernate" % "hibernate-entitymanager" % "4.1.9.Final")

Wobei der JDBC-Treiber für PostgreSQL nur ein Beispiel ist; für andere Datenbanken musst du natürlich den jeweiligen Treiber angeben.

Wesentliche Änderungen