2014-10-22 33 views
6

Devo classi che hanno una relazione uno-a-molti. Quando provo ad accedere alla collezione caricata pigramente ottengo il LazyInitializationException. Cerco il web da un po 'ora e ora so che ottengo l'eccezione perché la sessione utilizzata per caricare la classe che detiene la collezione è chiusa. Tuttavia non ho trovato una soluzione (o almeno non li ho capiti). Fondamentalmente ho quelle classi:Come risolvere LazyInitializationException in Spring Data JPA?

utente

@Entity 
@Table(name = "user") 
public class User { 

    @Id 
    @GeneratedValue 
    @Column(name = "id") 
    private long id; 

    @OneToMany(mappedBy = "creator") 
    private Set<Job> createdJobs = new HashSet<>(); 

    public long getId() { 
     return id; 
    } 

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

    public Set<Job> getCreatedJobs() { 
     return createdJobs; 
    } 

    public void setCreatedJobs(final Set<Job> createdJobs) { 
     this.createdJobs = createdJobs; 
    } 

} 

UserRepository

public interface UserRepository extends JpaRepository<User, Long> {} 

UserService

@Service 
@Transactional 
public class UserService { 

    @Autowired 
    private UserRepository repository; 

    boolean usersAvailable = false; 

    public void addSomeUsers() { 
     for (int i = 1; i < 101; i++) { 
      final User user = new User(); 

      repository.save(user); 
     } 

     usersAvailable = true; 
    } 

    public User getRandomUser() { 
     final Random rand = new Random(); 

     if (!usersAvailable) { 
      addSomeUsers(); 
     } 

     return repository.findOne(rand.nextInt(100) + 1L); 
    } 

    public List<User> getAllUsers() { 
     return repository.findAll(); 
    } 

} 

lavoro

@Entity 
@Table(name = "job") 
@Inheritance 
@DiscriminatorColumn(name = "job_type", discriminatorType = DiscriminatorType.STRING) 
public abstract class Job { 

    @Id 
    @GeneratedValue 
    @Column(name = "id") 
    private long id; 

    @ManyToOne 
    @JoinColumn(name = "user_id", nullable = false) 
    private User creator; 

    public long getId() { 
     return id; 
    } 

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

    public User getCreator() { 
     return creator; 
    } 

    public void setCreator(final User creator) { 
     this.creator = creator; 
    } 

} 

JobRepository

public interface JobRepository extends JpaRepository<Job, Long> {} 

JobService

@Service 
@Transactional 
public class JobService { 

    @Autowired 
    private JobRepository repository; 

    public void addJob(final Job job) { 
     repository.save(job); 
    } 

    public List<Job> getJobs() { 
     return repository.findAll(); 
    } 

    public void addJobsForUsers(final List<User> users) { 
     final Random rand = new Random(); 

     for (final User user : users) { 
      for (int i = 0; i < 20; i++) { 
       switch (rand.nextInt(2)) { 
       case 0: 
        addJob(new HelloWorldJob(user)); 
        break; 
       default: 
        addJob(new GoodbyeWorldJob(user)); 
        break; 
       } 
      } 
     } 
    } 

} 

App

Ho letto spesso che la sessione deve essere associata al thread corrente, ma non so come farlo con le configurazioni basate su annotazione di Spring. Qualcuno potrebbe indicarmi come farlo?

P.S. Voglio usare il caricamento lazy, quindi il carico impaziente non è un'opzione.

+0

È possibile incollare il codice per i servizi che caricano le entità? – Smutje

risposta

-1

Change

@OneToMany(mappedBy = "creator") 
private Set<Job> createdJobs = new HashSet<>(); 

a

@OneToMany(fetch = FetchType.EAGER, mappedBy = "creator") 
private Set<Job> createdJobs = new HashSet<>(); 

Oppure utilizzare Hibernate.initialize all'interno del vostro servizio, che ha lo stesso effetto.

+0

Penso che i downvotes per questa risposta siano ingiusti. In qualche contesto è carente, ma è altrimenti del tutto corretto. Questo è necessario perché il predefinito 'FetchType' per' @ OneToMany' è Lazy. Cambiare il 'FetchType' in un ambiente ansioso fa sì che l'oggetto figlio venga incluso nel grafico dell'oggetto prima che la sessione venga chiusa, risolvendo così il problema (colpo di performance riconosciuto). – 8bitjunkie

2

Hai 2 opzioni.

Opzione 1: Come già detto da BetaRide, utilizzare la strategia EAGER recupero

Opzione 2: Dopo aver preso il user dal database usando Hibernate, aggiungere la riga di seguito in codice per caricare gli elementi di raccolta:

Hibernate.initialize(user.getCreatedJobs()) 

Questo dice in letargo per inizializzare gli elementi di raccolta

3

Fondamentalmente, è necessario recuperare i dati pigri mentre si è all'interno di una transazione. Se le tue classi di servizio sono @Transactional, allora tutto dovrebbe essere ok mentre ci sei dentro. Una volta usciti dalla classe di servizio, se si tenta di eseguire la raccolta lazy con get, si otterrà quell'eccezione, che si trova nel metodo main(), riga System.out.println(random.getCreatedJobs());.

Ora, si tratta di ciò che i metodi di servizio devono restituire. Se si prevede che userService.getRandomUser() restituisca un utente con i lavori inizializzati in modo da poterli manipolare, è quindi responsabilità del metodo recuperarlo. Il modo più semplice per farlo con Hibernate è chiamando Hibernate.initialize(user.getCreatedJobs()).

+0

Quindi ogni interazione con il database deve essere eseguita in un contesto transazionale e un contesto transazionale assicura che una sessione sia disponibile? È possibile associare una sessione al thread corrente in modo che la sessione sia sempre disponibile? – stevecross

+0

@feuerball, cosa intendi per 'associare una sessione al thread corrente'? Internamente la modalità di sospensione utilizza l'oggetto di sessione per ottenere la connessione al database e, una volta completata la transazione, la sessione viene chiusa insieme alla connessione DB. Quindi, anche se si tenta di tenere la sessione al di fuori del contesto della transazione, non è di alcuna utilità. – Chaitanya

+0

@feuerball Puoi mettere '@ Transactional' su altri livelli, invece delle classi di servizio, ma il livello di servizio è il posto naturale per loro. I metodi di servizio incapsulano le operazioni aziendali, che dovrebbero essere trasferite al database nel suo insieme o ripristinate nel suo complesso. Qualsiasi altra "sessione di associazione al thread corrente", IMHO, anche se probabilmente possibile, è un uso improprio di questo framework e può portare a rompere i confini tra i livelli dell'applicazione. –

2

considerare l'utilizzo di JPA 2.1, con Entità grafici:

pigro carico era spesso un problema con JPA 2.0. Dovevi definire l'entità FetchType.LAZY o FetchType.EAGER e assicurarti che la relazione venga inizializzata all'interno della transazione.

questo potrebbe essere fatto da:

  • utilizzando una query specifica che legge l'entità
  • o accedendo alla relazione al codice attività (query aggiuntiva per ogni relazione).

Entrambi gli approcci sono ben lungi dall'essere perfetto, JPA 2.1 grafici entità sono una soluzione migliore per questo:

0

Per coloro che non hanno la possibilità utilizzare JPA 2.1 ma si vuole mantenere la possibilità di restituire un'entità nel proprio controller (e non una stringa/JsonNode/byte []/void con wri te in risposta):

c'è ancora la possibilità di creare un DTO nella transazione, che verrà restituito dal controller.

@RestController 
@RequestMapping(value = FooController.API, produces = MediaType.APPLICATION_JSON_VALUE) 
class FooController{ 

    static final String API = "/api/foo"; 

    private final FooService fooService; 

    @Autowired 
    FooController(FooService fooService) { 
     this.fooService= fooService; 
    } 

    @RequestMapping(method = GET) 
    @Transactional(readOnly = true) 
    public FooResponseDto getFoo() { 
     Foo foo = fooService.get(); 
     return new FooResponseDto(foo); 
    } 
} 
+0

'readOnly' non è un attributo valido per' javax.transaction.Transactional'. Quando provo a usarlo, il mio IDE lo sottolinea in lettura e mi dice che questo non è valido. – 8bitjunkie

Problemi correlati