简体   繁体   中英

<SpringBoot / Hibernate> InvocationException on calling JpaRepository.findAll(Example example)

I am noticing an InvocationException being returned when I execute a JpaRepository.findAll(Example example) on H2 database.

It occurs when I try to configure the foregin key relationship between the 2 tables "Account" and "Transaction" (ie An account can have many transactions, but a transaction can only belong to one account).

Before I add the @OneToMany and @ManyToOne annotations, there were no issues.

Welcome for any help, thank you.

Request:

在此处输入图片说明

Query is successful but it gives an InvocationException, which in turns give a HTTP500.

在此处输入图片说明

Service:

AccountService.java

...
......
public List<Transaction> getAllTransactions(Account account) {

    TransactionPK inputTransactionPK = new TransactionPK();
    inputTransactionPK.setAccountNum(account.getAccountNum());

    Transaction inputTransaction = new Transaction();
    inputTransaction.setTransactionPK(inputTransactionPK);

    ExampleMatcher matcher = ExampleMatcher.matchingAll().withIgnorePaths("debitAmt", "creditAmt");

    Example<Transaction> example = Example.of(inputTransaction, matcher);

    List<Transaction> transactionList = transactionRepository.findAll(example);

    log.info("##################################################\n"
            + "Retrieved transaction list for account with account number " + account.getAccountNum()
            + "\n##################################################");

    return transactionList;
}
...
......

Table models:

Account.java

package com.somecompany.account.model;

import java.sql.Timestamp;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import lombok.Data;

@Entity
@Data
public class Account {

    @OneToMany(mappedBy = "account")
    private Set<Transaction> transaction;

    @Column(name = "cust_id")
    @NotEmpty(message = "Customer ID cannot be null nor empty")
    @Pattern(regexp = "^[0-9]+$", message = "Customer ID must be a number")
    @Min(value = 1L, message = "Customer ID must not be less than 1")
    @Max(value = 9999999999L, message = "Customer ID must not be larger than 9999999999")
    private long custId;

    @Column(name = "account_num")
    @Id
    @NotEmpty(message = "Account number cannot be null nor empty")
    @Pattern(regexp = "^[0-9]+$", message = "Account number must be a number")
    @Min(value = 1L, message = "Account number  must not be less than 1")
    @Max(value = 9999999999L, message = "Account number must not be larger than 9999999999")
    private long accountNum;

    @Column(name = "account_name")
    @NotEmpty(message = "Account name cannot be null nor empty")
    @Size(min = 1, max = 30, message = "Account name must have length between 1 and 30")
    private String accountName;

    @Column(name = "account_type")
    @NotEmpty(message = "Account type cannot be null nor empty")
    @Size(min = 1, max = 7, message = "Account type must have length between 1 and 7")
    private String accountType;

    @Column(name = "balance_date")
    @NotEmpty(message = "Balance date cannot be null nor empty")
    private Timestamp balanceDate;

    @Column(name = "currency")
    @NotEmpty(message = "Currency cannot be null nor empty")
    @Size(min = 3, max = 3, message = "Currency must have length exactly equal to 3")
    private String currency;

    @Column(name = "opening_available_balance", columnDefinition = "Decimal(20,2) default '0.0'")
    @NotEmpty(message = "Opening available balance cannot be null nor empty")
    @Pattern(regexp = "^[0-9.]+$", message = "Opening available balance must be a decimal number")
    @DecimalMin(value = "0.0", message = "Opening available balance cannot be negative")
    private float openingAvailableBalance;
}

Transaction.java

package com.somecompany.account.model;

import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

import lombok.Data;

@Entity
@Data
public class Transaction {

    @ManyToOne
    @JoinColumn(name = "account_num", referencedColumnName = "account_num", insertable = false, updatable = false, nullable = false)
    private Account account;

    @EmbeddedId
    private TransactionPK transactionPK;

    @Column(name = "account_name")
    @NotEmpty(message = "Account name cannot be null nor empty")
    @Size(min = 1, max = 30, message = "Account name must have length between 1 and 30")
    private String accountName;

    @Column(name = "currency")
    @NotEmpty(message = "Currency cannot be null nor empty")
    @Size(min = 3, max = 3, message = "Currency must have length exactly equal to 3")
    private String currency;

    @Column(name = "debit_amt", columnDefinition = "Decimal(20,2) default '0.0'")
    @NotEmpty(message = "Debit amount cannot be null nor empty")
    @DecimalMin(value = "0.0", message = "Debit amount cannot be negative")
    private float debitAmt;

    @Column(name = "credit_amt", columnDefinition = "Decimal(20,2) default '0.0'")
    @NotEmpty(message = "Credit amount cannot be null nor empty")
    @DecimalMin(value = "0.0", message = "Credit amount cannot be negative")
    private float creditAmt;

    @Column(name = "debit_credit")
    @NotEmpty(message = "Debit/Credit cannot be null nor empty")
    @Size(min = 1, max = 6, message = "Debit/Credit must have length between 1 and 6")
    private String debitCredit;

    @Column(name = "transaction_narrative")
    @Size(min = 0, max = 50, message = "Transaction narrative must have length between 0 and 50")
    private String transactionNarrative;
}

TransactionPK.java

package com.somecompany.account.model;

import java.io.Serializable;
import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

import lombok.Data;

@Embeddable
@Data
public class TransactionPK implements Serializable {

    /**
    * 
    */
    private static final long serialVersionUID = 1L;

    @Column(name = "account_num")
    @NotEmpty(message = "Account number cannot be null nor empty")
    @Pattern(regexp = "^[0-9]+$", message = "Account number must be a number")
    @Min(value = 1L, message = "Account number  must not be less than 1")
    @Max(value = 9999999999L, message = "Account number must not be larger than 9999999999")
    private long accountNum;

    @Column(name = "value_date")
    @NotEmpty(message = "Value date cannot be null nor empty")
    private Timestamp valueDate;
}

H2 DB primary and foreign key info:

在此处输入图片说明

Sample DB data on SpringBoot app startup (data.sql):

INSERT INTO ACCOUNT (cust_id, account_num, account_name, account_type, balance_date, currency, opening_available_balance) VALUES
(1111111111, 1111111111, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'Savings', TIMESTAMP '2020-11-01 11:01:01', 'SGD', 99999.99),
(2, 2, 'B', 'Savings', TIMESTAMP '2020-11-02 11:02:02', 'AUD', 0.0),
(1111111111, 3333333333, 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', 'Current', TIMESTAMP '2020-11-03 11:03:03', 'USD', 99999.99);

INSERT INTO TRANSACTION (account_num, account_name, value_date, currency, debit_amt, credit_amt, debit_credit, transaction_narrative) VALUES
(1111111111, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', TIMESTAMP '2012-11-01 11:01:01', 'SGD', 0.0, 99999.99, 'Credit', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'),
(2, 'Savings Account', TIMESTAMP '2012-11-02 11:02:02', 'USD', 0.1, 0.0, 'Debit', null),
(1111111111, 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', TIMESTAMP '2012-11-03 11:03:03', 'USD', 99999.99, 0.0, 'Debit', 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC');

在此处输入图片说明

After some investigation, I have made the following changes and the application run as expected finally.

Account.java

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import com.fasterxml.jackson.annotation.JsonBackReference;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
/**
* The model class for "Account" table.
* 
* @author patrick
*
*/
public class Account {

    @OneToMany(mappedBy = "transactionPK.account")
    @JsonBackReference
    private List<Transaction> transactions = new ArrayList<>();

    @Column(name = "cust_id")
    @NotEmpty(message = "Customer ID cannot be null nor empty")
    @Pattern(regexp = "^[0-9]+$", message = "Customer ID must be a number")
    @Min(value = 1L, message = "Customer ID must not be less than 1")
    @Max(value = 9999999999L, message = "Customer ID must not be larger than 9999999999")
    private long custId;

    @Column(name = "account_num")
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @NotEmpty(message = "Account number cannot be null nor empty")
    @Pattern(regexp = "^[0-9]+$", message = "Account number must be a number")
    @Min(value = 1L, message = "Account number  must not be less than 1")
    @Max(value = 9999999999L, message = "Account number must not be larger than 9999999999")
    private long accountNum;

    @Column(name = "account_name")
    @NotEmpty(message = "Account name cannot be null nor empty")
    @Size(min = 1, max = 30, message = "Account name must have length between 1 and 30")
    private String accountName;

    @Column(name = "account_type")
    @NotEmpty(message = "Account type cannot be null nor empty")
    @Size(min = 1, max = 7, message = "Account type must have length between 1 and 7")
    private String accountType;

    @Column(name = "balance_date")
    @NotEmpty(message = "Balance date cannot be null nor empty")
    private Timestamp balanceDate;

    @Column(name = "currency")
    @NotEmpty(message = "Currency cannot be null nor empty")
    @Size(min = 3, max = 3, message = "Currency must have length exactly equal to 3")
    private String currency;

    @Column(name = "opening_available_balance", columnDefinition = "Decimal(20,2) default '0.0'")
    @NotEmpty(message = "Opening available balance cannot be null nor empty")
    @Pattern(regexp = "^[0-9.]+$", message = "Opening available balance must be a decimal number")
    @DecimalMin(value = "0.0", message = "Opening available balance cannot be negative")
    private float openingAvailableBalance;
}

Transaction.java

import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
/**
* The model class for "Transaction" table.
* 
* @author patrick
*
*/
public class Transaction {

    @EmbeddedId
    private TransactionPK transactionPK;

    @Column(name = "account_name")
    @NotEmpty(message = "Account name cannot be null nor empty")
    @Size(min = 1, max = 30, message = "Account name must have length between 1 and 30")
    private String accountName;

    @Column(name = "currency")
    @NotEmpty(message = "Currency cannot be null nor empty")
    @Size(min = 3, max = 3, message = "Currency must have length exactly equal to 3")
    private String currency;

    @Column(name = "debit_amt", columnDefinition = "Decimal(20,2) default '0.0'")
    @NotEmpty(message = "Debit amount cannot be null nor empty")
    @DecimalMin(value = "0.0", message = "Debit amount cannot be negative")
    private float debitAmt;

    @Column(name = "credit_amt", columnDefinition = "Decimal(20,2) default '0.0'")
    @NotEmpty(message = "Credit amount cannot be null nor empty")
    @DecimalMin(value = "0.0", message = "Credit amount cannot be negative")
    private float creditAmt;

    @Column(name = "debit_credit")
    @NotEmpty(message = "Debit/Credit cannot be null nor empty")
    @Size(min = 1, max = 6, message = "Debit/Credit must have length between 1 and 6")
    private String debitCredit;

    @Column(name = "transaction_narrative")
    @Size(min = 0, max = 50, message = "Transaction narrative must have length between 0 and 50")
    private String transactionNarrative;
}

TransactionPK.java

import java.io.Serializable;
import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.validation.constraints.NotEmpty;

import com.fasterxml.jackson.annotation.JsonManagedReference;

import lombok.Data;

@Embeddable
@Data
/**
* The model class for the EmbeddedId (i.e. primary key) of the "Transaction" table.
* 
* @author patrick
*
*/
public class TransactionPK implements Serializable {

    /**
    * 
    */
    private static final long serialVersionUID = 1L;

    @ManyToOne
    @JoinColumn(name = "account_num", referencedColumnName = "account_num", insertable = false, updatable = false, nullable = false)
    @JsonManagedReference
    private Account account;

    @Column(name = "value_date")
    @NotEmpty(message = "Value date cannot be null nor empty")
    private Timestamp valueDate;
}

I have created an "account" field in TransactionPK replacing the "account_num" field (the account object already has the "account_num" info anyway), and annotated it with @ManyToOne. This is because the releationship is "An account can have many transactions (ie list of transactions), but a transaction only belongs to one account". The releationship is at the object level but not field level.

For the "List transactions" in "Account" and "Account account" in "TransactionPK", they are for indicating the foreign key relationship only, they don't have to be existing in the JSON files. And if we just leave it like that, it will give an infinite recursion error when serializing to JSON (since each has an element of the other, it can never finish generating the JSON).

To solve the issue, we may mark these fields with @JsonIgnore, which will skip serializing both fields. Or if we need to show one but not the other (eg show "account" in Transaction JSON, but not showing "transactions" in Account JSON), then we can annotate the one which we want to keep with the @JsonManagedReference, and mark the one that we don't want to show in JSON with @JsonBackReference.

Referece: https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion

Another change I made is not using @Data. Avoid using @Data from lombok when you are working with entities if they will be used with ORM like JPA.The @Data's implementation of equals() and hashCode() may mess with the object comparison.

Referece: https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM