¡La versión 2.0 de Play ya está lista! Ayúdanos a traducir la documentación de la útlima versión y sigue nuestro progreso.

Manuales, tutoriales & referencias

Consulte

Contenidos

Elija la versión

Buscar

Busque con google

Libros

Persistencia JPA

Play incluye un conjunto de funciones (helpers) muy útiles para simplificar la gestión de tus entidades JPA.

Note que siempre puedes volver a utilizar la API pura de JPA cuando quieras.

Iniciando el gestor de entidades JPA

Play iniciará automáticamente el gestor de entidades de Hibernate cuando encuentre una o más clases anotadas con la anotación @javax.persistence.Entity. No obstante, asegúrate de que hayas configurado correctamente la fuente de datos JDBC o fallará.

Obteniendo el gestor de entidades JPA

Una vez que se haya iniciado el gestor de entidades JPA, puedes obtenerlo desde el código de la aplicación usando el helper de JPA. Por ejemplo:

public static index() {
    Query query = JPA.em().createQuery("select * from Article");
    List<Article> articles = query.getResultList();
    render(articles);
}

Gestión de transacciones

Play gestionará automáticamente las transacciones por tí. Iniciará una transacción automáticamente la primera vez que efectúe una openración JPA en un pedido HTTP y la cerrará cuando se envíe la respuesta HTTP. Si el código arroja una excepción, la transacción hará rollback automáticamente.

Si necesitas forzar un rollback de la transacción desde el código fuente, puedes usar el método JPA.setRollbackOnly(), que le dice a JPA que no haga commit de la transacción actual.

También puedes usar anotaciones apra especificar cómo hay que gestionar las transacciones.

Si anotas el método en el controlador con @play.db.jpa.Transactional(readOnly=true), la transacción será de sólo lectura.

No hay necesidad de evitar que Play inicie una transacción si no desea hacer uso de ninguna. Si no realiza ninguna acción JPA mientras procesa el pedido HTTP, no se iniciará ninguna transacción. Si quiere forzar este comportamiento, puede anotar el método de su controlador, o la clase con la anotación @play.db.jpa.NoTransaction.

Si intenta realizar una acción JPA en un controlador anotado con @play.db.jpa.NoTransaction, una exceción será arrojada.

La clase de soporte play.db.jpa.Model

Esta es la clase helper principal para JPA. Si haces que una de tus entidades JPA extienda la clase play.db.jpa.Model obtendrás muchos métodos de ayuda para simplificar el acceso JPA.

Por ejemplo, veamos este objeto del modelo Post:

@Entity
public class Post extends Model {
    public String title;
    public String content;
    public Date postDate;
    
    @ManyToOne
    public Author author;
    
    @OneToMany
    public List<Comment> comments;
}

La clase play.db.jpa.Model automáticamente proporciona un campo Long id autogenerado. Creemos que es buena idea en general usar el Long id autogenerado como clave primaria para los modelos JPA (la clave primaria técnica) y gestionar la clave primaria funcional usando otro campo.

Hemos usado el hecho de que Play automáticamente considera a los miembros public de la clase Post como properties. Así que no tenemos que escribir los métodos setter/getter para este objeto.

Manejo personalizado de id con GenericModel

Nada te obliga a basar tus entidades en play.db.jpa.Model. Tus entidades JPA también pueden extender la clase play.db.jpa.GenericModel. Esto es lo que tienes que hacer si no quieres usar el Long id como clave primaria para tu entidad.

Por ejemplo, he aquí el código para una entidad User muy sencilla. El id es un UUID, las propiedades name y mail son obligatorias y usamos Play Validation para asegurar las reglas de negocio sencillas.

@Entity
public class User extends GenericModel {
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    public String id;
    
    @Required
    public String name;
    
    @Required
    @MaxSize(value=255, message = "email.maxsize")
    @play.data.validation.Email
    public String mail;
}

Busncado objetos

La clase play.db.jpa.Model te proporciona varios modos de consultar la información de los modelos. Por ejemplo:

Buscar por ID

El modo más sencillo de encontrar un objeto.

Post aPost = Post.findById(5L);

Traer todos

List<Post> posts = Post.findAll();

Esta es la forma más sencilla de recuperar todos los posts, pero puede hacerse lo mismo con:

List<Post> posts = Post.all().fetch();

Lo que permite paginar los resultados:

// los 100 primeros posts
List<Post> posts = Post.all().fetch(100);

o incluso,


// hasta un maximo de 100 posts empezando en el 50
List<Post> posts = Post.all().from(50).fetch(100);

Buscar usando una consulta simplificada

Esto te permite crear buscadores muy expresivos, pero sólo funcionarán para consultas sencillas.

Post.find("byTitle", "My first post").fetch();
Post.find("byTitleLike", "%hello%").fetch();
Post.find("byAuthorIsNull").fetch();
Post.find("byTitleLikeAndAuthor", "%hello%", connectedUser).fetch();

Las consultas sencillas siguen esta sintaxis: [Property][Comparator]And?, donde Comparator puede ser alguno de los siguientes:

  • LessThan - menor que el valor dado
  • LessThanEquals - menor o igual que el valor dado
  • GreaterThan - mayor que un valor dado
  • GreaterThanEquals - mayor o igual que un valor dado
  • Like - Equivalente a la expresión like de SQL, excepto que la propiedad siempre se convertirá a minúsculas.
  • Ilike - Similar a Like, excepto que no es sensitivo a minúsculas, lo que indica que también se convertirá el argumento a minúsculas.
  • Elike - Equivalente a la expresión like de SQL, sin conversión.
  • NotEqual - Niega la igualdad
  • Between - Entre dos valores (toma dos argumentos)
  • IsNotNull - No nulo (no requiere argumento)
  • IsNull - Es nulo (no requiere argumento)

Buscar usando una consulta JPQL

Puedes usar consultas JPQL:

Post.find(
    "select p from Post p, Comment c " +
    "where c.post = p and c.subject like ?", "%hop%"
);

o incluso una parte de ella:

Post.find("title", "My first post").fetch();
Post.find("title like ?", "%hello%").fetch();
Post.find("author is null").fetch();
Post.find("title like ? and author is null", "%hello%").fetch();
Post.find("title like ? and author is null order by postDate", "%hello%").fetch();

Incluso se puede especificar sólo el comando order by:

Post.find("order by postDate desc").fetch();

Contando objetos

Puedes fácilmente contar objetos.

long postCount = Post.count();

O contar usando una consulta:

long userPostCount = Post.count("author = ?", connectedUser);

Guardando los ficheros cargados con play.db.jpa.Blob

Puedes usar el tipo play.db.jpa.Blob para guardar los ficheros cargados (con upload) en el sistema de ficheros (no en la base de datos). En el servidor, Play guarda la imagen o fichero cargados en la carpeta attachments/, dentro de la aplicación. El nombre de fichero (un UUID) y el tipo MIME se guardan en un atributo de la base de datos cuyo tipo SQL es VARCHAR.

El caso de uso básico de cargar (upload), almacenar y servir un fichero es extremadamente fácil en Play. Esto es porque Play automáticamente asigna un fichero cargado desde un formulario HTML al modelo JPA, y porque Play incluye métodos que hacen que servir datos binarios sea tan sencillo como si fueran texto plano. Para guardar los ficheros cargados en el modelo, añade una propiedad del tipo play.db.jpa.Blob.

import play.db.jpa.Blob;
@Entity
public class User extends Model {
 
   public String name;
   public Blob photo;
}

Para cargar ficheros, añade un formulario a tu plantilla de la vista, y usa un control de carga de ficheros para la propiedad Blob del modelo:

#{form @addUser(), enctype:'multipart/form-data'}
   <input type="file" name="user.photo">
   <input type="submit" name="submit" value="Upload">
#{/form}

Luego, en el controlador, añade una acción que salve lo cargado en un nuevo objeto del modelo:

public static void addUser(User user) {
   user.save();
   index();
}

Este código no parece hacer otra cosa más que salvar la entidad JPA, porque la carga del fichero la gestiona automáticamente Play. Primero, antes de arrancar el método de acción, el fichero cargado se salva en una subcarpeta de tmp/uploads/. Luego, cuando se salva la entidad, el fichero se copia a la carpeta attachments/, con un UUID como nombre de fichero. Finalmente, cuando la acción se ha completado, se borra el fichero temporal.

Al cargar otro fichero para el mismo usuario, éste se salvará como un nuevo fichero en el servidor, cuyo nombre es un nuevo UUID. Esto significa que el fichero original estará ahora huérfano. Si no tienes espacio ilimitado en disco, tendrás que implementar tu propio esquema para limpiar estos archivos, quizás con un job asincrónico.

Si el pedido HTTP no especifica el tipo MIME correcto del fichero, puedes asignarlo mediante la extensión del nombre de fichero, como se describe en subiendo ficheros desde el controlador.

Para salvar los fichers adjuntos en una carpeta diferente, hay que configurar attachments.path.

Para servir un fichero guardado, se pasa Blob.get() al método renderBinary del Controlador, como se ve en respuesta binaria del controlador.

Salvado explícito

Hibernate mantiene una cache de objetos que han sido solicitados a la base de datos. Estos objetos se llaman objetos persistentes, y lo son mientras el EntityManager que se usó para recuperarlos esté activo. Esto quiere decir que los cambios a estos Objetos entre los límites de una transacción se persisten automáticamente cuando se finaliza la transacción. En el estándar JPA, estas actualizaciones son implícitas dentro de los límites de la transacción; no tienes que llamar explícitamente a ningún método para persistir los valores.

La principal desventaja de este enfoque, es que tenemos que gestionar todos nuestros objetos manualmente. En vez de decirle al EntityManager que actualice un objeto (lo que sería más intuitivo), tenemos que decirle qué objetos NO ha de actualizar. Lo hacemos llamando a la función refresh(), que esencialmente hace rollback sobre una sola entidad. Hacemos esto justo antes de llamar a commit en la transacción o cuando decidimos que el objeto no ha de ser actualizado.

Este es un caso de uso común, cuando se está editando un objeto persistente después de enviar el formulario:

public static void save(Long id) {
    User user = User.findById(id);
    user.edit("user", params.all());
    validation.valid(user);
    if(validation.hasErrors()) {
        // Tenemos que desechar explícitamente las modificaciones del usuario...
        user.refresh();
        edit(id);
    }
    show(id);
}

Según mi experiencia, la mayoría de los desarrolladores no tienen presente este comportamiento, y olvidan desechar el estado del objeto en caso de errores, asumiendo que el objeto no se salvará sin una llamada explícita a save().

Así que eso es exactamnte lo que hemos cambiado en Play. Todos los objetos persistentes que extienden JPASupport/JPAModel no se salvarán sin una llamada explícita al método save(). Así que podríamos reescribir el código anterior de esta forma:

public static void save(Long id) {
    User user = User.findById(id);
    user.edit("user", params.all());
    validation.valid(user);
    if(validation.hasErrors()) {
        edit(id);
    } else{
       user.save(); // salvado explicito aqui
       show(id);
    }
}

Esto es mucho más intuitivo. Además, como puede ser tedioso llamar a save() sobre un grafo grande de objetos, la llamada al método save() se hace automáticamente en cascada, grabando también los cambios en las entidades cuyas relaciones hayan sido anotadas con el atributo cascade=CascadeType.ALL.

Más sobre problemas genéricos de tipos

La clase play.db.jpa.Model define un conjunto de métodos genéricos. Estos métodos usan un parámetro type para especificar el tipo de retorno del método. Cuando se usan estos métodos, el tipo concreto a utilizar como valor de retorno se deriva a partir del contexto de la invocación mediante inferencia de tipos.

Por ejemplo, el método findAll se define como:

<T> List<T> findAll();

Y se usa de esta forma:

List<Post> posts = Post.findAll();

Aquí el compilador Java resuelve el tipo real de T mediante el hecho de que asignas el resultado del método a un List<Post>. Luego T se resuelve como de tipo Post.

Desafortunadamente, esto no funciona si el valor de retorno genérico del método se usa como parámetro para la invocación a otro método o en un bucle, así que el código siguiente fallará con un error del compilador que dice “Type mismatch: cannot convert from element type Object to Post” (Error de tipos: no puedo convertir de elemento de tipo Object a Post):

for(Post p : Post.findAll()) {
    p.delete();
}

Por supuesto, puede resolverse mediante una variable local temporal, como en :

List<Post> posts = Post.findAll(); // aqui sí funciona la inferencia de tipos
for(Post p : posts) {
    p.delete();
}

Pero hay una manera más simple. Puedes usar una característica de Java muy práctica pero poco conocida, que hace el código más corto y más legible a la vez:

for(Post p : Post.<Post>findAll()) {
    p.delete();
}

Es importante advertir que Play no soporta XA (commits en dos fases). Si usas diferentes configuraciones JPA en el mismo pedido HTTP, Play intentará hacer commit en tantas transacciones como pueda. Si el commit a la primera base de datos tiene éxito y el segundo commit a otra base de datos falla, el primer commit no podrá deshacerse (no podrá hacer rollback). Ten esto en cuenta cuando uses múltiples configuraciones JPA en el mismo request.

Próximos pasos

Ahora veremos algunas Bibliotecas de Play.