Reputation: 3750
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
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
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 Dealer
side and add @JoinColumn
to define which column we are using for account refference.
Upvotes: 1
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
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:
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
.@JoinTable
annotation to enforce the usage of such a tableUpvotes: 0