Mostrando entradas con la etiqueta JPA. Mostrar todas las entradas
Mostrando entradas con la etiqueta JPA. Mostrar todas las entradas

jueves, 31 de marzo de 2011

Conectar 2 bases de datos con JPA

Leyendo sobre el tema he visto que esto es algo tan sencillo como duplicar los beans entityManager, entityManagerFactory y datasource con la información de conexión a la base de datos nueva. También habría que generar un nuevo persistence-unit en el fichero persistence.xml.

Después en la aplicación, suponiendo que inyectamos el EntityManager a nuestros DAOs mediante la anotación @PersistenceContext, a cada uno de ellos habría que incluirle el atributo unitName especificado en el persistence.xml:

@PersistenceContext(unitName="defaultDB")

En mi caso esto no me ha valido porque en el momento de tener que conectar otra base de datos a mi aplicación, ésta importaba una serie de librerías comunes que utilizamos para más proyectos en los que había objetos DAO con un @PersistenceContext sin especificar ningún unitName. Cambiar la librería común para que especifira el unitName es inviable ya que en cada aplicación en la que se utiliza su valor es diferente.

Al arrancar la aplicación sin hacer nada la excepción que aparece es:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [javax.persistence.EntityManagerFactory] is defined: expected single bean but found 2
at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor.findDefaultEntityManagerFactory(PersistenceAnnotationBeanPostProcessor.java:536)

Esta claro. El DAO que tiene el @PersistenceContext no sabe qué entityManager tiene que recibir y no podemos modificarlo para especificárselo.

En la excepción se puede ver que el error ocurre en el método findDefaultEntityManagerFactory de la clase PersistenceAnnotationBeanPostProcessor de Spring. Es el encargado de inyectar el entityManager a los @PersistenceContext.

Me he creado una clase que hereda de PersistenceAnnotationBeanPostProcessor y sobreescribe el método findDefaultEntityManagerFactory de tal manera que voy a devolver el entityManagerFactory que yo quiera. La clase en cuestión quedaría así:



public class PersistenceAnnotationBeanPostProcessor extends org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor{

private EntityManagerFactory defaultEntityManagerFactory;

protected EntityManagerFactory findDefaultEntityManagerFactory(String requestingBeanName)
throws NoSuchBeanDefinitionException{
if (defaultEntityManagerFactory!=null){
return defaultEntityManagerFactory;
}
return super.findDefaultEntityManagerFactory(requestingBeanName);
}

public void setDefaultEntityManagerFactory(
EntityManagerFactory defaultEntityManagerFactory) {
this.defaultEntityManagerFactory = defaultEntityManagerFactory;
}

}



Lo único que tendríamos hacer a continuación es definir un bean en la configuración de Spring con esta clase en vez de la que utiliza Spring por defecto:









Eso sí, tenemos que especificar en el atributo "defaultEntityManagerFactory" qué entityManager queremos que se inyecte cuando nos encontramos con un @PersistenceContext sin especificar un unitName.

La conexión a la segunda base de datos yo sólo la he utilizado para realizar consultas sobre ella, nunca para realizar modificaciones. No sé si habría algún fallo en las transacciones, ya que el JpaTransactionManager está ligado únicamente a un EntityManagerFactory.

lunes, 6 de diciembre de 2010

Vistas de Bases de datos con JPA

Una de las cosas que más me gusta de JPA es que una vez que desplegamos nuestra aplicación web no tenemos que preocuparnos de la creación de las tablas en la base de datos ni de los tipos de los campos.

Si tenemos que trabajar con vistas en base de datos, JPA las trata de la misma forma que si fueran tablas. Con que pongamos la anotación @Entity encima de una clase y se le de el nombre de una vista, se trabajaría igual que con cualquier tabla. La única diferencia es que no se pueden realizar operaciones de escritura (inserts y updates).

Si tenemos configurado JPA para que automáticamente genere todas las tablas, tenemos un pequeño problema, ya que para crear la vista, primero tendríamos que arrancar la aplicación web y que se creen todas las tablas necesarias. En ese momento también se creará una con el nombre de la vista que nosotros queremos crear, ya que como hemos dicho antes, le hemos añadido la anotación @Entity como al resto de entidades.

Esta tabla no tiene ningún sentido porque nunca vamos a meter datos en ella. Lo que tendremos que hacer es eliminarla y generar la vista manualmente después del primer arranque.

No sé si será lo más óptimo posible o si hay alguna otra posibilidad que yo desconozca, pero se me ocurrió una forma de poder añadir un listener en nuestra aplicación web para que no sea necesario tener que realizar todos estos pasos y que en el momento de arrancar la aplicación, se realicen automáticamente y no tengamos que preocuparnos en adelante por la creación manual de las vistas.

La idea es añadir a nuestra aplicación un Listener que se encargue de realizar los siguientes pasos:

- Mirar si existe la tabla con el nombre de la vista que queremos crear. En caso de que exista (Esto ocurriría únicamente la primera vez que arranca la aplicación web) se borrará.
- Si la vista existe (Esto ocurriría en los posteriores arranques de la aplicación), ésta se borrará para que si ha habido algún cambio de versión en la aplicación que requiere añadir, suprimir o modificar algún campo de la vista, no sea necesaria ninguna intervención manual.
- La query de la vista estará harcodeada en el listener, y lo que se hará después será crearla.

Este listener necesita que todas las tablas estén ya creadas, por lo que se tendrá que ejecutar siempre después del de Spring, que es el que se encarga de cargar JPA. Por ello, es importante que no se nos olvide ponerlo siempre después del ContextLoaderListener en el fichero web.xml


.....


org.springframework.web.context.ContextLoaderListener




RUTA DEL LISTENER

.....


El código del listener lo pongo a continuación. Si alguien lo quiere reutilizar que tenga en cuenta que la forma en la que he obtenido por código una referencia al DataSource de la base de datos posiblemente será diferente, ya que yo he aprovechado que el DataSource lo tengo definido como un Bean de Spring. Otra persona igual tiene que recuperarlo directamente mediante JNDI.



public class DBViewsCreator implements ServletContextListener
{
private static final Log log = LogFactory.getLog(DBViewsCreator.class);
private static final String TESTVIEW_NAME = "vplayer";
private static final String TESTVIEW_QUERY = "Select * from player p where p.active = true";

public void contextInitialized(ServletContextEvent servletContextEvent)
{
log.info("Creating DB views");
Connection conn = null;
try {
DataSource ds = getDatasource();
conn = ds.getConnection();
DatabaseMetaData md = conn.getMetaData();
DatabaseUtils.recreateView (md, TESTVIEW_NAME, TESTVIEW_QUERY, conn);
} catch (Exception e) {
log.error("Error during Database Views creation",e);
} finally {
try {
if (conn != null) conn.close(); }
catch (SQLException e) {
log.error("Error closing database connection",e);
}
}
}

private DataSource getDatasource (){
//Obtenemos el datasource de donde nos venga mejor, ya sea mediante JNDI o accediendo a un bean de Spring.
// En mi caso he utilizado la clase AppContext para recuperar el bean con nombre "dataSource". Explicada en
// http://blog.jdevelop.eu/2008/07/06/access-the-spring-applicationcontext-from-everywhere-in-your-application/
DataSource ds = (Datasource)AppContext.getApplicationContext().getBean ("dataSource");
return ds;
}

public void recreateView (DatabaseMetaData md, String viewName, String query, Connection conn) throws SQLException{
deleteView(md, viewName, query, conn);
createView(md, viewName, query, conn);
}

public void deleteView (DatabaseMetaData md, String viewName, String query, Connection conn) throws SQLException{
//primero comprobamos si existe la definición con el nombre de la vista, ya sea tabla o vista.
if (existsQuartzSchema(md, viewName)){
//Si existe la borramos y recreamos para tenerla siempre actualizada.
Statement stmt = conn.createStatement();
try {
//La primera vez que se arranque la aplicación, la siguiente instrucción dará error
// porque no existe la vista.
stmt.execute("DROP VIEW "+viewName);
} catch (SQLException e) {
try {
stmt.execute("DROP TABLE "+viewName);
} catch (SQLException e1) {
log.error("Error droping table "+viewName, e1);
throw e;
}
} finally{
if (stmt != null) stmt.close();
}
}
}

public static void createView (DatabaseMetaData md, String viewName, String query, Connection conn) throws SQLException{
//Creamos la vista.
Statement stmt = conn.createStatement();
try{
stmt.execute("CREATE VIEW "+viewName+" AS ("+query+")");
log.info("view "+viewName+" created successfully");
} catch (SQLException e) {
log.error("Error creating view "+viewName+" with query: "+query, e);
throw e;
} finally{
if (stmt != null) stmt.close();
}
}

public void contextDestroyed(ServletContextEvent servletContextEvent) {}
}


Espero que os sirva de ayuda, y si a alguien se le ocurre una idea mejor de cómo integrar las vistas en JPA que lo comente por favor.