Ke Vin
Ke Vin

Reputation: 3750

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

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 = "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

}

and here is my Dealer.java model :

@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

}

and here is my Repository :

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

}

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("/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();
        }
    }

}

then i post the JSON into my save endpoint :

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

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 ('USA001', 1)

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

Here is my UPDATED Model after some try :

my Account.java I delete 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

}

and here is my Dealer.java. Added @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

}

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

> "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"

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

Upvotes: 1

Views: 18956

Answers (4)

Andronicus
Andronicus

Reputation: 26036

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

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_account_id")
    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:

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

Before saving both-side synchronization might also be needed:

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

Edit:

From Author edits must cascade to child:

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

You might also add @JsonIgnore over Action or List<Dealer> to avoid stackoverflow on serialization to json.

Upvotes: 11

Eklavya
Eklavya

Reputation: 18410

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 = "account_id")
public List<Dealer> 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.

Upvotes: 1

Kunal Vohra
Kunal Vohra

Reputation: 2846

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="troop")
    public Set<Soldier> getSoldiers() {
    ...
}

@Entity
public class Soldier {
    @ManyToOne
    @JoinColumn(name="troop_fk")
    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="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() {
    ...
}

Comment below if you have any further questions on the explanation given. :)

Upvotes: 1

Trinova
Trinova

Reputation: 473

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 = "account")) 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

Upvotes: 0

Related Questions