如何在数据库中存储功能逻辑
How to store function logic in the database
我正在制作财务管理应用程序。我有一个数据库,其中包含用户拥有他的钱的所有地方,其中包括银行。这是 table 的结构..
CREATE TABLE IF NOT EXISTS reserves (
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) NOT NULL,
balance DECIMAL(10, 2) NOT NULL
)
CREATE TABLE IF NOT EXISTS banks (
reserve_id SMALLINT UNSIGNED UNIQUE NOT NULL,
apy DECIMAL(4, 2) NOT NULL,
accrued_interest DECIMAL(10, 4) NOT NULL,
last_transaction DATE,
FOREIGN KEY(reserve_id) REFERENCES reserves(id)
)
在这个模型中,我可以有一个固定的 APY,它将在插入时设置。但在现实世界中,银行有基于余额的可变利率。银行table.
中每个银行的具体情况都不同
在 JAVA class 中,我可以使用定义为 Function<BigDecimal, Big Decimal> APY
的 APY 非常轻松地捕获它,我可以在其中存储特定的 APY 逻辑并使用 APY.apply(balance)
来检索任何时候的利率。
但我不知道如何将此逻辑存储在 mySQL 数据库中。
我知道我可以创建一个单独的 table,例如 bank_balance_interest,我可以在其中将利率存储到特定银行的 ID 的最低余额,然后参考它。
但就是感觉不对。一方面,它非常繁琐和乏味。此外,如果没有明确的利益平衡边界,仍然不会有任何解决方案,而是一个连续的函数。
有没有更优雅的方法?
编辑
这是我的一些代码:
public class Reserve {
short id;
final String name;
BigDecimal balance;
ReservesData reservesData;
public Reserve(short id, String name, BigDecimal balance) {
this.id = id;
this.name = name;
this.balance = balance;
reservesData = ReservesData.instance;
}
public Reserve(String name) {
this((short) -1, name, new BigDecimal("0.0"));
}
@Override
public String toString() {
return name;
}
public short getId() {
return id;
}
public String getName() {
return name;
}
public BigDecimal getBalance() {
return balance;
}
public boolean transact(BigDecimal amount) {
if(balance.add(amount).compareTo(new BigDecimal("0.0")) < 0) return false;
balance = balance.add(amount);
return true;
}
public boolean save() {
if(id == -1)
return (id = reservesData.addReserve(this)) != -1;
return reservesData.updateReserve(this);
}
}
.
public class Bank extends Reserve{
private final Function<BigDecimal, BigDecimal> APY;
private BigDecimal accruedInterest;
private Date lastTransactionDate;
private final BanksData banksData;
public Bank(short id, String name, BigDecimal balance, Function<BigDecimal, BigDecimal> APY) {
super(id, name, balance);
this.APY = APY;
accruedInterest = new BigDecimal("0.0");
banksData = BanksData.instance;
}
public Bank(String name, Function<BigDecimal, BigDecimal> APY) {
this((short) -1, name, new BigDecimal("0.0"), APY);
}
@Override
public BigDecimal getBalance() {
return balance.add(accruedInterest);
}
public Function<BigDecimal, BigDecimal> getAPY() {
return APY;
}
public BigDecimal getAccruedInterest() {
return accruedInterest;
}
public void setAccruedInterest(BigDecimal accruedInterest) {
this.accruedInterest = accruedInterest;
}
.
public class ReservesDAO implements ReservesData {
public ReservesDAO() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
stmt.executeUpdate("""
CREATE TABLE IF NOT EXISTS reserves (
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) NOT NULL,
balance DECIMAL(10, 2) NOT NULL
)"""
);
} catch (SQLException sqlException) {
System.out.println("Failed to create reserves table on the database!");
sqlException.printStackTrace();
}
}
@Override
public short addReserve(Reserve reserve) {
try (
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
INSERT INTO reserves (name, balance) VALUES (?, ?)""", Statement.RETURN_GENERATED_KEYS
)
) {
pstmt.setString(1, reserve.getName());
pstmt.setBigDecimal(2, reserve.getBalance());
pstmt.executeUpdate();
ResultSet rs = pstmt.getGeneratedKeys();
if (rs.next())
return rs.getShort(1);
else throw new RuntimeException("Auto-Generated ID was not returned from reserves!");
} catch (SQLException sqlException) {
System.out.println("Failed to insert " + reserve.getName() + " info in the database!");
sqlException.printStackTrace();
return -1;
}
}
public Reserve getReserve(short id) {
try(
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
SELECT * FROM reserves WHERE id = ?""")
) {
pstmt.setShort(1, id);
ResultSet rs = pstmt.executeQuery();
if(rs.next())
return new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3));
else throw new RuntimeException("No reserve found on the database with the id " + id);
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserve from the database!");
sqlException.printStackTrace();
return null;
}
}
public List<Reserve> getAllReserves() {
List<Reserve> reserves = new ArrayList<>();
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM reserves");
while(rs.next())
reserves.add(new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3)));
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserves from the database!");
sqlException.printStackTrace();
}
return reserves;
}
@Override
public BigDecimal getTotalReserveBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves""");
if(rs.next()) return rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total reserve balance from database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public List<Reserve> getAllWallets() {
List<Reserve> reserves = new ArrayList<>();
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT reserves.* FROM reserves
LEFT JOIN banks ON reserves.id = banks.id
WHERE banks.id IS NULL
""");
while(rs.next())
reserves.add(new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3)));
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserves from the database!");
sqlException.printStackTrace();
}
return reserves;
}
@Override
public BigDecimal getTotalWalletBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves
LEFT JOIN banks ON reserves.id = banks.id
WHERE banks.id IS NULL
""");
if(rs.next())
return rs.getBigDecimal(1) == null ? new BigDecimal("0.0") : rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total wallet balance from database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public boolean updateReserve(Reserve reserve) {
try(PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
UPDATE reserves SET name = ?, balance = ? WHERE id = ?""")
) {
pstmt.setString(1, reserve.getName());
pstmt.setBigDecimal(2, reserve.getBalance());
pstmt.setShort(3, reserve.getId());
pstmt.executeUpdate();
return true;
} catch(SQLException sqlException) {
System.out.println("Failed to update reserves with new data!");
sqlException.printStackTrace();
return false;
}
}
}
...
public class BanksDAO extends ReservesDAO implements BanksData {
public BanksDAO() {
try(
Statement stmt = MyConnection.getMySQLconnection().createStatement()
) {
stmt.executeUpdate("""
CREATE TABLE IF NOT EXISTS banks (
id SMALLINT UNSIGNED UNIQUE NOT NULL,
apy DECIMAL(4, 2) NOT NULL, // I have no way to store a logic here, so currently it only stores fixed value.
accrued_interest DECIMAL(10, 4) NOT NULL,
last_transaction_date DATE,
FOREIGN KEY(id) REFERENCES reserves(id)
)"""
);
} catch (SQLException sqlException) {
System.out.println("Failed to create banks table on the database!");
sqlException.printStackTrace();
}
}
@Override
public short addBank(Bank bank) {
try (
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
INSERT INTO banks(id, apy, accrued_interest, last_transaction_date) VALUES (?, ?, ?, ?)"""
)
) {
short id = addReserve(bank);
pstmt.setShort(1, id);
pstmt.setBigDecimal(2, bank.getAPY());
pstmt.setBigDecimal(3, bank.getAccruedInterest());
pstmt.setDate(4, bank.getLastTransactionDate());
pstmt.executeUpdate();
return id;
} catch (SQLException sqlException) {
System.out.println("Failed to insert " + bank.getName() + " info in the database!");
sqlException.printStackTrace();
return -1;
}
}
@Override
public Bank getBank(short reserve_id) {
try(
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
SELECT * FROM reserves NATURAL JOIN banks WHERE id = ?""")
) {
pstmt.setShort(1, reserve_id);
ResultSet rs = pstmt.executeQuery();
if(!rs.next()) return null;
Bank requestedBank = new Bank(rs.getShort(1), rs.getString(2),
rs.getBigDecimal(3), rs.getBigDecimal(4));
requestedBank.setAccruedInterest(rs.getBigDecimal(5));
requestedBank.setLastTransactionDate(rs.getDate(6));
return requestedBank;
} catch (SQLException sqlException) {
System.out.println("Failed to fetch bank data from the database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public List<Bank> getAllBanks() {
List<Bank> allBanks = new ArrayList<>();
try(
Statement stmt = MyConnection.getMySQLconnection().createStatement()
) {
ResultSet rs = stmt.executeQuery("SELECT * FROM reserves NATURAL JOIN banks");
while(rs.next()) {
Bank bank = new Bank(rs.getShort(1), rs.getString(2),
rs.getBigDecimal(3), rs.getBigDecimal(4));
bank.setAccruedInterest(rs.getBigDecimal(5));
bank.setLastTransactionDate(rs.getDate(6));
allBanks.add(bank);
}
return allBanks;
} catch (SQLException sqlException) {
System.out.println("Failed to fetch bank data from the database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public BigDecimal getTotalBankBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves NATURAL JOIN banks""");
if(rs.next())
return rs.getBigDecimal(1) == null ? new BigDecimal("0.0") : rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total bank balance from database!");
sqlException.printStackTrace();
return null;
}
}
}
现在我可以将银行初始化为:
Bank bank1 = new Bank("TestBank1", balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5"));
虽然我可以创建另一个银行:
Bank bank2 = new Bank("TestBank2", balance -> balance.compareTo(new BigDecimal("8000")) == -1 ? new BigDecimal("3.5") : new BigDecimal("5.3"));
现在这两个库都是在内存中创建的,只要应用程序是 运行 就可以完美运行。但是当我需要长期使用它时,我不能直接将 Funtion 类型的变量存储到 mysql 数据库中。
许多人建议使用存储过程,如果对银行 table 中的每个银行只有 1 个类似 balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5")
的逻辑,那将会奏效,但此信息每次都会更改。这意味着如果我的银行中有 50 个条目 table 我需要为我的银行中的每个条目创建 50 个具有 50 种逻辑的不同存储过程 table,以便随着余额的变化不断更新 APY 字段.我认为可能有更好的方法不?
Advice/opinion
数据库用于永久存储数字和文本。
应用程序是用来计算和决定事情的。
也就是说,您描述的复杂业务逻辑可能用Java(或其他应用程序语言)编码更合适。同时,利率的断点等应该存储在数据库中。
换句话说:“业务逻辑”属于应用程序,而不属于数据库。 (当然,它们之间有一条灰线。例如,SQL 非常擅长汇总 table 中的所有数据;因此,我会在 SQL 中这样做,不是 Java.)
十进制
银行有挑剔的规则,这些规则可能与 DECIMAL
或 DOUBLE
提供的规则不同——在 Java 或 MySQL 或_任何其他计算机语言中。注意可能需要的规则。特别是,DECIMAL(10, 4)
在您的应用程序中不太可能足够。另一方面,如果呈现给客户的是 apy DECIMAL(4, 2)
可能就足够了。但是,再次提醒,生成该数字时要注意舍入规则。您可能被迫手动输入该号码。
注意 DECIMAL
和 DOUBLE
之间舍入特性的差异。
回答
如果您选择在数据库中实现算法,请参阅 CREATE STORED PROCEDURE
和 CREATE FUNCTION
。
存储过程可以用来封装一组SQL语句。它可以接收和发送字符串和数字,但不能接收和发送数组。它能够读取 tables(因此是某种“数组”。)
可以在表达式出现的任何地方调用函数。它可以接收一些 numbers/strings 并产生一个数字或字符串。
数组
我不清楚需要什么,但我设想 table 几行或几十行,每一行都说“对于高达 $xxx 的值,使用 y.yy%利率"。
存储过程有“游标”和“循环”,但它们很笨拙;应用程序语言可能有更好的代码。
我认为你被误解了。您可能不是在问如何将逻辑移动到数据库中(存储过程就是答案),而是在问如何将您在代码中实现的逻辑存储到数据库中,以便稍后恢复该状态。所以这是我回答的前提。
你的这些计算相关的设计不是很好。您绝对应该考虑将这些 APY 计算方法表达为 类 实现某种 IAPYCalculationMethod
接口,并使用一种方法来实际计算它。匿名 lambda 就像你拥有的那样不适合这个目的。
那么假设您确实有一个带有 CalculateAPY
方法的 IAPYCalculatioMethod
接口,然后是一个如下所示的 BalanceBasedCalculationMethod
:
// code is not complete, just to give you an idea
class BalanceBasedCalculationMethod implements IAPYCalculationMethod {
public BalanceBasedCalculationMethod(BigDecimal balanceThreshold, BigDecimal whenLower, BigDecimal whenGreater) { ... }
public BigDecimal CalculateAPY(Bank bank) {
if (bank.getBalance() < this.balanceThreshold)
return this.whenLower;
else
return this.whenGreater;
}
}
然后当您创建一个新银行时:
bank1 = new Bank("TestBank1", new BalanceBasedCalculationMethod(10000, 4, 5));
bank2 = new Bank("TestBank2", new BalanceBasedCalculationMethod(8000, 3.5, 5.3));
这已经好多了。这还允许您以某种方式序列化所有这些。你可以有一个 table 包含所有计算方法,银行和所用方法之间的关系,以及一个 JSON 参数(因为不同的方法可能有不同的参数;你可能还想将它们存储在一个单独的 table 而不是 JSON).
CREATE TABLE IF NOT EXISTS apy_calculation_method
(
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY
name VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS bank_calculation_method
(
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
bank_id SMALLINT UNSIGNED NOT NULL REFERENCES reserves(id),
method_id SMALLINT UNSIGNED NOT NULL REFERENCES apy_calculation_method(id),
arguments JSON
);
为了表示我们基于余额的方法和使用它的银行,我们将有:
INSERT INTO apy_calculation_method (name) VALUES ('Balance Based');
那么每个银行的方法:
INSERT INTO bank_calculation_method (bank_id, method_id, arguments)
VALUES (1, 1, '{"balance": 10000, "whenLower": 4, "whenGreater": 5}')
, (2, 1, '{"balance": 8000, "whenLower": 3.5, "whenGreater": 5.3}');
还有银行:
INSERT INTO banks (id, bank_method_id, accrued_interest)
VALUES (1, 1, 0) -- first bank using the first method (balance based with 10000 balance)
, (2, 2, 0) -- second bank using the second method (balance based with 8000 balance)
此方法可以更严格或更严格(规范化/非规范化),例如,您可以将计算方法作为 JSON 存储在银行 table 中(具有 {"method": "balance", "balance": 10000, .... }
结构排序)。您的计算方法工厂的设计以及它如何 serialized/deserializes 数据 to/from 数据库取决于它,但我相信您可以弄清楚。
这一切为您提供的是将计算方法序列化到数据库中的选项,而不是某种无法以任何方式引用的随机 lambda 函数。因此,无需将实际逻辑写入数据库,只需编写“使用的逻辑类型”和参数即可。
作为一个额外的好处,这创建了一个 testable 设计,您可以为 BalanceBasedCalculationMethod 编写一个测试以确保它实际执行它应该执行的操作。您无法自动测试这些 lambda 表达式,因为无法判断它们可以做什么或不能做什么,对吗?
我正在制作财务管理应用程序。我有一个数据库,其中包含用户拥有他的钱的所有地方,其中包括银行。这是 table 的结构..
CREATE TABLE IF NOT EXISTS reserves (
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) NOT NULL,
balance DECIMAL(10, 2) NOT NULL
)
CREATE TABLE IF NOT EXISTS banks (
reserve_id SMALLINT UNSIGNED UNIQUE NOT NULL,
apy DECIMAL(4, 2) NOT NULL,
accrued_interest DECIMAL(10, 4) NOT NULL,
last_transaction DATE,
FOREIGN KEY(reserve_id) REFERENCES reserves(id)
)
在这个模型中,我可以有一个固定的 APY,它将在插入时设置。但在现实世界中,银行有基于余额的可变利率。银行table.
中每个银行的具体情况都不同在 JAVA class 中,我可以使用定义为 Function<BigDecimal, Big Decimal> APY
的 APY 非常轻松地捕获它,我可以在其中存储特定的 APY 逻辑并使用 APY.apply(balance)
来检索任何时候的利率。
但我不知道如何将此逻辑存储在 mySQL 数据库中。
我知道我可以创建一个单独的 table,例如 bank_balance_interest,我可以在其中将利率存储到特定银行的 ID 的最低余额,然后参考它。
但就是感觉不对。一方面,它非常繁琐和乏味。此外,如果没有明确的利益平衡边界,仍然不会有任何解决方案,而是一个连续的函数。
有没有更优雅的方法?
编辑 这是我的一些代码:
public class Reserve {
short id;
final String name;
BigDecimal balance;
ReservesData reservesData;
public Reserve(short id, String name, BigDecimal balance) {
this.id = id;
this.name = name;
this.balance = balance;
reservesData = ReservesData.instance;
}
public Reserve(String name) {
this((short) -1, name, new BigDecimal("0.0"));
}
@Override
public String toString() {
return name;
}
public short getId() {
return id;
}
public String getName() {
return name;
}
public BigDecimal getBalance() {
return balance;
}
public boolean transact(BigDecimal amount) {
if(balance.add(amount).compareTo(new BigDecimal("0.0")) < 0) return false;
balance = balance.add(amount);
return true;
}
public boolean save() {
if(id == -1)
return (id = reservesData.addReserve(this)) != -1;
return reservesData.updateReserve(this);
}
}
.
public class Bank extends Reserve{
private final Function<BigDecimal, BigDecimal> APY;
private BigDecimal accruedInterest;
private Date lastTransactionDate;
private final BanksData banksData;
public Bank(short id, String name, BigDecimal balance, Function<BigDecimal, BigDecimal> APY) {
super(id, name, balance);
this.APY = APY;
accruedInterest = new BigDecimal("0.0");
banksData = BanksData.instance;
}
public Bank(String name, Function<BigDecimal, BigDecimal> APY) {
this((short) -1, name, new BigDecimal("0.0"), APY);
}
@Override
public BigDecimal getBalance() {
return balance.add(accruedInterest);
}
public Function<BigDecimal, BigDecimal> getAPY() {
return APY;
}
public BigDecimal getAccruedInterest() {
return accruedInterest;
}
public void setAccruedInterest(BigDecimal accruedInterest) {
this.accruedInterest = accruedInterest;
}
.
public class ReservesDAO implements ReservesData {
public ReservesDAO() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
stmt.executeUpdate("""
CREATE TABLE IF NOT EXISTS reserves (
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) NOT NULL,
balance DECIMAL(10, 2) NOT NULL
)"""
);
} catch (SQLException sqlException) {
System.out.println("Failed to create reserves table on the database!");
sqlException.printStackTrace();
}
}
@Override
public short addReserve(Reserve reserve) {
try (
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
INSERT INTO reserves (name, balance) VALUES (?, ?)""", Statement.RETURN_GENERATED_KEYS
)
) {
pstmt.setString(1, reserve.getName());
pstmt.setBigDecimal(2, reserve.getBalance());
pstmt.executeUpdate();
ResultSet rs = pstmt.getGeneratedKeys();
if (rs.next())
return rs.getShort(1);
else throw new RuntimeException("Auto-Generated ID was not returned from reserves!");
} catch (SQLException sqlException) {
System.out.println("Failed to insert " + reserve.getName() + " info in the database!");
sqlException.printStackTrace();
return -1;
}
}
public Reserve getReserve(short id) {
try(
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
SELECT * FROM reserves WHERE id = ?""")
) {
pstmt.setShort(1, id);
ResultSet rs = pstmt.executeQuery();
if(rs.next())
return new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3));
else throw new RuntimeException("No reserve found on the database with the id " + id);
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserve from the database!");
sqlException.printStackTrace();
return null;
}
}
public List<Reserve> getAllReserves() {
List<Reserve> reserves = new ArrayList<>();
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT * FROM reserves");
while(rs.next())
reserves.add(new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3)));
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserves from the database!");
sqlException.printStackTrace();
}
return reserves;
}
@Override
public BigDecimal getTotalReserveBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves""");
if(rs.next()) return rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total reserve balance from database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public List<Reserve> getAllWallets() {
List<Reserve> reserves = new ArrayList<>();
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT reserves.* FROM reserves
LEFT JOIN banks ON reserves.id = banks.id
WHERE banks.id IS NULL
""");
while(rs.next())
reserves.add(new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3)));
} catch (SQLException sqlException) {
System.out.println("Failed to fetch reserves from the database!");
sqlException.printStackTrace();
}
return reserves;
}
@Override
public BigDecimal getTotalWalletBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves
LEFT JOIN banks ON reserves.id = banks.id
WHERE banks.id IS NULL
""");
if(rs.next())
return rs.getBigDecimal(1) == null ? new BigDecimal("0.0") : rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total wallet balance from database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public boolean updateReserve(Reserve reserve) {
try(PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
UPDATE reserves SET name = ?, balance = ? WHERE id = ?""")
) {
pstmt.setString(1, reserve.getName());
pstmt.setBigDecimal(2, reserve.getBalance());
pstmt.setShort(3, reserve.getId());
pstmt.executeUpdate();
return true;
} catch(SQLException sqlException) {
System.out.println("Failed to update reserves with new data!");
sqlException.printStackTrace();
return false;
}
}
}
...
public class BanksDAO extends ReservesDAO implements BanksData {
public BanksDAO() {
try(
Statement stmt = MyConnection.getMySQLconnection().createStatement()
) {
stmt.executeUpdate("""
CREATE TABLE IF NOT EXISTS banks (
id SMALLINT UNSIGNED UNIQUE NOT NULL,
apy DECIMAL(4, 2) NOT NULL, // I have no way to store a logic here, so currently it only stores fixed value.
accrued_interest DECIMAL(10, 4) NOT NULL,
last_transaction_date DATE,
FOREIGN KEY(id) REFERENCES reserves(id)
)"""
);
} catch (SQLException sqlException) {
System.out.println("Failed to create banks table on the database!");
sqlException.printStackTrace();
}
}
@Override
public short addBank(Bank bank) {
try (
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
INSERT INTO banks(id, apy, accrued_interest, last_transaction_date) VALUES (?, ?, ?, ?)"""
)
) {
short id = addReserve(bank);
pstmt.setShort(1, id);
pstmt.setBigDecimal(2, bank.getAPY());
pstmt.setBigDecimal(3, bank.getAccruedInterest());
pstmt.setDate(4, bank.getLastTransactionDate());
pstmt.executeUpdate();
return id;
} catch (SQLException sqlException) {
System.out.println("Failed to insert " + bank.getName() + " info in the database!");
sqlException.printStackTrace();
return -1;
}
}
@Override
public Bank getBank(short reserve_id) {
try(
PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""
SELECT * FROM reserves NATURAL JOIN banks WHERE id = ?""")
) {
pstmt.setShort(1, reserve_id);
ResultSet rs = pstmt.executeQuery();
if(!rs.next()) return null;
Bank requestedBank = new Bank(rs.getShort(1), rs.getString(2),
rs.getBigDecimal(3), rs.getBigDecimal(4));
requestedBank.setAccruedInterest(rs.getBigDecimal(5));
requestedBank.setLastTransactionDate(rs.getDate(6));
return requestedBank;
} catch (SQLException sqlException) {
System.out.println("Failed to fetch bank data from the database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public List<Bank> getAllBanks() {
List<Bank> allBanks = new ArrayList<>();
try(
Statement stmt = MyConnection.getMySQLconnection().createStatement()
) {
ResultSet rs = stmt.executeQuery("SELECT * FROM reserves NATURAL JOIN banks");
while(rs.next()) {
Bank bank = new Bank(rs.getShort(1), rs.getString(2),
rs.getBigDecimal(3), rs.getBigDecimal(4));
bank.setAccruedInterest(rs.getBigDecimal(5));
bank.setLastTransactionDate(rs.getDate(6));
allBanks.add(bank);
}
return allBanks;
} catch (SQLException sqlException) {
System.out.println("Failed to fetch bank data from the database!");
sqlException.printStackTrace();
return null;
}
}
@Override
public BigDecimal getTotalBankBalance() {
try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {
ResultSet rs = stmt.executeQuery("""
SELECT SUM(balance) FROM reserves NATURAL JOIN banks""");
if(rs.next())
return rs.getBigDecimal(1) == null ? new BigDecimal("0.0") : rs.getBigDecimal(1);
return new BigDecimal("0.0");
} catch (SQLException sqlException) {
System.out.println("Could not get total bank balance from database!");
sqlException.printStackTrace();
return null;
}
}
}
现在我可以将银行初始化为:
Bank bank1 = new Bank("TestBank1", balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5"));
虽然我可以创建另一个银行:
Bank bank2 = new Bank("TestBank2", balance -> balance.compareTo(new BigDecimal("8000")) == -1 ? new BigDecimal("3.5") : new BigDecimal("5.3"));
现在这两个库都是在内存中创建的,只要应用程序是 运行 就可以完美运行。但是当我需要长期使用它时,我不能直接将 Funtion
许多人建议使用存储过程,如果对银行 table 中的每个银行只有 1 个类似 balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5")
的逻辑,那将会奏效,但此信息每次都会更改。这意味着如果我的银行中有 50 个条目 table 我需要为我的银行中的每个条目创建 50 个具有 50 种逻辑的不同存储过程 table,以便随着余额的变化不断更新 APY 字段.我认为可能有更好的方法不?
Advice/opinion
数据库用于永久存储数字和文本。
应用程序是用来计算和决定事情的。
也就是说,您描述的复杂业务逻辑可能用Java(或其他应用程序语言)编码更合适。同时,利率的断点等应该存储在数据库中。
换句话说:“业务逻辑”属于应用程序,而不属于数据库。 (当然,它们之间有一条灰线。例如,SQL 非常擅长汇总 table 中的所有数据;因此,我会在 SQL 中这样做,不是 Java.)
十进制
银行有挑剔的规则,这些规则可能与 DECIMAL
或 DOUBLE
提供的规则不同——在 Java 或 MySQL 或_任何其他计算机语言中。注意可能需要的规则。特别是,DECIMAL(10, 4)
在您的应用程序中不太可能足够。另一方面,如果呈现给客户的是 apy DECIMAL(4, 2)
可能就足够了。但是,再次提醒,生成该数字时要注意舍入规则。您可能被迫手动输入该号码。
注意 DECIMAL
和 DOUBLE
之间舍入特性的差异。
回答
如果您选择在数据库中实现算法,请参阅 CREATE STORED PROCEDURE
和 CREATE FUNCTION
。
存储过程可以用来封装一组SQL语句。它可以接收和发送字符串和数字,但不能接收和发送数组。它能够读取 tables(因此是某种“数组”。)
可以在表达式出现的任何地方调用函数。它可以接收一些 numbers/strings 并产生一个数字或字符串。
数组
我不清楚需要什么,但我设想 table 几行或几十行,每一行都说“对于高达 $xxx 的值,使用 y.yy%利率"。
存储过程有“游标”和“循环”,但它们很笨拙;应用程序语言可能有更好的代码。
我认为你被误解了。您可能不是在问如何将逻辑移动到数据库中(存储过程就是答案),而是在问如何将您在代码中实现的逻辑存储到数据库中,以便稍后恢复该状态。所以这是我回答的前提。
你的这些计算相关的设计不是很好。您绝对应该考虑将这些 APY 计算方法表达为 类 实现某种 IAPYCalculationMethod
接口,并使用一种方法来实际计算它。匿名 lambda 就像你拥有的那样不适合这个目的。
那么假设您确实有一个带有 CalculateAPY
方法的 IAPYCalculatioMethod
接口,然后是一个如下所示的 BalanceBasedCalculationMethod
:
// code is not complete, just to give you an idea
class BalanceBasedCalculationMethod implements IAPYCalculationMethod {
public BalanceBasedCalculationMethod(BigDecimal balanceThreshold, BigDecimal whenLower, BigDecimal whenGreater) { ... }
public BigDecimal CalculateAPY(Bank bank) {
if (bank.getBalance() < this.balanceThreshold)
return this.whenLower;
else
return this.whenGreater;
}
}
然后当您创建一个新银行时:
bank1 = new Bank("TestBank1", new BalanceBasedCalculationMethod(10000, 4, 5));
bank2 = new Bank("TestBank2", new BalanceBasedCalculationMethod(8000, 3.5, 5.3));
这已经好多了。这还允许您以某种方式序列化所有这些。你可以有一个 table 包含所有计算方法,银行和所用方法之间的关系,以及一个 JSON 参数(因为不同的方法可能有不同的参数;你可能还想将它们存储在一个单独的 table 而不是 JSON).
CREATE TABLE IF NOT EXISTS apy_calculation_method
(
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY
name VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS bank_calculation_method
(
id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
bank_id SMALLINT UNSIGNED NOT NULL REFERENCES reserves(id),
method_id SMALLINT UNSIGNED NOT NULL REFERENCES apy_calculation_method(id),
arguments JSON
);
为了表示我们基于余额的方法和使用它的银行,我们将有:
INSERT INTO apy_calculation_method (name) VALUES ('Balance Based');
那么每个银行的方法:
INSERT INTO bank_calculation_method (bank_id, method_id, arguments)
VALUES (1, 1, '{"balance": 10000, "whenLower": 4, "whenGreater": 5}')
, (2, 1, '{"balance": 8000, "whenLower": 3.5, "whenGreater": 5.3}');
还有银行:
INSERT INTO banks (id, bank_method_id, accrued_interest)
VALUES (1, 1, 0) -- first bank using the first method (balance based with 10000 balance)
, (2, 2, 0) -- second bank using the second method (balance based with 8000 balance)
此方法可以更严格或更严格(规范化/非规范化),例如,您可以将计算方法作为 JSON 存储在银行 table 中(具有 {"method": "balance", "balance": 10000, .... }
结构排序)。您的计算方法工厂的设计以及它如何 serialized/deserializes 数据 to/from 数据库取决于它,但我相信您可以弄清楚。
这一切为您提供的是将计算方法序列化到数据库中的选项,而不是某种无法以任何方式引用的随机 lambda 函数。因此,无需将实际逻辑写入数据库,只需编写“使用的逻辑类型”和参数即可。
作为一个额外的好处,这创建了一个 testable 设计,您可以为 BalanceBasedCalculationMethod 编写一个测试以确保它实际执行它应该执行的操作。您无法自动测试这些 lambda 表达式,因为无法判断它们可以做什么或不能做什么,对吗?