2014-10-22 14 views
6

Mam do zajęć, które mają stosunek jeden do wielu. Gdy próbuję uzyskać dostęp do leniwie załadowanej kolekcji, otrzymuję numer LazyInitializationException. Poszukuję internetu od jakiegoś czasu i teraz wiem, że otrzymałem wyjątek, ponieważ sesja użyta do załadowania klasy przechowującej kolekcję została zamknięta. Jednak nie znalazłem rozwiązania (a przynajmniej ich nie zrozumiałem). Zasadniczo mam tych klas:Jak rozwiązać problem LazyInitializationException w Spring Data JPA?

użytkownika

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

} 

Praca

@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

@Configuration 
@EnableAutoConfiguration 
@ComponentScan 
public class App { 

    public static void main(final String[] args) { 
     final ConfigurableApplicationContext context = SpringApplication.run(App.class); 
     final UserService userService = context.getBean(UserService.class); 
     final JobService jobService = context.getBean(JobService.class); 

     userService.addSomeUsers();         // Generates some users and stores them in the db 
     jobService.addJobsForUsers(userService.getAllUsers());  // Generates some jobs for the users 

     final User random = userService.getRandomUser();   // Picks a random user 

     System.out.println(random.getCreatedJobs()); 
    } 

} 

Często czytałem, że sesja musi być powiązana z bieżącym wątkiem, ale nie wiem, jak to zrobić z konfiguracjami opartymi na adnotacjach Spring. Czy ktoś może mi wskazać, jak to zrobić?

P.S. Chcę korzystać z leniwego ładowania, więc chętne ładowanie nie jest opcją.

+0

Czy możesz wkleić kod usług, które ładują obiekty? – Smutje

Odpowiedz

-1

Zmień

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

do

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

Albo użyć Hibernate.initialize wewnątrz serwisu, który ma ten sam efekt.

+0

Myślę, że opinie na temat tej odpowiedzi są niesprawiedliwe. Brakuje go w pewnym kontekście, ale poza tym jest całkowicie poprawny. Jest to konieczne, ponieważ domyślnym 'FetchType' dla' @ OneToMany' jest Lazy. Zmiana 'FetchType' na chętniej powoduje, że obiekt potomny zostanie włączony do wykresu obiektu przed zamknięciem sesji, tym samym lecząc problem (potwierdzony trafienie wydajności). – 8bitjunkie

2

Masz 2 opcje.

Wariant 1: Jak wspomniano przez BetaRide użyć strategii EAGER ściągam

Opcja 2: Po zdobyciu user z bazy danych przy użyciu hibernacji, należy dodać poniższą linię kodu, aby załadować elementy kolekcji:

Hibernate.initialize(user.getCreatedJobs()) 

mówi hibernacji zainicjować elementów kolekcji

3

Zasadniczo, musisz pobrać leniwych danych, gdy jesteś w transakcji. Jeśli twoje klasy usług to @Transactional, wszystko powinno być w porządku, gdy jesteś w nich. Po wydostaniu się z klasy usług, jeśli spróbujesz uzyskać leniwą kolekcję, otrzymasz wyjątek, który jest w Twojej metodzie main(), linii System.out.println(random.getCreatedJobs());.

Teraz sprowadzamy się do tego, jakie metody obsługi należy zwrócić. Jeśli oczekuje się, że userService.getRandomUser() zwróci użytkownika z zainicjowanymi zadaniami, aby można było nimi manipulować, wówczas ta metoda musi je pobrać. Najprostszym sposobem zrobienia tego przy pomocy Hibernate jest wywołanie Hibernate.initialize(user.getCreatedJobs()).

+0

Więc każda interakcja z databse musi odbywać się w kontekście transakcyjnym, a kontekst transakcyjny zapewnia, że ​​sesja jest dostępna? Czy można powiązać sesję z bieżącym wątkiem, aby sesja była zawsze dostępna? – stevecross

+0

@feuerball, co rozumiesz przez "powiązanie sesji z bieżącym wątkiem"? Wewnętrznie w trybie hibernacji używa się obiektu sesji do uzyskania połączenia z bazą danych, a po zakończeniu transakcji sesja jest również zamykana razem z połączeniem DB. Więc nawet jeśli próbujesz trzymać sesję poza kontekstem transakcji, to nie ma sensu. – Chaitanya

+0

@feuerball Możesz umieścić '@ Transactional' na innych poziomach, zamiast w klasach usług, ale warstwa usługi jest dla nich naturalnym miejscem. Metody serwisowania obejmują operacje biznesowe, które powinny zostać przekazane do bazy danych jako całości lub wycofane jako całość. Każda inna "asocjująca sesja z bieżącym wątkiem", prawdopodobnie podczas IMHO, jest nadużyciem tego szkieletu i może prowadzić do zerwania granic między warstwami aplikacji. –

2

Rozważ użycie JPA 2.1, z Entity wykresach:

Lazy loading często problem z JPA 2.0. Trzeba było zdefiniować w jednostce FetchType.LAZY lub FetchType.EAGER i upewnić się, że relacja zostanie zainicjowana w ramach transakcji.

Można to zrobić poprzez:

  • przy użyciu określonego zapytania, które odczytuje podmiotowi
  • lub przez dostęp do relacji kodem biznesowej (dodatkowy zapytań dla każdej relacji).

Oba podejścia są dalekie od doskonałości, JPA 2.1 wykresy podmiotu są lepszym rozwiązaniem dla niego:

0

Dla tych, którzy nie mają możliwości używać JPA 2.1, ale chcesz zachować możliwość zwrotu encji w kontrolerze (a nie String/JsonNode/byte []/void with wri w odpowiedzi):

nadal istnieje możliwość zbudowania DTO w transakcji, która zostanie zwrócona przez kontrolera.

@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' nie jest poprawnym atrybutem dla' javax.transaction.Transactional'. Kiedy próbuję go użyć, mój IDE podkreśla go w czytaniu i mówi mi, że to nie jest poprawne. – 8bitjunkie

Powiązane problemy