Exponiendo y consumiendo servicios de datos con mybatis, play framework 2.0 y jersey

Mucho tiempo sin  bloggear pero bueno hay que devolver un poco a la comunidad así que voy a escribir en esta ocasión sobre como exponer servicios de datos con play, utilizando mybatis como mapeador de datos y la librería Jersey para consumir los servicios expuestos.

El modelo de datos con el que vamos a trabajar es el siguiente:

Como ven en el modelo no hago uso de claves foráneas, ni PK compuestas, eso es una práctica (buena o mala) personal, así que no se alarmen si no ven un modelo ER típico.

Empezando con mybatis (anteriormente ibatis); es un mapeador de datos muy simple de utilizar y muy flexible; he sido fan de hibernate desde sus primeras versiones pero me he encontrado con la simplicidad de este framework que no te quita el control del SQL que escribes (algo que para algunos desarrolladores es importante), pero a su vez hace realmente fácil el mapeo entre las tablas y los objetos.

Hay varias opciones para el mapeo de datos con mybatis, en mi caso me gusta tener la conexión a la base de datos en XML y los mapeos con anotaciones (también se pueden hacer con XML al estilo de los .hbml), así que lo primero es crear un archivo de configuración, en este caso lo llamamos dbconfig.xml.

Ahora En el archivo de configuración dbconfig.xml, como estoy utilizando jdbc de SQL Server la configuracion va de la siguiente manera

<pre><configuration>
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
				<property name="url" value="jdbc:sqlserver://localhost\sql2008:51085;databaseName=dummy" />
				<property name="username" value="user" />
				<property name="password" value="secret" />
			</dataSource>
		</environment>
	</environments>
</configuration>

Tomando como ejemplo del modelo una de las clases:

public class Genre implements Serializable{

	private static final long serialVersionUID = -9130550406579029310L;
	private int id;
	private String name;

	public Genre(){
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return String.format("%s - %s", this.id, this.name);
	}

}

Debemos definir una interfaz, la cual tiene las sentencias SQL y los metodos con las anotaciones correspondientes para el mapeo de los datos entre los objetos y la base de datos, los mapeos son para cualquier operación (Select, Update, Delete); para nuestro ejemplo vamos solo a hacer uso de Selects; de la clase Genre vamos a definir 2 métodos: Uno para consultar todos los registros de la base y otro para hacer una consulta por ID

public interface IGenreMapper {
	final String SELECT = "select id,name from genre";
	final String SELECT_BY_ID = "select id,name from genre where id=#{id}";

	@Select(SELECT)
	@Results(value = {
			@Result(column = "id", property = "id", javaType = Integer.class),
			@Result(column = "name", property = "name", javaType= String.class) })
	public List selectAll();

	@Select(SELECT_BY_ID)
	@Results(value = {
			@Result(column = "id", property = "id", javaType = Integer.class),
			@Result(column = "name", property = "name", javaType= String.class) })
	public Genre selectByID(@Param("id") int id);
}

Como se puede ver sin mucho esfuerzo es que sobre los métodos está definida la anotacion @Select que hace uso de las sentencias SQL que tenemos como Strings, y que de esas columnas seleccionadas tenemos el mapeo hacia que propiedades de la clase Genre corresponde cada una; y cuando estamos introduciendo parámetros #{nombre_párametro} en el comando sql como el caso del SELECT_BY_ID los podemos relacionar fácilmente con la anotación @Param con los párametros que se reciben en el método.

Una vez creada la interfaz hay que crear la clase que la implemente ya que ese es el objeto que finalmente se va a instanciar para hacer las consultas; la implementación es poca cosa, es simplemente invocar a una sesion de mybatis que nos devuelve un mapeador para que  internamente haga uso de lo definido en la interfaz de mapeo y obtener los resultados de la ejecución.

public class GenreDAO extends BaseDAO implements IGenreMapper{

public GenreDAO(SqlSession session){
		super(session);
	}

	@Override
	public List selectAll() {
		List result = session.getMapper(IGenreMapper.class).selectAll();
		closeSession();
		return result;
	}

	@Override
	public Genre selectByID(int id) {
		Genre result = session.getMapper(IGenreMapper.class).selectByID(id);
		closeSession();
		return result;
	}
}

Hay ciertos detalle en la clase mostrada, como por ejemplo heredar de un BaseDAO que tiene un constructor que recibe un objeto que implemente SqlSession de mybatis pero ya eso es como lo desees implementar, para mayor detalle pueden descargar el fuente relacionado al post en el link que se encuentra al final para que vean la implementación completa.

Algo que si voy a detallar es el caso cuando tenemos una propiedad compleja en nuestra clase; por ejemplo la clase Song tiene las propiedades Album y Genre que son del tipo de las clases Album y Genre respectivamente:

public class Song implements Serializable{

	private static final long serialVersionUID = -3728048783934199398L;
	private int id;
	private String name;
	private String duration;
	private Album album;
	private Genre genre;
        .
        .
        .
}

Así que para cargar las propiedades, en nuestra interface de mapeo utilizamos la anotación @One que nos permite llenar una propiedad compleja a partir de un método select que devuelva ese tipo de objeto. Como se mostro unas líneas arriba, el mapeador IGenreMapper tiene un método selectByID, del cual vamos a hacer uso en el mapeador Song para llenar la propiedad Genre, pasandole el id para que realice la consulta:

public interface ISongMapper {
	final String SELECT = "select id,album,name,duration,genre from song";

	@Select(SELECT)
	@Results( value= {
			@Result(column="id", property="id"),
			@Result(column="album", property="album", one=@One(select="com.musicservices.datamodel.mapper.IAlbumMapper.selectByID"),javaType=Album.class),
			@Result(column="name", property="name"),
			@Result(column="duration", property="duration"),
			@Result(column="genre", property="genre", one=@One(select="com.musicservices.datamodel.mapper.IGenreMapper.selectByID"), javaType=Genre.class)
	})
	public List selectAll();

}

La misma estrategia se sigue para cargar la propiedad Album, de esta manera el mapeo es completo desde nuestra base de datos relacional hacia el modelo de objetos aunque tengamos propiedades complejas.
Adicionalmente he creado una clase DataManager en la cual están las líneas donde se instancian los objetos de mybatis y el acceso a todos mis objetos DAO para centralizar el llamado a las clases, nos vamos a centrar en el código para instanciar el objeto SqlSessionFactory que es la interfaz entre nuestros objetos y la base de datos:

private DataManager(){
		if(sessionFactory == null){
			try {
				Reader reader = Resources.getResourceAsReader(DataManager.class.getClassLoader(),"dbconfig.xml");
				sessionFactory = new SqlSessionFactoryBuilder().build(reader);
				sessionFactory.getConfiguration().addMapper(IGenreMapper.class);
				sessionFactory.getConfiguration().addMapper(IAlbumMapper.class);
				sessionFactory.getConfiguration().addMapper(ISongMapper.class);
			} catch (IOException e) {
				e.printStackTrace();
			}

		}
	}

Aquí cargamos el archivo dbconfig.xml para poder crear la sesión de mybatis a través de la clase SessionFactoryBuilder, y luego de eso registramos todas las interfaces de mapeo que hayamos creado, las cuales le sirven a mybatis para hacer su trabajo, ya que como vimos en la implementación de las interfaces hacemos uso del metodo getMapper para hacer referencia a los mapeadores registrados.

Una vez con la capa de datos lista exportamos los binarios como un .jar y procedemos a exponerla como servicios Rest a traves del framework play, lo primero es descargar los binarios desde playframework.org descomprimirlo en un directorio (recomendado sin espacios en los nombres de las carpetas) y con el comando play new [NombreAplicacion] crear la estructura de un proyecto Java:

Para mayor comodidad y modificar los archivos creados por play podemos importarlo como un proyecto para Eclipse, para esto es recomendable agregar la ruta de instalación de play a las variables de ambiente para ubicarnos dentro del directorio de nuestra aplicación y ejecutar los comandos play y luego eclipsify, y luego de eso importamos el proyecto dentro de nuestro workspace de Eclipse (el paso es opcional ya que puedes editar los archivos desde cualquier editor de texto)


Navegando dentro de los directorios de nuestra aplicación play nos vamos a encontrar con las carpetas app y conf, que son las que vamos a utilizar para el ejemplo ya que solo vamos a hacer uso del framework para exponer servicios de datos, no vamos a abarcar toda la funcionalidad, solamente el enrutamiento por url hacia los controladores que van a hacer uso de nuestro modelo de datos para realizar las consultas a la base de datos.

Empezamos creando una carpeta lib en el proyecto de play y agregamos todas las dependencias de mybatis, jdbc, y el .jar creado con nuestro modelo de datos.

Ahora sí, lo primero es dentro de /app/controllers creamos un nuevo controlador Album.java que hereda de  play.mvc.controller y le agregamos un método estático public static Result all(), esta nueva notación es parte del nuevo play 2.0, en resumen vamos a crear un método que controle todos los requests realizados por HTTP GET a la ruta /album y los devuelva en formato Json, finalmente el código con todas las consideraciones antes mencionadas queda así:

public class Album extends Controller{

	@play.mvc.BodyParser.Of(Json.class)
	public static Result all(){
		ObjectMapper mapper = new ObjectMapper();
		String json = null;
		try {
			List<com.musicservices.datamodel.entities.Album> albumList = DataManager.Instance().albumDao().selectAll();
			json = mapper.writeValueAsString(albumList);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ok(json);
	}
}

Una vez que tenemos el controlador debemos decirle a play cual es la url de la que este método del controlador se va a hacer responsable, modificamos el archivo /conf/routes y agregamos la línea para indicarle que todo lo que pase por /album y sea un HTTP GET va a ser manejado por nuestro controlador:

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.Application.index()
GET	    /album                      controllers.Album.all()

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.at(path="/public", file)

Listo, ahora procedemos a levantar nuestra aplicación en play con el comando run

*En dias previos a la redaccion de este post aparecio la versión 2.0 de play y al tratar de referenciar al archivo xml de configuración de mybatis que se encuentra dentro del jar da un error de classpath, asi que como workaround deben copiar el archivo dbConfig.xml dentro de su aplicación play en la ruta [AplicacionPlay]\target\scala-2.9.1\classes 

Una vez que tenemos todo listo accedemos por URL a localhost:9000/album y vemos el resultado es un string con notación Json de la colección de resultados obtenidos:


Bajo el mismo esquema creamos el controlador para hacer consultas de Albumes por código, donde ahora el método debe recibir un parámetro, y llamamos al metodo selectByID del mapeador:

@play.mvc.BodyParser.Of(Json.class)
	public static Result getByID(Integer id){
		ObjectMapper mapper = new ObjectMapper();
		String json = null;
		try {
			com.musicservices.datamodel.entities.Album album = DataManager.Instance().albumDao().selectByID(id.intValue());
			json = mapper.writeValueAsString(album);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ok(json);
	}

Y en la configuración de rutas debemos indicar el mapeo hacia el nuevo metodo controlador de la siguiente manera:

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.Application.index()
GET	    /album                      controllers.Album.all()
GET	    /album/:id                 controllers.Album.getByID(id: Integer)

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.at(path="/public", file)

Accedemos nuevamente por url a la ruta /album/{id} donde {id} en este caso es el valor de la columna id en la base de datos por la cual estamos consultando en al tabla Album, los resultados son los siguientes:

Y así podemos replicar el mismo esquema para todos los controladores que deseemos publicar. Ahora para consumir estos servicios Json vamos a hacer un simple cliente Java que haciendo uso de la libreria Jersey va a obtener via HTTP GET los resultados de la consulta y con la librería Gson de google vamos a regresarlos a su estado inicial de una Lista de objetos.

public static void main(String[] args) throws UnsupportedEncodingException{

		String albumName = URLEncoder.encode("Mane Attraction","UTF-8");
		String url = String.format("http://localhost:9000/song/album/%s",albumName);

		Client client = Client.create();
		WebResource res = client.resource(url);
		String jsonData = res.get(String.class);

		List<Song> songs = new ArrayList<>();
		Type listType = TypeToken.get(new TypeToken<Collection<Song>>(){}.getType()).getType();
		songs = new Gson().fromJson(jsonData, listType);
		for(Song s: songs){
			System.out.println(s);
		}
	}

En las primeras 2 líneas armamos la url al servicio expuesto de play, utilizando la clase URLEncoder ya que si no lo hacemos los espacios o cualquier caracter especial no se va a formatear correctamente, luego haciendo uso de las clases de Jersey generamos un WebResource de la url le hacemos un HTTP Get sólo con invocar el método del mismo nombre que nos devuelve el Json generado por nuestro servidor Rest; luego de esto tenemos que transformar este resultado en objetos manejables para java; por lo que hacemos uso de Gson, al cual tenemos que informarle el tipo de objeto al que debe transformar el String que le vamos a pasar, al pasar el tipo de la instancia de TypeToken<Collection<Song>> le indicamos que queremos transformar el Json a una lista de tipo Song, invocamos el metodo fromJson y listo estamos consumiendo ya los servicios expuestos de nuestro modelo de datos.
Los Rest services son una alternativa a los tradicionales Web Services, con play es tan simple como ya lo hemos visto; la flexibilidad de mybatis para mapear datos es fantastica y que podemos decir de las apis de Jersy y Gson que con no más de 9 líneas de código nos encapsulan una capa de comunicación y transformación de datos que debemos tener en cuenta y bajo nuestra amplia gama de frameworks para dar soluciones tecnológicas.

Saludos,
gish@c

 

Descargar fuentes