【问题标题】:How to prevent saving child object with JPA?如何防止使用 JPA 保存子对象?
【发布时间】:2017-03-24 14:29:19
【问题描述】:

我在学校和学生实体之间建立了 OneToMany 关系。我想要做的是当我保存学校对象时不要保存或更新学生对象。(当然不要删除它们)

当我尝试像下面这样保存学校对象时,它也会更新我的学生对象,但我不希望它们被更新,而只是可连接的。有什么办法吗?

我删除了 Cascade,但它仍然无法正常工作。

School school = new School();
school.setStudents(studentList);
repository.save(school);

我的实体;

@OneToMany(fetch = FetchType.EAGER)
@JoinTable(name = "school_student", joinColumns = {@JoinColumn(name = "school_id")},
        inverseJoinColumns = {@JoinColumn(name = "student_id")})
private List<Student> students;

编辑:

  • 当我尝试保存/更新/删除学校对象时,不要保存/更新/删除学生对象。认为我对 Student 表没有任何插入/更新/删除权限。

【问题讨论】:

  • 你的意思是不是通过@OneToMany 注释的cascade 属性?
  • @JohnBollinger 我不明白你的意思。我可以使用 Cascade,但我不知道如何使用 cascade。
  • 您的意思是您使用第三张表来定义哪些学生去哪些学校?那将需要@ManyToMany 注释,而不是@OneToMany。如果没有cascade,则默认为无操作。
  • 不一定,@coladict。连接表最常用于(确实是必需的)多对多关系,但也可用于映射其他类型的关系。 JPA 明确记录了对此的支持。
  • 我已经写了一个答案,但在这样做的过程中我开始怀疑这是一个 X/Y 问题。你能更全面地解释一下你在这里真正想要完成的事情吗?

标签: java jpa


【解决方案1】:

当我尝试像下面这样保存学校对象时,它也会更新我的学生对象,但我不希望它们被更新,而只是可连接的。有什么办法吗?

我认为您的意思是您希望管理关联本身——即哪些学生与学校相关联——而不是学生的详细信息。出于概念和实际原因,恐怕这没有多大意义。

从概念上讲,一对多关系的“拥有”方始终是“多方”。当您通过连接表管理关联时,这有点武断(但仍然如此),但当关联是通过实体属性直接管理时,这一点很重要。更新关联需要管理拥有方的实体,在本例中是您的 Students。

实际上,仅从School 方管理关联会带来一些困难。例如,如果将 new Student 添加到 School 会怎样?在持久化关联之前需要持久化该学生,但您希望避免这种情况发生。当您将 Student 从一个 School 移动到另一个 Student 时,会出现类似但较小的问题。

现在,您确实可以避免从Schools 到它们的Students 级联持久性操作,但我希望这样做的结果是无法管理它们之间的关联以及管理Schools .如果这是您想要做的,那么您可以从 @OneToMany 注释中省略任何 cascade 属性,或者显式指定一个空的级联类型列表。

然而,请注意,当您提交对持久性上下文的更改时,对所有持久性实体的更改会被保存。如果您想修改 Student 实体而不将这些更改保存到数据库中,那么您最好的选择可能是分离这些实体,或者制作它们的分离副本。

更新

正如cmets中已经阐明的,本质问题是当应用程序没有更新Student的基表的权限,但确实有足够的权限时,如何修改SchoolStudent实体之间的关系在表示关系的连接表上。您不能在对Schools 进行持久更改的同时自动执行此操作,因为School 不是——也不能是——关系的拥有方。

澄清一下:当您将Student 实体移动到不同的Schools 时,JPA 会尝试保存它们,因为出于其目的,Student 和他的特定School 之间的关联是状态的一部分Student。如果Student 实体附加到持久性上下文并且是脏的(例如,因为它被分配给了不同的School),那么它将在提交对 PC 的更改时更新。这与级联或School 的状态无关,只是您通过将Students 的状态移动到不同的Schools'students 列表来间接修改它们的状态。

由于您使用的是连接表,您可以修改您的对象模型以将每个学生/学校关联表示为一个独立的实体。如果这些协会有自己的属性(例如注册日期),那将是有意义的,但否则我不会推荐它。

另一种方法是编写本机查询(通过 JPA);你会在网上找到很多关于它的信息,比如https://blogs.oracle.com/JPQL01/entry/native_query_in_java_persistence。然而,这确实带来了一些问题。特别是,它会使EntityManager 的缓存无效,如果不加以处理,可能会产生严重的问题。如果您正常执行更新,它会在不触发实体生命周期方法的情况下产生实体更改,这对您来说也可能很重要。

关于缓存,解决方案是分离受影响的StudentSchool(旧的和新的)实体。您可以使用EntityManager.detach() 选择性地执行此操作,或者en masse 使用EntityManager.clear()。之后,您需要重新查询或合并要继续使用的实体,这可能会导致一些混乱的问题,具体取决于它们的使用范围以及其他代码对它们的假设。

对于生命周期方法,如果您需要确保它们触发,那么您可以选择自己调用它们。

This article 提供了一些关于您可能需要做什么的更多详细信息。

【讨论】:

  • 你提到的困难无关紧要。我已经没有任何级联注释,但它仍然会更新我的子对象。你的解决方案是什么?
  • @hellzone,我提到的困难是key。他们解释了为什么你要求做的事情,如果我确实正确地描述了它,JPA 不支持。如果不管理其拥有方的实体,您将无法管理关系。你似乎想要这样做是我得出结论的一个重要原因,表达为对问题的评论,你正在提出一个 X-Y 问题:你要求 X 作为解决不同问题 Y 的一种手段,而你最好直接询问 Y。
  • 就像我在问 System.out.println("test" == "test") 的输出是什么?并且您说在比较字符串时不要使用 == 符号。伙计,我不在乎 == 或字符串相等或其他一些事情。告诉我程序的输出。 “例如,如果将新学生添加到学校怎么办?”我对学生表没有任何插入/更新/删除权限,并且应用程序没有任何接口来添加新学生对象。
  • @hellzone,那么是的,它一个 X-Y 问题。 Y 是“当我没有修改Student 的权限时,如何修改Students 和Schools 之间的关系?”我将很快更新我的答案来讨论这个问题。
  • 以防万一 51.1K 声誉不足以提供可信度 - 这个答案是正确的,表示对 JPA 原则的完美理解。但是我想说将关联表映射为一个不同的实体是解决 OP 问题的解决方案,无论关联是否具有属性
【解决方案2】:

或许你可以试试@JoinColumn:

@OneToMany
@JoinColumn(name = "school_id", updatable = false, insertable = false)
private List<Student> students;

【讨论】:

    【解决方案3】:

    您向我们展示的结构不是您列出的一对多关系。这是多对多。

    @ManyToMany(cascade = {}, targetEntity = Student.class)
    @JoinTable(name = "school_student", joinColumns = {
    // references column "id" of "school"
        @JoinColumn(name = "school_id", referencedColumnName = "id")
    }, inverseJoinColumns = {
    // references column "id" of "student"
        @JoinColumn(name = "student_id", referencedColumnName = "id")
    })
    private List<Student> students;
    

    就这么简单。

    【讨论】:

    • 多对多关系需要连接表,但使用连接表并不一定会使关系成为多对多。以这种方式映射它可以解决问题,前提是关系是单向的,但它会改变关系的语义,并且很可能会导致其他问题。
    【解决方案4】:

    删除这些注释

    @OneToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "school_student", joinColumns = {@JoinColumn(name = "school_id")},
        inverseJoinColumns = {@JoinColumn(name = "student_id")})
    

    所以hibernate实体管理器可能不会关联这个对象自动更新

    因此,您必须在 set 方法中向学生编写手动查询,或在代码逻辑中将学生列表作为参数传递给学生以设置学生

    private List<Student> students;
    

    然后它会按你的预期工作

    【讨论】:

    • 所以你的意思是我必须手动编写所有的保存、更新、删除方法?
    • 不会反映您的学校对象更改。您唯一需要做的就是手动填充学生删除注释
    【解决方案5】:

    我建议删除school.setStudents(studentList),因为您告诉hibernate 编辑这个学生的many2one 字段。如果它不起作用,请尝试 session.clear() ,而不是删除声明并确保您从学生方面执行此操作。 我的意思是为学生定义一个学校而不是相反。

    package main;
    
    import org.hibernate.Session;
    
    import hibernate.ConnectionFactory;
    import hibernate.models.Car;
    import hibernate.models.Client;
    
    public class MainClass {
    
        public static void main(String[] args) {
            try {
                /*
                 * if you remove cascading configuration from both side 
                 * hibernate will not save unsaved transient instance
                 */
                if(ConnectionFactory.buildSessionFactory()){
                    Session session = ConnectionFactory.getSessionFactory().openSession();
                    session.beginTransaction();
                    // save a client instance 
                    Client client = new Client();
                    client.setName("client");
                    session.save(client);
                    session.getTransaction().commit();
                    session.close();
    
    
                    session = ConnectionFactory.getSessionFactory().openSession();
                    session.beginTransaction();
                    Car car = new Car();
                    car.setName("Audi");
                    client = (Client) session.get(Client.class, 1);
                    client.setName("client1");
                    car.setClient_id(client);
                    /* inset statement
                     * to prevent hibernate from updating client instance just clear
                     * the session and save only the wanted instance 
                     */
                    session.clear();
                    session.save(car);
                    session.getTransaction().commit();
                    session.close();
    
                    session = ConnectionFactory.getSessionFactory().openSession();
                    session.beginTransaction();
                    car = (Car) session.get(Car.class, 1);
                    car.setName("Audi1");
                    client = (Client) session.get(Client.class, 1);
                    client.setName("Charif1");
                    car.setClient_id(client);
                    /* update statement
                     * to prevent hibernate from saving other instance just clear
                     * the session and update only the wanted instance 
                     */
                    session.clear();
                    session.saveOrUpdate(car);
                    session.getTransaction().commit();
                    session.close();
    
                    /**
                     * but you you are doing from the wrong side
                     * you are trying to tell that this client have
                     * this cars but Hibernate will try to link this 
                     * car by editing the many2one field ??!! 
                     * so remove the statement 
                     * school.setStudents(studentList);
                     * 
                     * 
                     */
                }
            } catch (Exception e) {
                // TODO: handle exception
            }
    
    
    
            try {
                ConnectionFactory.getSessionFactory().close();
            } catch (Exception e) {e.printStackTrace();}
    
        }
    
    }
    

    【讨论】:

      猜你喜欢
      • 2015-07-08
      • 2011-04-25
      • 2023-04-03
      • 1970-01-01
      • 1970-01-01
      • 2021-05-31
      • 2018-02-24
      • 1970-01-01
      • 2016-06-19
      相关资源
      最近更新 更多