如何在Spring Data中使用ManyToMany集合

时间:2014-05-03 15:47:40

标签: java spring hibernate spring-mvc jpa

我真的很困惑如何在我的应用程序中使用@ManyToMany子集合。为了演示我的问题,我在https://bitbucket.org/ctombaugh/test

设置了一个示例项目

主要问题是我保存父对象时删除了子集合。我能想到的唯一方法就是从数据库中获取所有子集合并在保存之前将它们附加到父对象,但我不能相信这是正确的方法。所以我想我做错了什么,希望有人可以查明它并提高我对JPA如何工作的理解。

我想提前提到这个问题与我的other question有关,但我认为最好关注问题的核心,删除所有上下文,并使用一个干净的示例项目。我希望这不是问题。

父对象类Test

@Entity
public final class Test implements Identifiable<Integer> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private volatile Integer id;

    @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
    @JoinTable(name = "test_label", joinColumns = { @JoinColumn(name = "test") }, inverseJoinColumns = { @JoinColumn(name = "label") })
    private Set<Label> labels = new HashSet<Label>();

    private volatile String name = "";

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Test test = (Test) o;
        if (!id.equals(test.id)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        if (id == null) return 0;
        return id.hashCode();
    }

}

子对象类Label

@Entity
public final class Label implements Identifiable<Integer> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private volatile Integer id;

    private volatile String name = "";

    public Label() {
    }

    public Label(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Label label = (Label) o;
        if (!id.equals(label.id)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        if (id == null) return 0;
        return id.hashCode();
    }

}

服务TestServiceImpl

@Service
final class TestServiceImpl implements TestService {

    private final TestRepository testRepository;

    @Autowired
    TestServiceImpl(TestRepository testRepository) {
        this.testRepository = testRepository;
    }

    @Override
    public List<Test> findAll() {
        return testRepository.findAll();
    }

    @Override
    @Transactional
    public Test save(Test test) {
        return testRepository.save(test);
    }

    @Override
    @Transactional
    public Test saveAndAdd(Test test, Label label) {
        test = testRepository.save(test);
        test.getLabels().add(label);
        return testRepository.save(test);
    }

    @Override
    @Transactional
    public Test create() {
        return testRepository.save(new Test());
    }

    @Override
    public Test findOne(Integer id) {
        return testRepository.findOne(id);
    }

}

表单支持对象TestForm

public class TestForm {

    private Test test;
    private Label label;

    public Test getTest() {
        return test;
    }

    public void setTest(Test test) {
        this.test = test;
    }

    public Label getLabel() {
        return label;
    }

    public void setLabel(Label label) {
        this.label = label;
    }

}

controller TestConroller

@Controller
@RequestMapping("/")
public class TestController {

    private TestService testService;
    private LabelService labelService;

    private static final Logger logger = Logger.getLogger(TestController.class);

    @Autowired
    TestController(TestService testService, LabelService labelService) {
        this.testService = testService;
        this.labelService = labelService;

        // populate labels
        List<Label> labels = labelService.findAll();
        if (labels == null || labels.size() == 0) {
            labelService.create("label a");
            labelService.create("label b");
        }

    }

    @ModelAttribute("labels")
    public List<Label> getLabels() {
        return labelService.findAll();
    }

    @ModelAttribute("user")
    public String getUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String name = auth.getName();
        return name;
    }

    @RequestMapping(value="/", method = RequestMethod.GET)
    public String firstPage(ModelMap model) {
        List<Test> tests = testService.findAll();
        model.addAttribute("tests", tests);
        return "index";
    }

    @RequestMapping(value="/login", method = RequestMethod.GET)
    public String login(ModelMap model) {
        return "login";
    }

    @RequestMapping(value="/addtest", method = RequestMethod.GET)
    public String addExpert(ModelMap model) {
        testService.create();
        return "redirect:/";
    }

    @RequestMapping(value="/test/{testId}", method=RequestMethod.GET)
    public String editProject(@PathVariable Integer testId, ModelMap model) {
        TestForm form = new TestForm();
        Test test = testService.findOne(testId);
        form.setTest(test);
        model.addAttribute("testForm", form);
        return "test";
    }

    @RequestMapping(value="/test", method=RequestMethod.POST)
    public String submitTest(@ModelAttribute("testForm") TestForm testForm, BindingResult result) {
        logger.info("labels: " + testForm.getTest().getLabels());
        if (testForm.getTest().getLabels() != null) {
            logger.info("labels size: " + testForm.getTest().getLabels().size());
        }
        testService.save(testForm.getTest());
        return "redirect:/";
    }

    @RequestMapping(value="/addlabel", method=RequestMethod.POST)
    public String addLabel(@ModelAttribute("testForm") TestForm testForm, BindingResult result, ModelMap model) {
        Test test = testForm.getTest();
        Label label = testForm.getLabel();
        test = testService.saveAndAdd(test, label);
        return "redirect:/test/" + test.getId();
    }
}

Thymeleaf模板表单test.html

<form id="testForm" action="#" th:action="@{/test}" th:object="${testForm}" method="post">

    <input type="hidden" th:field="*{test.id}" />

    <p>
        <label for="name">Name</label>
        <input id="name" type="text" th:field="*{test.name}" />
    </p>

    <p>
        <button id="testbutton" type="submit" name="testbutton">Save</button>
    </p>

    <h4>Labels</h4>

    <table>
        <tr>
            <th>id</th>
            <th>name</th>
        </tr>
        <tr th:each="label : *{test.labels}">
            <td th:text="${label.id}"></td>
            <td th:text="${label.name}"></td>
        </tr>
    </table>

    <p>
        <select id="label" th:field="*{label}">
            <option value="">(select label)</option>
            <option th:each="label : ${labels}"
                    th:value="${label.id}"
                    th:text="${label.name}"></option>
        </select>

        <a href="#" onclick="addLabel();">Add</a>
    </p>
</form>

申请上下文ApplicationContext

@Configuration
@EnableWebMvc
@ComponentScan
@Import({ SecurityConfig.class })
@PropertySource("classpath:application.properties")
@EnableJpaRepositories("test.model")
public class ApplicationContext extends WebMvcConfigurerAdapter {

    @Autowired
    public StringToLabel stringToLabel;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**").addResourceLocations("/resources/css/**");
        registry.addResourceHandler("/js/**").addResourceLocations("/resources/js/**");
    }

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(stringToLabel);
    }

    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource datasource = new DriverManagerDataSource();
        datasource.setDriverClassName("org.hsqldb.jdbcDriver");
        datasource.setUrl("jdbc:hsqldb:mem:test");
        datasource.setUsername("sa");
        datasource.setPassword("");
        return datasource;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(){
        LocalContainerEntityManagerFactoryBean factoryBean
                = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setDataSource( dataSource() );
        factoryBean.setPackagesToScan( new String[ ] { "test.model" } );
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        factoryBean.setJpaVendorAdapter( vendorAdapter );
        factoryBean.setJpaProperties( this.additionalProperties() );
        return factoryBean;
    }

    @Bean
    public PlatformTransactionManager transactionManager(){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(
                this.entityManagerFactory().getObject() );
        return transactionManager;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation(){
        return new PersistenceExceptionTranslationPostProcessor();
    }

    Properties additionalProperties() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.hbm2ddl.auto", "update");
        properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        return properties;
    }

}

应用初始值设定项ApplicationInitializer

public class ApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(ApplicationContext.class);
        rootContext.setDisplayName("Test application");

        servletContext.addListener(new ContextLoaderListener(rootContext));

        ServletRegistration.Dynamic dispatcher =
                servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");

        FilterRegistration.Dynamic filter = servletContext.addFilter("openEntityManagerInViewFilter", OpenEntityManagerInViewFilter.class);
        filter.setInitParameter("singleSession", "true");
        filter.addMappingForServletNames(null, true, "dispatcher");

    }

}

编辑1:显然我无法初始化服务中的任何集合(调用size()无法正常工作)。这是否意味着父对象被分离?这也适用于OneToMany个集合,但我可以通过保存子节点而不是父节点来添加几个子节点。

我想我的配置一定有问题,我能做点什么吗?

1 个答案:

答案 0 :(得分:0)

看起来你是懒惰加载@ ManyToMany集合,然后将旧的分离对象与空子节点(因为子节点从未加载)合并到一个新会话并保存父节点。 当父节点被保存时,你也会将相同的操作级联到子节点,导致删除为hibernate / jpa,假设您手动删除它们并合并父节点。

为了避免这种情况,要么急切地加载所有关系中的孩子,要么不要独立于父母那样级联合并管理孩子。