【问题标题】:Why Spring JPA Bidirectional OneToMany and ManyToOne is not updating the foreign key column?为什么 Spring JPA 双向 OneToMany 和 ManyToOne 不更新外键列?
【发布时间】:2020-07-19 20:02:48
【问题描述】:

嗨,我正在使用 OneToMany 和 ManyToOne 双向关系学习 Spring JPA,在某些示例中,当我在两侧编写时,我看到 OneToMany 和 ManyToOne 关系,JPA 添加一个新列作为外键列并从父表。但是当我尝试我的时,该列始终为空白。这是我的代码的样子:

这是我的 Account.java 模型:

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

    @Id
    @NotBlank(message = "Not Blank")
    @Size(min = 0, max = 20)
    public String accountId;

    @NotBlank(message = "Not Blank")
    public String accountName;

    @NotBlank(message = "Not Blank")
    @Email(message = "Should be the right email")
    public String accountEmail;

    @NotBlank(message = "Not Blank")
    @Size(min = 5, message = "Minimal 5 char")
    public String accountAddress;

    @NotBlank(message = "Not Blank")
    public String town;

    @NotBlank(message = "Not Blank")
    public String npwp;

    @NotBlank(message = "Not Blank")
    public String phoneNumber;

    public String fax;

    public String remarks;

    @NotNull
    public Date entryTime;

    @NotNull
    public Boolean active;

    @OneToMany(mappedBy="account", cascade = CascadeType.ALL, orphanRemoval = true)
    public List<Dealer> dealer;

//getter setter skipped

}

这是我的 Dealer.java 模型:

@Entity
@Table(name = "msDealer")
public class Dealer {

    @Id
    @NotBlank(message = "Tidak Boleh Kosong")
    @Size(min = 0, max = 20)
    public String dealerId;

    @NotBlank(message = "Tidak Boleh Kosong")
    public String dealerName;

    @NotBlank(message = "Tidak Boleh Kosong")
    @Email(message = "Masukkan Email yang bener")
    public String dealerEmail;

    @NotBlank(message = "Tidak Boleh Kosong")
    @Size(min = 5, message = "Minimal 5 karakter")
    public String dealerAddress;

    @ManyToOne(fetch = FetchType.LAZY)
    public Account account;

//getter setter skipped

}

这是我的存储库:

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {

}

这是我的服务:

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    public Account save(Account account) {
        return accountRepository.save(account);
    }

}

这是我的控制器:

@RestController
@RequestMapping("/api/account")
public class AccountController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private final int ROW_PER_PAGE = 10;

    @Autowired
    private AccountService accountService;

    @PostMapping("/new")
    public ResponseEntity<Account> addAccount(@Valid @RequestBody Account account) {
        try {
            Account newAccount = accountService.save(account);
            return ResponseEntity.created(new URI("/api/account/" + newAccount.getAccountId()))
                    .body(account);
        } catch(Exception ex) {
            logger.error(ex.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
    }

}

然后我将 JSON 发布到我的保存端点:

{
  "accountId": "USA001",
  "accountName": "string",
  "accountEmail": "string",
  "accountAddress": "string",
  "town": "string",
  "npwp": "string",
  "phoneNumber": "string",
  "fax": "string",
  "remarks": "string",
  "entryTime": "2020-04-07T15:01:29.404Z",
  "active": true,
  "dealer": [
    {
      "dealerId": "MMO001",
      "dealerName": "string",
      "dealerEmail": "string",
      "dealerAddress": "string"
    }
  ]
}

当我保存它时,显示在我的终端中的休眠看起来将查询插入到那 2 个表中,但是当我检查我的数据库表(即 postgresql)时,我发现有一个字段“account_account_id”为空,什么我想念这里吗?

我希望 Hibernate 像这样运行 sql:

insert into account (account_id, account_name, ...etc)
values ('USA001', 1)

insert into dealer (account_account_id, dealer_name, dealer_id, ...etc)
values ('USA001', 'New dealer 1', 'MMO001')

经过一番尝试,这是我的更新模型:

我的帐户.java 我删除 cascade = CascadeType.ALL, orphanRemoval = true

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

    @Id
    @NotBlank(message = "Tidak Boleh Kosong")
    @Size(min = 0, max = 20)
    public String accountId;

    @NotBlank(message = "Tidak Boleh Kosong")
    public String accountName;

    @NotBlank(message = "Tidak Boleh Kosong")
    @Email(message = "Masukkan Email yang bener")
    public String accountEmail;

    @NotBlank(message = "Tidak Boleh Kosong")
    @Size(min = 5, message = "Minimal 5 karakter")
    public String accountAddress;

    @NotBlank(message = "Tidak Boleh Kosong")
    public String town;

    @NotBlank(message = "Tidak Boleh Kosong")
    public String npwp;

    @NotBlank(message = "Tidak Boleh Kosong")
    public String phoneNumber;

    public String fax;

    public String remarks;

    @NotNull
    public Date entryTime;

    @NotNull
    public Boolean active;

    @OneToMany(mappedBy="account")
    // @JoinColumn(name = "accountId")
    public List<Dealer> dealer;

//getter setter skipped

}

这是我的 Dealer.java。添加了@JoinColumn:

@Entity
@Table(name = "msDealer")
public class Dealer {

    @Id
    @NotBlank(message = "Tidak Boleh Kosong")
    @Size(min = 0, max = 20)
    public String dealerId;

    @NotBlank(message = "Tidak Boleh Kosong")
    public String dealerName;

    @NotBlank(message = "Tidak Boleh Kosong")
    @Email(message = "Masukkan Email yang bener")
    public String dealerEmail;

    @NotBlank(message = "Tidak Boleh Kosong")
    @Size(min = 5, message = "Minimal 5 karakter")
    public String dealerAddress;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id")
    public Account account;

//getter setter skipped

}

现在错误越来越奇怪了,我在保存 JSON 数据时遇到了这个错误

> "Unable to find com.api.b2b.Model.Dealer with id MMO001; nested
> exception is javax.persistence.EntityNotFoundException: Unable to find
> com.api.b2b.Model.Dealer with id MMO001"

在某些教程中它有效,但我的不是,我做错了什么?

这是我的 github 仓库:https://github.com/Fly-Away/LearningSpring

【问题讨论】:

  • JFYI:如果您修改 ORM,您应该重新创建数据库(以正确更新)或者如果无法手动更新它。否则,您将在数据库中看到已弃用的表/列。
  • 是的,我每次重新加载项目时都使用 spring.jpa.hibernate.ddl-auto=create 重新创建表

标签: java sql spring hibernate hibernate-mapping


【解决方案1】:

为了在双向关系中保存孩子与父母,在孩子实体中设置父母也同步双方。

这里在dealer对象中设置account引用

public Account save(Account account) {
    for (Dealer dealer: account.getDealer()) {
        dealer.setAccount(account);
    }
    return accountRepository.save(account);
}

更新:

但是,如果您想使用单向关系,请删除 Dealer 实体中的 Account 关系。删除这部分

@ManyToOne(fetch = FetchType.LAZY)
public Account account;

然后更新Account表中的关系。

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "account_id")
public List<Dealer> dealer;

这里我们删除了mappedBy,因为目前我们删除了Dealerside 中的映射,并添加了@JoinColumn 来定义我们用于帐户引用的列。

【讨论】:

  • 是的,我可以这样做,但这是手动的,我必须循环经销商并手动设置它,它不应该由 JPA Hibernate 自动处理吗?因为我尝试了单向 OneToMany 关系,它会自动更新我的外键列
  • @KeVin 你可能有一个双向关联,需要正确设置“链接的两侧”。
  • @KeVin 您在实体类中使用双向关系,因此您必须使用account.getDealer().forEach(d -&gt; d.setAccount(account)); 使双方同步
  • @KeVin 我添加了单向关系的解决方案。但它会产生比双向映射更多的查询
  • 以这种方式解决了我的问题,但我不想要这种方式(因为我正在学习并找出 mappedBy 发生了什么,加上生成更多查询,这是我试图避免的) ,我已经更新了我的代码,就像你在保存之前所说的那样,我仍然得到那个错误,我已经将我的源代码上传到了 github,你可以看看吗? github.com/Fly-Away/LearningSpring
【解决方案2】:

如果您在两个实体之间存在双向关系(此处为 AccountDealer),您必须决定哪一方是所述关系的所有者。 默认情况下,One 端是所有者,它导致一个 Join-Table,在修改列表时更新。

由于您定义了 mappedBy 属性 (@OneToMany(mappedBy = "account")),Many 方是关系的所有者。这意味着msDealer 表中的account 列将保存Account 的外键,然后不再使用Join-Table。 Join-Table 可能是在将 mappedBy 定义添加到注释之前初始化数据库的遗留物。

您的选择:

  1. Dealer 保留所有者,不要使用连接表。如果您想观察数据库中的副作用,请查看msDealer.account 列。
  2. 使用@JoinTable 注释强制使用此类表

【讨论】:

  • 你能帮我完成 2 个表之间模型的完整代码吗?我尝试在我的 Account.java 中添加 @JoinTable 它仍然不会更新列,仍然为空
  • 你需要连接表吗?为什么不想让One-side 成为所有者?
  • 是的,我需要,嗯,我会尝试,所以我在我的 Account 模型中删除了 @OneToMany 标签,并像这样在我的 Dealer 模型中定义了 @ManyToOne(mappedBy = "dealer")?但是 mappedBy 似乎不适用于(@ManyToOne 关系,你能提供两个模型的代码吗?
  • 哦,是的,如果我在我的 (@OneToMany 一侧添加@JoinColumn 正在工作,但我仍然对 mappedBy 感到好奇,我可以让它与 mappedBy 一起使用并使用你的 1 号选项吗?我仍然没有得到它是关于“看看列 msDealer.account,我应该在哪里看?你能给我举个关于 (@JoinTable 的用法吗?
  • PS:即使Many-side 是所有者,您也可以强制执行 JoinColumn。但这只需要在加载时额外加入。如果你没有充分的理由这样做,那就太逊了。这听起来可能很刺耳,但您是否考虑过阅读文档? docs.jboss.org/hibernate/orm/5.4/userguide/html_single/…
【解决方案3】:

您在孩子方面缺少@JoinColumn

@Entity
@Table(name = "ms_dealer")
public class Dealer {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_account_id")
    public Account account;

    // other fields

}

你在父端使用了mappedBy,但是在子端没有映射。您需要指出,Dealer 是关系所有者 - 它具有外键。

编辑:如果您保留(不合并)Account 实体及其子实体,则不应传递子实体的 ID。 (实际上,在持久化时传递任何 id 都是代码异味,很可能是性能杀手。)使用的 json 应该如下所示:

{
  "accountName": "string",
  "accountEmail": "string",
  "accountAddress": "string",
  "town": "string",
  "npwp": "string",
  "phoneNumber": "string",
  "fax": "string",
  "remarks": "string",
  "entryTime": "2020-04-07T15:01:29.404Z",
  "active": true,
  "dealer": [
    {
      "dealerName": "string",
      "dealerEmail": "string",
      "dealerAddress": "string"
    }
  ]
}

在保存双方同步之前可能还需要:

account.getDealer().forEach(d -> d.setAccount(account));

编辑:

Author 编辑必须级联到子级:

@OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Dealer> dealer;

您还可以在 ActionList&lt;Dealer&gt; 上添加 @JsonIgnore 以避免序列化为 json 时出现堆栈溢出。

【讨论】:

  • @KeVin 你可以在子端添加cascade = CascadeType.ALL(在Dealer 类中Account@ManyToOne 注释中,然后再试一次?
  • 是的,仍然没有运气,列仍然是空的,@JoinColumn(name = "account_id") 应该是 @JoinColumn(name = "accountId") 对吗?因为我的模型名称是 accountId 而我的 db 列是 account_id,所以 jpa 会将大写转换为下划线对吗?
  • @KeVin 不,应该和数据库里的一样,你是这样查的吗?
  • 是的,我都试过了,结果还是一样,我的ms_dealer里面的account_id列还是空
  • 谢谢你的兄弟,我真的很感谢你的帮助,它是如此详细和耐心,真的帮助了我。谢谢
【解决方案4】:

正如您所说的您正在学习,我想给您一个详细的答案,以便您理解。你在这里缺少的是@JoinColumn

@JoinColumn 可以用于关系的双方。这里的重点在于物理信息重复(列名)以及未优化的SQL将产生一些额外的UPDATE 语句的查询

根据documentation

由于多对一(几乎)始终是 JPA 规范中双向关系的所有者方,因此一对多关联由 @OneToMany(mappedBy=...)

通过基本代码示例了解

@Entity
public class Troop {
    @OneToMany(mappedBy="troop")
    public Set<Soldier> getSoldiers() {
    ...
}

@Entity
public class Soldier {
    @ManyToOne
    @JoinColumn(name="troop_fk")
    public Troop getTroop() {
    ...
} 

Troop 通过 troop 属性与Soldier 具有双向的一对多关系。您不必(不得)在mappedBy 端定义任何物理映射。

要将双向一对多映射,一对多作为拥有方,您必须删除mappedBy 元素并将多对一@JoinColumn 设置为insertableupdatable 为假。此解决方案未经优化,会产生一些额外的UPDATE 语句。

@Entity
public class Troop {
    @OneToMany
    @JoinColumn(name="troop_fk") //we need to duplicate the physical information
    public Set<Soldier> getSoldiers() {
    ...
}

@Entity
public class Soldier {
    @ManyToOne
    @JoinColumn(name="troop_fk", insertable=false, updatable=false)
    public Troop getTroop() {
    ...
}

如果您对给出的解释有任何其他问题,请在下方评论。 :)

【讨论】:

  • 是的,我注意到我想阻止使用该选项的 UPDATE 语句,我想使用选项 1,使用 mappedBy,但是当我发布我的 JSON 时,它不会插入“ Troop_fk”也许我在保存时错过了一些东西?还是我错过了模型中的某些内容?
  • 我无法正确理解您的问题。能否请您缩小问题范围。
  • 我的问题是,当我将父项和子项作为一个 JSON 插入时,OneToMany 在父端使用 mappedBy,在子端使用 ManyToOne 和 JoinColumn,它给了我一个错误,有人说我需要像这样循环并设置孩子“account.getDealer().forEach(d -> d.setAccount(account));”在保存之前,我尝试了它,它仍然无法解决我的错误,我尝试了另一种方法并且它有效,但我对映射方式感到好奇,就像你回答中的第一个选项一样,我的仍然无法正常工作,我已经上传了我在github上的代码,你可以看看吗? github.com/Fly-Away/LearningSpring
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-17
  • 2022-11-15
  • 2016-11-14
  • 1970-01-01
相关资源
最近更新 更多