Spring Data JPA (problem z relacją wiele do wielu)

0

Uczę sie Spring Data Jpa i stworzyłem prosty schemat bazy danych jak na obrazku poniżej:
title
Wszystkie 3 tabele są w relacjach wiele do wielu.

Gdy wstawiam poszczególne encje a nawet gdy je łączą to wszystko jest w porządku. Ale problem pojawia się gdy chce wyszukać wszystkie przedmioty, konkretnego studenta, które odbywają się w konkretnej sali wykładowej.
Korzystając ze Spring Data Jpa myślałem, że da się to napisać w taki sposób:

ModuleRepository.java

public interface ModuleRepository extends JpaRepository<Module, String> {
    List<Module> findAllByStudentsAndHalls(List<Student> students, List<Hall> halls);
}

DemoApplication.java

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
    
    @Bean
    public CommandLineRunner mappingDemo(HallRepository hallRepository,
                                         ModuleRepository moduleRepository,
                                         StudentRepository studentRepository) {
        return args -> {
            Student student = new Student("Jan", LocalDate.of(2000, 1 ,1));
            studentRepository.save(student);
    
            Hall hall = new Hall(120);
            hallRepository.save(hall);
    
            Module module = new Module("12345", "Algebra");
            moduleRepository.save(module);
            
            module.getStudents().add(student);
            module.getHalls().add(hall);
            moduleRepository.save(module);
            // do tego miejsca wszystko jest ok. Encje są zapisywane w bazie danych
    
            List<Module> modules = moduleRepository.findAllByStudentsAndHalls(Arrays.asList(student), Arrays.asList(hall));
            LOGGER.info(modules.toString());
        };
    }
    
    private static final Logger LOGGER = LoggerFactory.getLogger(DemoApplication.class);
}

Niestety próba wyszukania wszystkich przedmiotów, konkretnego studenta, które odbywają się w konkretnej sali wykładowej kończy się wyjątkiem:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2020-06-16 14:41:09.906 ERROR 13120 --- [ main] o.s.boot.SpringApplication : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'moduleRepository': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List pl.kkk.demo.repositories.ModuleRepository.findAllByStudentsAndHalls(java.util.List,java.util.List)! Operator SIMPLE_PROPERTY on students requires a scalar argument, found interface java.util.List in method public abstract java.util.List pl.kkk.demo.repositories.ModuleRepository.findAllByStudentsAndHalls(java.util.List,java.util.List).
at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:178) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.getObjectFromFactoryBean(FactoryBeanRegistrySupport.java:101) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getObjectForBeanInstance(AbstractBeanFactory.java:1821) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getObjectForBeanInstance(AbstractAutowireCapableBeanFactory.java:1266) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:621) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:609) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.data.repository.config.DeferredRepositoryInitializationListener.onApplicationEvent(DeferredRepositoryInitializationListener.java:51) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.repository.config.DeferredRepositoryInitializationListener.onApplicationEvent(DeferredRepositoryInitializationListener.java:36) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:404) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:361) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:898) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:554) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) ~[spring-boot-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at pl.kkk.demo.DemoApplication.main(DemoApplication.java:25) ~[main/:na]
Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List pl.kkk.demo.repositories.ModuleRepository.findAllByStudentsAndHalls(java.util.List,java.util.List)! Operator SIMPLE_PROPERTY on students requires a scalar argument, found interface java.util.List in method public abstract java.util.List pl.kkk.demo.repositories.ModuleRepository.findAllByStudentsAndHalls(java.util.List,java.util.List).
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:96) ~[spring-data-jpa-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:107) ~[spring-data-jpa-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateIfNotFoundQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:218) ~[spring-data-jpa-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$AbstractQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:81) ~[spring-data-jpa-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lookupQuery(QueryExecutorMethodInterceptor.java:99) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lambda$mapMethodsToQuery$1(QueryExecutorMethodInterceptor.java:92) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) ~[na:na]
at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133) ~[na:na]
at java.base/java.util.Collections$UnmodifiableCollection$1.forEachRemaining(Collections.java:1056) ~[na:na]
at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) ~[na:na]
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) ~[na:na]
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.mapMethodsToQuery(QueryExecutorMethodInterceptor.java:94) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lambda$new$0(QueryExecutorMethodInterceptor.java:84) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at java.base/java.util.Optional.map(Optional.java:258) ~[na:na]
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.<init>(QueryExecutorMethodInterceptor.java:84) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:331) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.lambda$afterPropertiesSet$5(RepositoryFactoryBeanSupport.java:297) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.util.Lazy.getNullable(Lazy.java:212) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.util.Lazy.get(Lazy.java:94) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.getObject(RepositoryFactoryBeanSupport.java:244) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.getObject(RepositoryFactoryBeanSupport.java:57) ~[spring-data-commons-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:171) ~[spring-beans-5.2.7.RELEASE.jar:5.2.7.RELEASE]
... 23 common frames omitted
Caused by: java.lang.IllegalStateException: Operator SIMPLE_PROPERTY on students requires a scalar argument, found interface java.util.List in method public abstract java.util.List pl.kkk.demo.repositories.ModuleRepository.findAllByStudentsAndHalls(java.util.List,java.util.List).
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.throwExceptionOnArgumentMismatch(PartTreeJpaQuery.java:171) ~[spring-data-jpa-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.validate(PartTreeJpaQuery.java:147) ~[spring-data-jpa-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:90) ~[spring-data-jpa-2.3.1.RELEASE.jar:2.3.1.RELEASE]
... 48 common frames omitted

Załączam jeszcze plik Module.java gdyby ktoś zechciał w niego spojrzeć (pominąłem importy, gettery, settery, etc.):

@Entity
public class Module {
    @Id
    @Column(length = 5, nullable = false)
    private String id;
    
    @Column(length = 40, nullable = false)
    private String name;
    
    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    @JoinTable(
            name = "rel_module_student",
            joinColumns = {
                    @JoinColumn(
                            name = "module_id",
                            referencedColumnName = "id",
                            nullable = false,
                            updatable = false)},
            inverseJoinColumns = {
                    @JoinColumn(
                            name = "student_id",
                            referencedColumnName = "id",
                            nullable = false,
                            updatable = false)}
    )
    List<Student> students = new ArrayList<>();
    
    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    @JoinTable(
            name = "rel_module_hall",
            joinColumns = {
                    @JoinColumn(
                            name = "module_id",
                            referencedColumnName = "id",
                            nullable = false,
                            updatable = false)},
            inverseJoinColumns = {
                    @JoinColumn(
                            name = "hall_id",
                            referencedColumnName = "id",
                            nullable = false,
                            updatable = false)}
    )
    List<Hall> halls = new ArrayList<>();
}

Jak to naprawić by działało? Źle nazwałem metodę w interfejsie ModuleRepository? Jeśli tak to jak powinna się nazywać? Przykład jest tak prosty, że zakładam, że nie potrzeba tutaj kombinować z JPQL czy zapytaniami natywnymi.

0
Failed to create query for method public abstract java.util.List pl.kkk.demo.repositories.ModuleRepository.findAllByStudentsAndHalls(java.util.List,java.util.List)! 
Operator SIMPLE_PROPERTY on students requires a scalar argument, found interface java.util.List in method public abstract java.util.List

A jakby miało to zadziałać na poziomie SQL-a?

 List<Module> findAllByStudentsAndHalls(List<Student> students, List<Hall> halls);
0

Na poziomie SQLa napisałbym korzystając z identyfikatorów Hall i Student. Czyli byłoby to coś takiego.

select _m.*
from module _m
inner join rel_module_student _rms
on _m.id = _rms.module_id
inner join rel_module_hall _rmh
on _m.id = _rmh.module_id
inner join student _s -- to i poniże złączenie jest nadmiarowe w tym przypadku ale nie w przypadku ogólnym
on _rms.student_id = _s.id
inner join hall _h
on _rmh.hall_id = _h.id
where _rms.id in (1, 2, 3, itd.) and _rmh.id in (1, 2, 3, itd.)

Wiem, że dwa złączenia tutaj są nadmiarowe do identyfikatory studentów i sal są już w tabelach rel_module_xxx ale dałem je bo docelowo szuka się po bardziej ludzkich polach jak imię studenta czy pojemność sali, a wtedy trzeba złączyć wszystkie 5 tabel.

Nie wiem czy dobrze zrozumiałem ale chodzi Tobie o to, że w SQLu podaję listę pól skalarnych, a w Springu przekazuję listę całych obiektów. Ale zakładam, że Spring jakoś to sobie rozkminia, w końcu wie co jest identyfikatorem encji. A może nie rozkminia?

No ale nawet gdybym chciał wyszukać faktycznie wszystkie przedmioty, konkretnego studenta podając jego np. imię, które odbywają się w konkretnej sali wykładowej podając jej kod. To jakby wyglądała ta metoda?

1

@Charles_Ray Wstawił już fajnego linka, @ReallyGrid mogę tylko streścić żebyś spróbował zamienić findAllByStudentsAndHalls na findAllByStudentsInAndHallsIn

1 użytkowników online, w tym zalogowanych: 0, gości: 1