为什么Spring JPA的双向一对多和多对一关系不能更新外键列呢?

huangapple go评论80阅读模式
英文:

Why Spring JPA Bidirectional OneToMany and ManyToOne is not updating the foreign key column?

问题

I've translated your provided text as requested. Here's the translated content:

嗨,我正在学习使用Spring JPA来创建OneToMany和ManyToOne双向关系。在某些示例中,我看到OneToMany和ManyToOne关系当我在两侧都写入时,JPA会添加一个新的列作为外键列,并将键值插入到父表中。但是,当我尝试我的时候,该列始终为空。以下是我的代码示例:

这是我的Account.java模型:

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

    @Id
    @NotBlank(message = "不能为空")
    @Size(min = 0, max = 20)
    public String accountId;

    @NotBlank(message = "不能为空")
    public String accountName;

    @NotBlank(message = "不能为空")
    @Email(message = "应该是正确的电子邮件")
    public String accountEmail;

    @NotBlank(message = "不能为空")
    @Size(min = 5, message = "至少5个字符")
    public String accountAddress;

    @NotBlank(message = "不能为空")
    public String town;

    @NotBlank(message = "不能为空")
    public String npwp;

    @NotBlank(message = "不能为空")
    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 = "不能为空")
    @Size(min = 0, max = 20)
    public String dealerId;

    @NotBlank(message = "不能为空")
    public String dealerName;

    @NotBlank(message = "不能为空")
    @Email(message = "应该是正确的电子邮件")
    public String dealerEmail;

    @NotBlank(message = "不能为空")
    @Size(min = 5, message = "至少5个字符")
    public String dealerAddress;

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

    //getter setter skipped
    
}

这是我的Repository:

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

}

这是我的Service:

@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"
    }
  ]
}

当我保存它时,在我的终端中显示出来的Hibernate查询,但是当我检查我的数据库表(PostgreSQL)时,我发现有一个字段"account_account_id"为空,我错过了什么?

我希望Hibernate运行的SQL如下所示:

insert into account (account_id, account_name, ...等)
values ('USA001', 1)
 
insert into dealer (account_account_id, dealer_name, dealer_id, ...等)
values ('USA001', 'New dealer 1', 'MMO001')

这是我尝试后更新的模型:

我的Account.java
我删除了*cascade = CascadeType.ALL, orphanRemoval = true*

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

    @Id
    @NotBlank(message = "不能为空")
    @Size(min = 0, max = 20)
    public String accountId;

    @NotBlank(message = "不能为空")
    public String accountName;

    @NotBlank(message = "不能为空")
    @Email(message = "应该是正确的电子邮件")
    public String accountEmail;

    @NotBlank(message = "不能为空")
    @Size(min = 5, message = "至少5个字符")
    public String accountAddress;

    @NotBlank(message = "不能为空")
    public String town;

    @NotBlank(message = "不能为空")
    public String npwp;

    @NotBlank(message = "不能为空")
    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 = "不能为空")
    @Size(min = 0, max = 20)
    public String dealerId;

    @NotBlank(message = "不能为空")
    public String dealerName;

    @NotBlank(message = "不能为空")
    @Email(message = "应该是正确的电子邮件")
    public String dealerEmail;

    @NotBlank(message = "不能为空")
    @Size(min = 5, message = "至少5个字符")
    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

Please note that code snippets and annotations have been left untranslated for clarity.

英文:

Hi i am learning Spring JPA using OneToMany and ManyToOne bidirectional relationship, in some example i see OneToMany and ManyToOne relationship when i written in two side, the JPA add a new column as the foreign key column and insert the key value from the Parent table. But when i try mine, the column is always blank. Here is how my code looked like :

Here is my Account.java model :

@Entity
@Table(name = &quot;msAccount&quot;)
public class Account {

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

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

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

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

    @NotBlank(message = &quot;Not Blank&quot;)
    public String town;
    
    @NotBlank(message = &quot;Not Blank&quot;)
    public String npwp;

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

    public String fax;

    public String remarks;

    @NotNull
    public Date entryTime;

    @NotNull
    public Boolean active;

    @OneToMany(mappedBy=&quot;account&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
    public List&lt;Dealer&gt; dealer;

//getter setter skipped
    
}

and here is my Dealer.java model :

@Entity
@Table(name = &quot;msDealer&quot;)
public class Dealer {

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

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

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

    @NotBlank(message = &quot;Tidak Boleh Kosong&quot;)
    @Size(min = 5, message = &quot;Minimal 5 karakter&quot;)
    public String dealerAddress;
    
    @ManyToOne(fetch = FetchType.LAZY)
    public Account account;

//getter setter skipped

}

and here is my Repository :

@Repository
public interface AccountRepository extends JpaRepository&lt;Account, Long&gt; {

}

and here is my Service :

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

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

}

and here is my controller :

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

    @Autowired
    private AccountService accountService;

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

}

then i post the JSON into my save endpoint :

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

and when i save it the hibernate that showed up in my terminal looked inserting query into that 2 table, but when i check my database table (which is postgresql) i found there is a field "account_account_id" that is null, what did i miss here?

i want the Hibernate run sql like this :

insert into account (account_id, account_name, ...etc)
values (&#39;USA001&#39;, 1)
 
insert into dealer (account_account_id, dealer_name, dealer_id, ...etc)
values (&#39;USA001&#39;, &#39;New dealer 1&#39;, &#39;MMO001&#39;)

Here is my UPDATED Model after some try :

my Account.java
I delete cascade = CascadeType.ALL, orphanRemoval = true

@Entity
@Table(name = &quot;msAccount&quot;)
public class Account {

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

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

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

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

    @NotBlank(message = &quot;Tidak Boleh Kosong&quot;)
    public String town;
    
    @NotBlank(message = &quot;Tidak Boleh Kosong&quot;)
    public String npwp;

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

    public String fax;

    public String remarks;

    @NotNull
    public Date entryTime;

    @NotNull
    public Boolean active;

    @OneToMany(mappedBy=&quot;account&quot;)
    // @JoinColumn(name = &quot;accountId&quot;)
    public List&lt;Dealer&gt; dealer;

//getter setter skipped
    
}

and here is my Dealer.java. Added @JoinColumn :

@Entity
@Table(name = &quot;msDealer&quot;)
public class Dealer {

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

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

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

    @NotBlank(message = &quot;Tidak Boleh Kosong&quot;)
    @Size(min = 5, message = &quot;Minimal 5 karakter&quot;)
    public String dealerAddress;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;account_id&quot;)
    public Account account;

//getter setter skipped

}

now the error is getting weird, i got this error when i save the JSON data

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

in some tutorial it worked, but mine is not, what did i do wrong?

here is my github repo : https://github.com/Fly-Away/LearningSpring

答案1

得分: 11

你在子类中缺少@JoinColumn

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

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

    // 其他字段

}

你在父类中使用了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<Dealer>上添加@JsonIgnore,以避免在序列化为JSON时出现堆栈溢出。

英文:

You're missing the @JoinColumn on the child side:

@Entity
@Table(name = &quot;ms_dealer&quot;)
public class Dealer {

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

    // other fields

}

You have used mappedBy on the parent side, but there is no mapping on the child side. You need to indicate, that the Dealer is the relationship owner - it has the foreign key.

Edit: if you're persisting (not merging) the Account entity, together with its children, you should not pass ids of child entities. (Actually passing any ids upon persist is a code smell and most probably a performance killer.) The json used should look like:

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

Before saving both-side synchronization might also be needed:

account.getDealer().forEach(d -&gt; d.setAccount(account));

Edit:

From Author edits must cascade to child:

@OneToMany(mappedBy = &quot;account&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
public List&lt;Dealer&gt; dealer;

You might also add @JsonIgnore over Action or List&lt;Dealer&gt; to avoid stackoverflow on serialization to json.

答案2

得分: 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,因为当前我们已经移除了Dealer一侧的映射,并添加了@JoinColumn来定义我们正在用于账户引用的列。

英文:

To save child with parent in the bidirectional relationship set parent in child entity also to sync both side.

Here set account reference in dealer objects

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

Update:

But if you want to use Unidirectional relation then remove Account relation in Dealer Entity. Remove this portion

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

Then update the relation in Account table.

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = &quot;account_id&quot;)
public List&lt;Dealer&gt; dealer;

Here we remove mappedBy because currently we removed mapping in Dealerside and add @JoinColumn to define which column we are using for account refference.

答案3

得分: 1

你所说的你正在学习,我想给你一个详细的答案,这样你就能更容易理解。你在这里缺少的是 @JoinColumn

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

根据文档

由于在 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 设置为 insertable=falseupdatable=false。这个解决方案不是优化的,会产生一些额外的 UPDATE 语句。

@Entity
public class Troop {
    @OneToMany
    @JoinColumn(name="troop_fk") //我们需要重复物理信息
    public Set<Soldier> getSoldiers() {
        ...
    }
}

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

如果对上述解释有进一步的问题,请在下方留言。 为什么Spring JPA的双向一对多和多对一关系不能更新外键列呢?

英文:

As you said you are learning, I would like to give you a detailed answer so it will be easy for you to understand. What you are missing here is @JoinColumn.

@JoinColumn could be used on both sides of the relationship. The point here is in physical information duplication (column name) along with not optimized SQL query that will produce some additional UPDATE statements.

According to documentation:

Since many to one are (almost) always the owner side of a bidirectional relationship in the JPA spec, the one to many association is annotated by @OneToMany(mappedBy=...)

Understand by basic code example

@Entity
public class Troop {
	@OneToMany(mappedBy=&quot;troop&quot;)
	public Set&lt;Soldier&gt; getSoldiers() {
	...
}

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

Troop has a bidirectional one to many relationship with Soldier through the troop property. You don't have to (must not) define any physical mapping in the mappedBy side.

To map a bidirectional one to many, with the one-to-many side as the owning side, you have to remove the mappedBy element and set the many to one @JoinColumn as insertable and updatable to false. This solution is not optimized and will produce some additional UPDATE statements.

@Entity
public class Troop {
	@OneToMany
	@JoinColumn(name=&quot;troop_fk&quot;) //we need to duplicate the physical information
	public Set&lt;Soldier&gt; getSoldiers() {
	...
}

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

Comment below if you have any further questions on the explanation given. 为什么Spring JPA的双向一对多和多对一关系不能更新外键列呢?

答案4

得分: 0

如果在两个实体之间存在双向关系(这里是AccountDealer),您需要决定哪一边是该关系的拥有者。
默认情况下,One一侧是拥有者,这会导致一个连接表,在修改列表时会更新该表。

由于您定义了mappedBy属性(@OneToMany(mappedBy = &quot;account&quot;)),Many一侧成为了关系的拥有者。这意味着msDealer表中的account列将保存Account的外键,然后将不再使用连接表。连接表可能是在您在注解中添加mappedBy定义之前初始化数据库时遗留下来的。

您有以下选项:

  1. Dealer保持为拥有者,并且不使用连接表。如果您想在数据库中观察副作用,请查看msDealer.account列。
  2. 使用@JoinTable注解强制使用这样的表。
英文:

If you have a bidirectional relationship between two entities (here Account and Dealer) you have to decide which side is the owner of said relationship.
By default the One-side is the owner which leads to a Join-Table which is updated when modifying the List.

Since you defined the mappedBy property (@OneToMany(mappedBy = &quot;account&quot;)) the Many-side is the owner of the relationship. This means the account column in the msDealer Table will hold the foreign key of the Account and then Join-Table will not be used anymore. The Join-Table is probably a left over from initializing the database before you added the mappedBy definition to the annotation.

Options you have:

  1. Let the Dealer stay the owner and don't use a Join-Table. If you want to observe side effects in the database look at column msDealer.account.
  2. Use the @JoinTable annotation to enforce the usage of such a table

huangapple
  • 本文由 发表于 2020年4月7日 23:08:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/61083247.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定