2010-06-14 3 views
38

Pour un défilement de réponse jusqu'à la fin de cette ...Hibernate/Spring: Échec de l'initialisation paresseusement - aucune session ou session fermée

Le problème de base est le même que le temps multiple demandé. J'ai un programme simple avec deux POJOs Event et User - où un utilisateur peut avoir plusieurs événements.

@Entity 
@Table 
public class Event { 
private Long id; 
private String name; 
private User user; 

@Column 
@Id 
@GeneratedValue 
public Long getId() {return id;} 
public void setId(Long id) { this.id = id; } 

@Column 
public String getName() {return name;} 
public void setName(String name) {this.name = name;} 

@ManyToOne 
@JoinColumn(name="user_id") 
public User getUser() {return user;} 
public void setUser(User user) {this.user = user;} 

} 

L'Utilisateur:

@Entity 
@Table 
public class User { 
private Long id; 
private String name; 
private List<Event> events; 

@Column 
@Id 
@GeneratedValue 
public Long getId() { return id; } 
public void setId(Long id) { this.id = id; } 

@Column 
public String getName() { return name; } 
public void setName(String name) { this.name = name; } 

@OneToMany(mappedBy="user", fetch=FetchType.LAZY) 
public List<Event> getEvents() { return events; } 
public void setEvents(List<Event> events) { this.events = events; } 

} 

Note: Ceci est un exemple de projet. Je vraiment voulez utiliser Lazy fetching ici.

Maintenant, nous devons configurer le printemps et mise en veille prolongée et un simple db.xml de base pour le chargement:

 

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xmlns:aop="http://www.springframework.org/schema/aop" 
     xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
      http://www.springframework.org/schema/aop 
      http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> 


<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" 
    destroy-method="close" scope="thread"> 
    <property name="driverClassName" value="com.mysql.jdbc.Driver" /> 
    <property name="url" value="jdbc:mysql://192.168.1.34:3306/hibernateTest" /> 
    <property name="username" value="root" /> 
    <property name="password" value="" /> 
    <aop:scoped-proxy/> 
</bean> 

<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> 
    <property name="scopes"> 
    <map> 
    <entry key="thread"> 
    <bean class="org.springframework.context.support.SimpleThreadScope" /> 
    </entry> 
    </map> 
    </property> 
</bean> 

<bean id="mySessionFactory" 
    class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" scope="thread"> 
    <property name="dataSource" ref="myDataSource" /> 
    <property name="annotatedClasses"> 
    <list> 
    <value>data.model.User</value> 
    <value>data.model.Event</value> 
    </list> 
    </property> 
    <property name="hibernateProperties"> 
    <props> 
    <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop> 
    <prop key="hibernate.show_sql">true</prop> 
    <prop key="hibernate.hbm2ddl.auto">create</prop> 
    </props> 
    </property> 
    <aop:scoped-proxy/> 

</bean> 

<bean id="myUserDAO" class="data.dao.impl.UserDaoImpl"> 
    <property name="sessionFactory" ref="mySessionFactory" /> 
</bean> 

<bean id="myEventDAO" class="data.dao.impl.EventDaoImpl"> 
    <property name="sessionFactory" ref="mySessionFactory" /> 
</bean> 

</beans> 
 

Note: J'ai joué avec le CustomScopeConfigurer et SimpleThreadScope, mais cela ne change rien.

J'ai simple dao-impl (seulement coller le userDao - le EventDao est à peu près la même chose - sauf avec la fonction "listWith":

 

public class UserDaoImpl implements UserDao{ 

private HibernateTemplate hibernateTemplate; 

public void setSessionFactory(SessionFactory sessionFactory) { 
    this.hibernateTemplate = new HibernateTemplate(sessionFactory); 

} 

@SuppressWarnings("unchecked") 
@Override 
public List listUser() { 
    return hibernateTemplate.find("from User"); 
} 

@Override 
public void saveUser(User user) { 
    hibernateTemplate.saveOrUpdate(user); 

} 

@Override 
public List listUserWithEvent() { 

    List users = hibernateTemplate.find("from User"); 
    for (User user : users) { 
    System.out.println("LIST : " + user.getName() + ":"); 
    user.getEvents().size(); 
    } 
    return users; 
} 

} 
 

Je reçois le org.hibernate.LazyInitializationException - Impossible d'initialiser paresseusement une collection de rôle: data.model.User.events, aucune session ou séance est levée à la ligne avec user.getEvents() taille();

et last but not least ici. est la classe de test que j'utilise:

 

public class HibernateTest { 

public static void main(String[] args) { 

    ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("basic-db.xml"); 


    UserDao udao = (UserDao) ac.getBean("myUserDAO"); 
    EventDao edao = (EventDao) ac.getBean("myEventDAO"); 


    System.out.println("New user..."); 
    User user = new User(); 
    user.setName("test"); 

    Event event1 = new Event(); 
    event1.setName("Birthday1"); 
    event1.setUser(user); 

    Event event2 = new Event(); 
    event2.setName("Birthday2"); 
    event2.setUser(user); 

    udao.saveUser(user); 
    edao.saveEvent(event1); 
    edao.saveEvent(event2); 

    List users = udao.listUserWithEvent(); 
    System.out.println("Events for users"); 
    for (User u : users) { 

    System.out.println(u.getId() + ":" + u.getName() + " --"); 
    for (Event e : u.getEvents()) 
    { 
    System.out.println("\t" + e.getId() + ":" + e.getName()); 
    } 
    } 

    ((ConfigurableApplicationContext)ac).close(); 
} 

} 
 

et voici l'exception:

 
1621 [main] ERROR org.hibernate.LazyInitializationException - failed to lazily initialize a collection of role: data.model.User.events, no session or session was closed 
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: data.model.User.events, no session or session was closed 
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:380) 
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:372) 
at org.hibernate.collection.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:119) 
at org.hibernate.collection.PersistentBag.size(PersistentBag.java:248) 
at data.dao.impl.UserDaoImpl.listUserWithEvent(UserDaoImpl.java:38) 
at HibernateTest.main(HibernateTest.java:44) 
Exception in thread "main" org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: data.model.User.events, no session or session was closed 
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:380) 
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:372) 
at org.hibernate.collection.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:119) 
at org.hibernate.collection.PersistentBag.size(PersistentBag.java:248) 
at data.dao.impl.UserDaoImpl.listUserWithEvent(UserDaoImpl.java:38) 
at HibernateTest.main(HibernateTest.java:44) 

Les choses ont essayé mais ne fonctionnent pas:

  • attribuer un threadScope et en utilisant BeanFactory (je "demande" ou "fil" - pas de différence remarqué):
 
    // scope stuff 
    Scope threadScope = new SimpleThreadScope(); 
    ConfigurableListableBeanFactory beanFactory = ac.getBeanFactory(); 
    beanFactory.registerScope("request", threadScope); 
    ac.refresh(); 
... 
  • Mise en place d'une transaction en obtenant l'objet de la session du deo:
 
... 
    Transaction tx = ((UserDaoImpl)udao).getSession().beginTransaction(); 
    tx.begin(); 
    users = udao.listUserWithEvent(); 
... 
  • obtenir une transaction dans le listUserWithEvent()
 
public List listUserWithEvent() { 
    SessionFactory sf = hibernateTemplate.getSessionFactory(); 
    Session s = sf.openSession(); 
    Transaction tx = s.beginTransaction(); 
    tx.begin(); 

    List users = hibernateTemplate.find("from User"); 
    for (User user : users) { 
    System.out.println("LIST : " + user.getName() + ":"); 
    user.getEvents().size(); 
    } 
    tx.commit(); 
    return users; 
} 

Je suis vraiment d'idées maintenant . En outre, l'utilisation de listUser ou listEvent fonctionne correctement.

Un pas en avant:

Merci à Thierry je suis arrivé un peu plus loin (je pense). J'ai créé la classe MyTransaction et je fais tout mon travail là-bas, obtenant tout à partir du printemps.Les nouveaux looks principaux comme ceci:

 

public static void main(String[] args) { 

    ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("basic-db.xml"); 

    // getting dao 
    UserDao udao = (UserDao) ac.getBean("myUserDAO"); 
    EventDao edao = (EventDao) ac.getBean("myEventDAO"); 

    // gettting transaction template 
    TransactionTemplate transactionTemplate = (TransactionTemplate) ac.getBean("transactionTemplate"); 

    MyTransaction mt = new MyTransaction(udao, edao); 
    transactionTemplate.execute(mt); 

    ((ConfigurableApplicationContext)ac).close(); 
} 
 

Malheureusement maintenant, il y a une exception de pointeur NULL @:. User.getEvents() taille(); (dans le daoImpl). Je sais qu'il ne devrait pas être nul (ni de la sortie dans la console ni de la mise en page db).

Voici la sortie de la console pour plus d'informations (je l'ai fait un chèque de user.getEvent() == null et imprimé "EVENT est NULL"):

 
New user... 
Hibernate: insert into User (name) values (?) 
Hibernate: insert into User (name) values (?) 
Hibernate: insert into Event (name, user_id) values (?, ?) 
Hibernate: insert into Event (name, user_id) values (?, ?) 
Hibernate: insert into Event (name, user_id) values (?, ?) 
List users: 
Hibernate: select user0_.id as id0_, user0_.name as name0_ from User user0_ 
1:User1 
2:User2 
List events: 
Hibernate: select event0_.id as id1_, event0_.name as name1_, event0_.user_id as user3_1_ from Event event0_ 
1:Birthday1 for 1:User1 
2:Birthday2 for 1:User1 
3:Wedding for 2:User2 
Hibernate: select user0_.id as id0_, user0_.name as name0_ from User user0_ 
Events for users 
1:User1 -- 
EVENT is NULL 
2:User2 -- 
EVENT is NULL 

Vous pouvez obtenir l'exemple de projet de http://www.gargan.org/code/hibernate-test1.tgz (il est une éclipse/projet Maven)

La solution (pour les applications console)

il y a en fait deux solutions à ce problème - en fonction de votre environnement:

Pour une application console vous avez besoin d'un modèle de transaction qui capture la logique db actutal et prend soin de la transaction:

 

public class UserGetTransaction implements TransactionCallback{ 

public List users; 

protected ApplicationContext context; 

public UserGetTransaction (ApplicationContext context) { 
    this.context = context; 
} 

@Override 
public Boolean doInTransaction(TransactionStatus arg0) { 
    UserDao udao = (UserDao) ac.getBean("myUserDAO"); 
    users = udao.listUserWithEvent(); 
    return null; 
} 

} 
 

Vous pouvez utiliser en appelant:

 

TransactionTemplate transactionTemplate = (TransactionTemplate) context.getBean("transactionTemplate"); 
UserGetTransaction mt = new UserGetTransaction(context); 
transactionTemplate.execute(mt); 
 

Pour Pour travailler, vous devez définir la classe de modèle pour le printemps (ie. dans votre-db.xml de base):

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> 
    <property name="transactionManager" ref="transactionManager"/> 
</bean> 

Une autre solution

merci andi

PlatformTransactionManager transactionManager = (PlatformTransactionManager) applicationContext.getBean("transactionManager"); 
    DefaultTransactionAttribute transactionAttribute = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED); 

transactionAttribute.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); 
    TransactionStatus status = transactionManager.getTransaction(transactionAttribute); 
    boolean success = false; 
    try { 
     new UserDataAccessCode().execute(); 
     success = true; 
    } finally { 
     if (success) { 
     transactionManager.commit(status); 
     } else { 
     transactionManager.rollback(status); 
     } 
    } 

(possible) La solution (pour les servlets)

servlets pas ce gros problème. Lorsque vous avez un servlet, vous pouvez simplement commencer et lier une transaction au début de votre fonction et délie à nouveau à la fin:

public void doGet(...) { 
    SessionFactory sessionFactory = (SessionFactory) context.getBean("sessionFactory"); 
    Session session = SessionFactoryUtils.getSession(sessionFactory, true); 
    TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session)); 

// Your code.... 

    TransactionSynchronizationManager.unbindResource(sessionFactory); 
} 

Répondre

26

Je pense que vous ne devriez pas utiliser la session de mise en veille prolongée des méthodes transactionnelles, mais laisser le printemps faire cette.

Ajouter ceci à votre printemps conf:

<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> 
    <property name="sessionFactory" ref="mySessionFactory" /> 
</bean> 

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> 
    <property name="transactionManager" ref="txManager"/> 
</bean> 

puis je modifier votre méthode d'essai pour utiliser le modèle de transaction de printemps: comme une note de côté

public static void main(String[] args) { 
    // init here (getting dao and transaction template) 

    transactionTemplate.execute(new TransactionCallback() { 
     @Override 
     public Object doInTransaction(TransactionStatus status) { 
      // do your hibernate stuff in here : call save, list method, etc 
     } 
    } 
} 

, les associations @OneToMany sont paresseux par défaut, vous n'avez donc pas besoin de l'annoter paresseux.(@ * ToMany sont LAZY par défaut, @ * ToOne sont EAGER par défaut)

EDIT: voici maintenant ce qui se passe du point de vue de la mise en veille prolongée:

  • séance ouverte (avec début de transaction)
  • enregistrer un utilisateur et le conserver dans la session (voir le cache de session en tant hashmap entité où la clé est l'identifiant de l'entité)
  • enregistrer un événement et le garder dans la session
  • sauver un autre événement et le garder dans la session
  • ... même avec toutes les opérations de sauvegarde puis ...

  • charger tous les utilisateurs (le « auprès des utilisateurs » requête)

  • à cette mise en veille prolongée point, voir qu'il a déjà l'objet dans sa session, Débarrassez-vous de celui qu'il a reçu de la demande et renvoyez celui de la session.
  • Votre session de l'utilisateur de la session n'a pas sa collection d'événements initialisée, vous obtenez donc null.
  • ...

Voici quelques points pour améliorer votre code:

  • dans votre modèle, lors de la commande de collecte n'est pas nécessaire, utilisez Set, pas la liste pour vos collections (événements Set privé , pas des événements privés Liste)
  • dans votre modèle, le type de vos collections, sinon mise en veille prolongée ne sera pas quelle entité chercher (Set < événements événement > privé)
  • lorsque vous définissez un côté d'un bidire relation ctional, et vous souhaitez utiliser le côté mappedBy de la relation dans la même transaction, réglez les deux côtés. Hibernate ne le fera pas pour vous avant le prochain tx (quand la session est une nouvelle vue de l'état db).

Donc, pour remédier au point ci-dessus, soit faire la sauvegarde dans une transaction, et le chargement dans un autre:

public static void main(String[] args) { 
    // init here (getting dao and transaction template) 
    transactionTemplate.execute(new TransactionCallback() { 
     @Override 
     public Object doInTransaction(TransactionStatus status) { 
      // save here 
     } 
    } 

    transactionTemplate.execute(new TransactionCallback() { 
     @Override 
     public Object doInTransaction(TransactionStatus status) { 
      // list here 
     } 
    } 
} 

ou un ensemble des deux côtés:

... 
event1.setUser(user); 
... 
event2.setUser(user); 
... 
user.setEvents(Arrays.asList(event1,event2)); 
... 

(également faire n'oubliez pas d'adresser les points d'amélioration de code ci-dessus, Set not List, collection tapant)

+1

J'ai essayé votre solution, et je me suis débarrassé du problème de Lazy - malheureusement, il ne charge aucun événement: user.getEvents() Produit un pointeur nul (bien que je puisse voir dans le db et depuis le premier itération que l'utilisateur a des événements associés). Est-il possible que hibernateTemplate.find doe snot résolve les dépendances? – Niko

+0

J'ai téléchargé mon test-projet avec votre suggestion déjà en place. peut-être que vous pouvez découvrir ce qui se passe. – Niko

3

Le problème est que votre dao utilise une session d'hibernation mais le l La charge azy de user.getName (je suppose que c'est là où elle se produit) se passe en dehors de cette session - soit pas du tout dans une session, soit dans une autre session. Typiquement, nous ouvrons une session d'hibernation avant nous faisons des appels DAO et ne le ferme pas jusqu'à ce que nous ayons fini avec toutes les charges paresseuses. Les requêtes Web sont généralement enveloppées dans une grande session, donc ces problèmes ne se produisent pas.

Typiquement, nous avons enveloppé nos appels dao et paresseux dans un SessionWrapper. Quelque chose comme ce qui suit:

public class SessionWrapper { 
    private SessionFactory sessionFactory; 
    public void setSessionFactory(SessionFactory sessionFactory) { 
     this.hibernateTemplate = new HibernateTemplate(sessionFactory); 
    } 
    public <T> T runLogic(Callable<T> logic) throws Exception { 
     Session session = null; 
     // if the session factory is already registered, don't do it again 
     if (TransactionSynchronizationManager.getResource(sessionFactory) == null) { 
      session = SessionFactoryUtils.getSession(sessionFactory, true); 
      TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session)); 
     } 

     try { 
      return logic.call(); 
     } finally { 
      // if we didn't create the session don't unregister/release it 
      if (session != null) { 
       TransactionSynchronizationManager.unbindResource(sessionFactory); 
       SessionFactoryUtils.releaseSession(session, sessionFactory); 
      } 
     } 
    } 
} 

Il est évident que la SessionFactory même SessionFactory qui a été injecté dans votre dao.


Dans votre cas, vous devez placer tout le corps listUserWithEvent dans cette logique. Quelque chose comme:

public List listUserWithEvent() { 
    return sessionWrapper.runLogic(new Callable<List>() { 
     public List call() { 
      List users = hibernateTemplate.find("from User"); 
      for (User user : users) { 
       System.out.println("LIST : " + user.getName() + ":"); 
       user.getEvents().size(); 
      } 
     } 
    }); 
} 

Vous aurez besoin d'injecter l'instance SessionWrapper dans votre daos.

+0

Malheureusement, je ne comprends pas vraiment cela. Voulez-vous dire que DAOImpl étend le SessionWrapper? Et qui (dans mon exemple) appellerait runLogic et quel serait le Callable? Peut-être son effondrement lorsque vous étendez l'exemple et l'adapter à l'exemple de code que j'ai fourni. En outre, ma question est spécifique à non-web, parce que je veux utiliser le même modèle/daos que j'utilise dans le web aussi pour des applications autonomes (Nos crawlers de données). – Niko

+0

Désolé, c'était déroutant. J'ai ajouté une section avec un exemple runLogic. Vous ne devriez pas avoir besoin de changer votre dao du tout. Il doit être injecté avec la même SessionFactory injectée dans le SessionWrapper. Le SessionWrapper est ensuite injecté dans les daos. – Gray

+0

C'est ce que fait le modèle de transaction de printemps. (il manque juste Typing: /) – Thierry

7

Je suis arrivé ici à la recherche d'un indice concernant un problème similaire. J'ai essayé la solution mentionnée par Thierry et ça n'a pas fonctionné. Après que j'ai essayé ces lignes et cela a fonctionné:

SessionFactory sessionFactory = (SessionFactory) context.getBean("sessionFactory"); 
Session session = SessionFactoryUtils.getSession(sessionFactory, true); 
TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session)); 

En effet ce que je fais est un processus de traitement par lots qui doivent tirer parti des gestionnaires existings de printemps/services. Après avoir chargé le contexte et fait quelques invocations, j'ai fondé le fameux numéro "manqué d'initialiser paresseusement une collection". Ces 3 lignes l'ont résolu pour moi.

+0

L'a corrigé! Bien que mon fichier xml dise , j'ai dû faire getBean ("wfSessionFactory") à la place. En outre, il pourrait probablement utiliser un appel "unbindResource" là-bas à la fin (voir les réponses à la fin de la question) – rogerdpack

9

En cas d'application Web, il est également possible de déclarer un filtre spécial dans web.xml, qui fera session par demande:

<filter> 
    <filter-name>openSessionInViewFilter</filter-name> 
    <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class> 
</filter> 

<filter-mapping> 
    <filter-name>openSessionInViewFilter</filter-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping> 

Après cela, vous pouvez lazyload vos données à tout moment au cours de la demande.

+0

dans mon application ce filtre a également été déclaré, mais l'exception est lancer. –

2

Intéressant!

J'ai rencontré le même problème dans la méthode @RequestMapping du gestionnaire @. La solution simple est d'ajouter une annotation @Transactional à la méthode de gestionnaire de telle sorte que la session reste ouverte pendant toute la durée de l'exécution du corps de la méthode

1

solution la plus simple à mettre en œuvre:

Dans le cadre de la session [dans l'API annotée avec @Transactional], procédez comme suit:

si a avait une liste <B> qui est chargé paresseusement, il suffit d'appeler une API qui fait que la liste est chargée

Quelle est cette API?

size(); API de la classe List.

Donc, tout ce qui est nécessaire est:

Logger.log (a.getBList.size());

Cet appel simple de journalisation de la taille s'assure qu'il obtient la liste entière avant de calculer la taille de la liste. Maintenant vous n'obtiendrez pas l'exception!

+2

Cela n'a pas fonctionné pour moi. – abbas

0

Ce qui a fonctionné pour nous dans JBoss était la solution # 2 tirée de this site at Java Code Geeks.

Web.xml:

<filter> 
     <filter-name>ConnectionFilter</filter-name> 
     <filter-class>web.ConnectionFilter</filter-class> 
    </filter> 
    <filter-mapping> 
     <filter-name>ConnectionFilter</filter-name> 
     <url-pattern>/faces/*</url-pattern> 
    </filter-mapping> 

ConnectionFilter:

import java.io.IOException; 
import javax.annotation.Resource; 
import javax.servlet.*; 
import javax.transaction.UserTransaction; 

public class ConnectionFilter implements Filter { 
    @Override 
    public void destroy() { } 

    @Resource 
    private UserTransaction utx; 

    @Override 
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 
     try { 
      utx.begin(); 
      chain.doFilter(request, response); 
      utx.commit(); 
     } catch (Exception e) { } 
    } 

    @Override 
    public void init(FilterConfig arg0) throws ServletException { } 
} 

Peut-être que ça marcherait avec le printemps aussi.

Questions connexes