web-dev-qa-db-fra.com

Hibernate/Spring: échec de l'initialisation paresseuse - aucune session ou session n'a été fermée

Pour une réponse, faites défiler jusqu'à la fin de la ...

Le problème de base est le même que celui demandé plusieurs fois. J'ai un programme simple avec deux événements POJO et un utilisateur - 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; }

}

Remarque: Ceci est un exemple de projet. I vraiment veux utiliser la récupération paresseuse ici.

Nous devons maintenant configurer spring et hibernate et disposer d’un simple basic-db.xml 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 les CustomScopeConfigurer et SimpleThreadScope, mais cela n'a rien changé.

J'ai un simple dao-impl (coller seulement le userDao - le EventDao est à peu près le même - 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 l'org.hibernate.LazyInitializationException - échec de l'initialisation paresseuse d'une collection de rôle: data.model.User.events, aucune session ou session n'a été fermée à la ligne avec user.getEvents (). Size () ;

Et enfin, voici 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] ERREUR org.hibernate.LazyInitializationException - échec de l'initialisation paresseuse d'une collection de rôle: data.model.User.events, aucune session ou session n'a été fermée 
 Org.hibernate.LazyInitializationException: n'a pas réussi à initialiser paresseusement une collection de rôle: data.model.User.events, aucune session ou session n'a été fermée 
 à org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException (AbstractPersistentCollection.Java:380) 
 à org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected (AbstractPersistentCollection.Java:372) 
 .PersistentBag.size (PersistentBag.Java:248) 
 À data.dao.impl.UserDaoImpl.listUserWithEvent (UserDaoImpl.Java:38) à. HibernateTest.main (HibernateTest.main (HibernateTest.Java:44) 
 Exception dans le fil "principal" org.hibernate.LazyIniti alizationException: échec de l'initialisation paresseuse d'une collection de rôle: data.model.User.events, aucune session ou session n'a été fermée 
 à org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException (Abstract, AbstractPersistentCollection.Java:380) [. .] à org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected (AbstractPersistentCollection.Java:372) 
 à org.hibernate.collection.AbstractPersistentCollection.readSize (AbstractPersistentCollection.read). .collection.PersistentBag.size (PersistentBag.Java:248) 
 à data.dao.impl.UserDaoImpl.listUserWithEvent (UserDaoImpl.Java:38) à HibernateTest.main (HibernateTest.main (HibernateTest.Java:44) ) 

Les choses ont essayé mais n'ont pas fonctionné:

  • assigner un threadScope et utiliser beanfactory (j'ai utilisé "request" ou "thread" - aucune différence constatée):
 // objet de l'étendue 
 Étendue threadScope = new SimpleThreadScope (); 
 ConfigurableListableBeanFactory beanFactory = ac.getBeanFactory (); 
 beanFactory.registerScope ("demande", threadScope ); 
 ac.refresh (); 
 ... 
  • Configurer une transaction en récupérant l’objet de session à partir du déo:
 ... 
 Transaction tx = ((UserDaoImpl) udao) .getSession (). BeginTransaction (); 
 Tx.begin (); 
 Utilisateurs = udao .listUserWithEvent (); 
 ... 
  • obtenir une transaction dans la listUserWithEvent ()
 public List listUserWithEvent () {
 SessionFactory sf = hibernateTemplate.getSessionFactory (); 
 Session s = sf.openSession (); 
 Transaction tx = s.beginTransaction (); 
 tx.begin (); 
 
 Répertorier les utilisateurs = hibernateTemplate.find ("from User"); 
 pour (Utilisateur: utilisateurs) { 
 System.out.println ("LIST:" + user.getName () + ":"); 
 User.getEvents (). Size (); 
} 
 tx.commit (); 
 renvoyer des utilisateurs; 
} 

Je suis vraiment à court d'idées maintenant. De plus, l’utilisation de listUser ou listEvent fonctionne parfaitement.

Avancer:

Grâce à Thierry, j'ai fait un pas de plus (je pense). J'ai créé la classe MyTransaction et j'y ai fait tout mon travail, en commençant par le printemps. Le nouveau principal ressemble à 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, il existe maintenant un pointeur nul Exception @: user.getEvents (). Size (); (dans le daoImpl).

Je sais qu'il ne devrait pas être nul (ni de la sortie dans la console ni de la présentation de la base de données).

Voici la sortie de la console pour plus d'informations (j'ai fait une vérification sur user.getEvent () == null et imprimé "EVENT is NULL"):

 Nouvel utilisateur ... 
 Hibernate: insérer dans l'utilisateur (nom) valeurs (?) 
 Hibernate: insérer dans utilisateur (nom) valeurs (?) 
 Hibernate : insérer dans Event (nom, user_id) valeurs (?,?) 
 Hibernate: insérer dans Event (nom, user_id) valeurs (?,?) 
 Hibernate: insérer dans Event (nom, user_id ) valeurs (?,?) 
 Liste des utilisateurs: 
 Hibernate: sélectionnez user0_.id comme id0_, user0_.name comme nom0_ dans Utilisateur user0 _ 
 1: Utilisateur1 
 2: Utilisateur2 
 Liste des événements: 
 Hibernate: sélectionnez event0_.id comme id1_, event0_.name comme nom1_, event0_.user_id comme utilisateur3_1_ à partir de l'événement event0 _ 
 1: Anniversaire1 pour 1: User1 
 2: Birthday2 pour 1: User1 
 3: Mariage pour 2: User2 
 Hibernate: sélectionnez user0_.id comme id0_, user0_.name comme nom0_ de l'utilisateur user0 _. .] Evénements pour les utilisateurs 
 1: User1 - 
 EVENT est NULL 
 2: User2 - 
 EVENT est NULL 

Vous pouvez obtenir le projet exemple à l'adresse http://www.gargan.org/code/hibernate-test1.tgz (il s'agit d'un projet Eclipse/maven)

La solution (pour les applications en console)

Il existe en fait deux solutions à ce problème, selon votre environnement:

Pour une application console, vous avez besoin d'un modèle de transaction qui capture la logique de la base de données active et prend en charge 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 l'utiliser en appelant:


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

Pour que cela fonctionne, vous devez définir la classe de modèle pour spring (c'est-à-dire dans votre fichier basic-db.xml):

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

Une autre solution (possible)

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);
      }
    }

La solution (pour les servlets)

Les servlets ne sont pas un gros problème. Lorsque vous avez une servlet, vous pouvez simplement démarrer et lier une transaction au début de votre fonction et la dissocier à 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);
}
38
Niko

Je pense que vous ne devriez pas utiliser les méthodes transactionnelles de session hibernate, mais laissez spring le faire.

Ajoutez ceci à votre conf de printemps:

<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>

et puis je modifierais votre méthode de test pour utiliser le modèle de transaction printanière:

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
        }
    }
}

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

EDIT: voici maintenant ce qui se passe du point de vue de l'hibernation:

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

  • puis chargez tous les utilisateurs (la requête "from Users")

  • à ce stade, hibernate voit qu’il a déjà l’objet dans sa session, donc ignore celui qu’il a obtenu de la requête et renvoie celui de la session.
  • votre utilisateur dans la session n'a pas sa collection d'événements initialisée, vous obtenez donc la valeur null.
  • ...

Voici quelques points pour améliorer votre code:

  • dans votre modèle, lorsque la commande de collection n'est pas nécessaire, utilisez Définir, pas Liste pour vos collections (événements Set privés, pas d'événements List privés)
  • dans votre modèle, tapez vos collections, sinon hibernate ne déterminera pas l'entité à extraire (événements privés Set <Event>)
  • lorsque vous définissez un côté d'une relation bidirectionnelle et que vous souhaitez utiliser le côté mappéBy de la relation dans la même transaction, définissez les deux côtés. Hibernate ne le fera pas pour vous avant le prochain envoi (lorsque la session est une vue récente de l'état de la base de données).

Donc, pour répondre au point ci-dessus, effectuez la sauvegarde dans une transaction et le chargement dans une 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 définir les deux côtés:

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

(N'oubliez pas non plus d'adresser les points d'amélioration du code ci-dessus, Définir pas la liste, saisie de la collection)

26
Thierry

Dans le cas d'une 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>

Ensuite, vous pouvez charger vos données à tout moment pendant la demande.

9
weekens

Je suis venu chercher un indice concernant un problème similaire. J'ai essayé la solution mentionnée par Thierry et cela n'a pas fonctionné. Après cela, 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 doit exploiter les gestionnaires/services d’existence Spring. Après avoir chargé le contexte et fait quelques invocations, j’ai fondé le fameux problème "Impossible d’initialiser paresseusement une collection". Ces 3 lignes l'ont résolu pour moi.

7
Fran Jiménez

Le problème est que votre dao utilise une session de veille prolongée mais que la charge paresseuse de user.getName (je suppose que c'est là où elle est lancée) se produit en dehors de cette session - pas dans une session du tout ou dans une autre session. En général, nous ouvrons une session d'hibernation avant/ nous passons des appels DAO et nous ne la fermons pas avant d'avoir terminé toutes les tâches paresseuses. Les demandes Web sont généralement encapsulées dans une grande session afin que ces problèmes ne se produisent pas.

En règle générale, nous avons intégré 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);
            }
        }
    }
}

De toute évidence, la SessionFactory est la même que la SessionFactory qui a été injectée dans votre dao.


Dans votre cas, vous devez envelopper le corps entier de 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 devrez injecter l'instance SessionWrapper dans vos daos.

3
Gray

Intéressant!

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

2
mhimu

La solution la plus simple à mettre en œuvre:

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

si A avait une liste <B> qui est chargée paresseusement, appelez simplement une API qui s'assure que la liste est chargée

Quelle est cette API?

taille(); API de la classe List.

Donc, tout ce qui est nécessaire est:

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

Ce simple appel de la consignation de la taille permet d’obtenir la liste complète avant de calculer la taille de la liste. Maintenant, vous n'obtiendrez pas l'exception!

1
ydntn

Ce qui a fonctionné pour nous chez JBoss est la solution n ° 2 tirée de ce site de 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 cela fonctionnerait avec le printemps aussi.

0
EpicPandaForce