使用JPA Hibernate自动保存子对象

89

我在 Parent 和 Child 表之间建立了一对多的关系。在 Parent 对象中,我有一个

List<Child> setChildren(List<Child> childs)

我的Child表中也有一个外键,该外键是一个ID,引用了数据库中的一个Parent行。因此,在我的数据库配置中,该外键不能为空。 另外,该外键是Parent表中的主键。

所以我的问题是如何通过类似于以下方式自动保存子对象:

session.save(parent);
我尝试了上述方法,但是遇到了一个数据库错误,它抱怨在Child表中的外键字段不能为空。有没有办法告诉JPA自动将这个外键设置为Child对象,以便自动保存子对象?

请展示映射表、代码和原始错误。 - Adeel Ansari
10个回答

104
我尝试了上面的方法,但是我遇到了一个数据库错误,提示Child表中的外键字段不能为空。有没有办法告诉JPA自动将这个外键设置到Child对象中,以便能够自动保存子对象呢?
嗯,在这里有两件事情。
首先,您需要级联保存操作(但我的理解是,如果您不这样做,则在“child”表中进行插入时不会出现FK约束冲突)。
其次,您可能具有双向关联,我认为您没有正确地设置“链接的双方”。您应该做类似于这样的事情:
Parent parent = new Parent();
...
Child c1 = new Child();
...
c1.setParent(parent);

List<Child> children = new ArrayList<Child>();
children.add(c1);
parent.setChildren(children);

session.save(parent);
常见的模式是使用链接管理方法:
@Entity
public class Parent {
    @Id private Long id;

    @OneToMany(mappedBy="parent")
    private List<Child> children = new ArrayList<Child>();

    ...

    protected void setChildren(List<Child> children) {
        this.children = children;
    }

    public void addToChildren(Child child) {
        child.setParent(this);
        this.children.add(child);
    }
}

代码变为:

Parent parent = new Parent();
...
Child c1 = new Child();
...

parent.addToChildren(c1);

session.save(parent);
参考文献

2
是的,我的问题实际上是我想要一个单向映射,但却有一个双向映射。问题在于我在子表中映射了外键。所以我从子表中删除了它,然后它就起作用了。我在父表中使用了@OneToMany和@JoinColumn。谢谢。 - Marquinio
是的,在我的代码中,我称之为“互惠关系”。另一种处理方法是在JPA层的.save之前进行“确保互惠”通过最后一次检查。 - granadaCoder
private Collection<MyParentJpaEntity> ensureReciprocal(Collection<MyParentJpaEntity> inputItems) { Collection<MyParentJpaEntity> returnItems = null; if (null != inputItems) { returnItems = inputItems; - granadaCoder
returnItems.parallelStream().forEach((par) -> { if (null != par.getMyChilds()) { for (MyChildJpaEntity chd : par.getMyChilds()) { if (null == detail.getParentMyParentJpaEntity()) { /* jpa 不喜欢空的“互惠”关系。缺少互惠关系有时会显示为子实体上没有代理键持久化 */ - granadaCoder
所以在我的非代码注释之间是你可以考虑的代码(总共3个注释,我一直达到了最大字符限制) - granadaCoder
显示剩余2条评论

40

我认为您需要通过xml/注解设置映射中的级联选项。请参考这里的Hibernate参考示例

如果您使用注解,您需要像这样做:

@OneToMany(cascade = CascadeType.PERSIST) // Other options are CascadeType.ALL, CascadeType.UPDATE etc..

默认级联行为仅为 NOTHING。 - masterziv
11
我认为应该是 @OneToMany(cascade = CascadeType.ALL),insert不存在! - tommueller
newnoise:我最近没有接触过Hibernate。但那时它是可用的选项之一。 - Adeel Ansari
3
CascadeType.INSERT 被替换为 CascadeType.PERSIST - Adeel Ansari
我尝试使用CascadeType.PERSIST,但它没有起作用。将其替换为CascadeType.ALL解决了问题。 - Jelle den Burger

5
以下程序描述了Hibernate中双向关系是如何工作的。
当父对象保存时,其子对象列表将自动保存。
在父对象方面:
    @Entity
    @Table(name="clients")
    public class Clients implements Serializable  {

         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)     
         @OneToMany(mappedBy="clients", cascade=CascadeType.ALL)
          List<SmsNumbers> smsNumbers;
    }

在子节点上添加以下注释:

  @Entity
  @Table(name="smsnumbers")
  public class SmsNumbers implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     int id;
     String number;
     String status;
     Date reg_date;
     @ManyToOne
     @JoinColumn(name = "client_id")
     private Clients clients;

    // and getter setter.

 }

主类:

 public static void main(String arr[])
 {
    Session session = HibernateUtil.openSession();
      //getting transaction object from session object
    session.beginTransaction();

    Clients cl=new Clients("Murali", "1010101010");
    SmsNumbers sms1=new SmsNumbers("99999", "Active", cl);
    SmsNumbers sms2=new SmsNumbers("88888", "InActive", cl);
    SmsNumbers sms3=new SmsNumbers("77777", "Active", cl);
    List<SmsNumbers> lstSmsNumbers=new ArrayList<SmsNumbers>();
    lstSmsNumbers.add(sms1);
    lstSmsNumbers.add(sms2);
    lstSmsNumbers.add(sms3);
    cl.setSmsNumbers(lstSmsNumbers);
    session.saveOrUpdate(cl);
    session.getTransaction().commit(); 
    session.close();    

 }

2
请详细阐述您的答案,以便提问者了解您的答案如何以及为什么有效。 - UmarZaii
它将插入1个客户端,然后是3个短信号码,之后会更新3个短信号码的fk。这对性能不利。使用双向LinkManagement。 - Tarunjeet Singh Salh
1
实体列表不能作为 @Id - Matthew Read

3
在您的setChilds函数中,您可能需要尝试循环遍历列表并执行以下操作:
child.parent = this;

您还应该将级联设置到父元素的适当值。

2
以下是在双向关系的子对象中分配父对象的方法:
假设您有一个关系,比如一对多,则对于每个父对象,存在一组子对象。 在双向关系中,每个子对象都将引用其父对象。
eg : Each Department will have list of Employees and each Employee is part of some department.This is called Bi directional relations.

为了实现这一点,一种方法是在保存父对象时为子对象分配父对象。
Parent parent = new Parent();
...
Child c1 = new Child();
...
c1.setParent(parent);

List<Child> children = new ArrayList<Child>();
children.add(c1);
parent.setChilds(children);

session.save(parent);

另一种方法是,你可以使用Hibernate Interceptor来实现,这样可以帮助你避免为所有的模型编写上述代码。

Hibernate拦截器提供了一些API来在执行任何数据库操作之前进行自定义操作。例如,在对象的onSave方法中,我们可以使用反射来将父对象分配给子对象。

public class CustomEntityInterceptor extends EmptyInterceptor {

    @Override
    public boolean onSave(
            final Object entity, final Serializable id, final Object[] state, final String[] propertyNames,
            final Type[] types) {
        if (types != null) {
            for (int i = 0; i < types.length; i++) {
                if (types[i].isCollectionType()) {
                    String propertyName = propertyNames[i];
                    propertyName = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
                    try {
                        Method method = entity.getClass().getMethod("get" + propertyName);
                        List<Object> objectList = (List<Object>) method.invoke(entity);

                        if (objectList != null) {
                            for (Object object : objectList) {
                                String entityName = entity.getClass().getSimpleName();
                                Method eachMethod = object.getClass().getMethod("set" + entityName, entity.getClass());
                                eachMethod.invoke(object, entity);
                            }
                        }

                    } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        return true;
    }

}

您可以将拦截器注册到配置中,如下所示:

new Configuration().setInterceptor( new CustomEntityInterceptor() );

请问您能否分享保存配置的代码? - Yusuf

1
在JPA中,@*To*关系中,父实体和子实体都必须在保存(父)之前进行相互分配。

0

如果同时使用hibernateJPA,在保存子对象时可能会出现一些问题。因此,建议使用org.hibernate.annotations来执行Cascade操作。


0
简而言之,将级联类型设置为all,就可以完成工作; 例如,在您的模型中添加以下代码。 @OneToMany(mappedBy = "receipt", cascade=CascadeType.ALL) private List saleSet;

0

我看到了一个非常有用的提示,可以在双向连接中为任何子项分配父项。如果您不想每次使用child.setParent(parent),您可以在父实体中创建带有注释@PrePersist的方法,该注释是JPA实体生命周期事件之一,在那里您可以定义this.children.forEach(child -> child.setParent(this));,这将在将其持久化到数据库之前创建父项和子项之间所需的连接。


0
如果您没有双向关系,并且只想保存/更新子表中的单个列,则可以创建具有子实体的JPA存储库,并调用save/saveAll或update方法。
注意:如果遇到FK违规,则意味着您的Postman请求中的主键和外键ID与子表中生成的ID不匹配,请检查您要更新的请求和子表中的ID(它们应该匹配/如果它们不匹配则会出现FK违规),在事务之前保存父项和子项时生成的任何ID,在尝试更新子表中的单个列时,这些ID应该与您的第二个调用匹配。
父项:
@Entity
@Table(name="Customer")
public class Customer implements Serializable  {

     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)  
     private UUID customerId ;
     
     @OneToMany(cascade = CascadeType.ALL) 
     @JoinColumn(name ="child_columnName", referencedColumnName= 
                "parent_columnName")
     List<Accounts> accountList;
}

子元素:

 @Entity
    @Table(name="Account")
    public class Account implements Serializable  {

         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)  
         private UUID accountid;
         
        
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接