如何在数据库中存储函数逻辑

Cha*_*jee 3 java mysql sql jdbc relational-database

我正在制作财务管理应用程序。我有一个数据库,其中包含用户拥有他/她的钱的所有地方,其中包括银行。该表的结构如下...

\n
CREATE TABLE IF NOT EXISTS reserves (\n                            id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,\n                            name VARCHAR(31) NOT NULL,\n                            balance DECIMAL(10, 2) NOT NULL\n                        )\nCREATE TABLE IF NOT EXISTS banks (\n                            reserve_id SMALLINT UNSIGNED UNIQUE NOT NULL,\n                            apy DECIMAL(4, 2) NOT NULL,\n                            accrued_interest DECIMAL(10, 4) NOT NULL,\n                            last_transaction DATE,\n                            FOREIGN KEY(reserve_id) REFERENCES reserves(id)\n                        )\n
Run Code Online (Sandbox Code Playgroud)\n

在这个模型中,我可以有一个固定的APY,它将在插入时设置。但在现实世界中,银行根据余额有浮动利率。银行表中每家银行的具体情况都不同。

\n

在 Java 类中,我可以通过将 APY 定义为Function<BigDecimal, Big Decimal> APY可以存储特定 APY 逻辑并用于APY.apply(balance)随时检索利率的位置来轻松捕获这一点。

\n

但我不知道如何将这个逻辑存储在 MySQL 数据库中。

\n

我知道我可以创建一个单独的表,例如bank_balance_interest,其中我可以将利率存储到特定银行的ID的最低余额,然后引用它。

\n

但就是感觉不对。一方面,这是非常麻烦和乏味的。此外,如果没有任何明确的边界来平衡兴趣,那么仍然不会有任何解决方案,而是一个连续函数。

\n

有更优雅的方法吗?

\n

这是我的一些代码:

\n
public class Reserve {\n    short id;\n    final String name;\n    BigDecimal balance;\n\n    ReservesData reservesData;\n    public Reserve(short id, String name, BigDecimal balance) {\n        this.id = id;\n        this.name = name;\n        this.balance = balance;\n\n        reservesData = ReservesData.instance;\n    }\n\n    public Reserve(String name) {\n        this((short) -1, name, new BigDecimal("0.0"));\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n\n    public short getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public BigDecimal getBalance() {\n        return balance;\n    }\n\n    public boolean transact(BigDecimal amount) {\n        if(balance.add(amount).compareTo(new BigDecimal("0.0")) < 0)\n            return false;\n        balance = balance.add(amount);\n        return true;\n    }\n\n    public boolean save() {\n        if(id == -1)\n            return (id = reservesData.addReserve(this)) != -1;\n        return reservesData.updateReserve(this);\n    }\n}\n\npublic class Bank extends Reserve{\n\n    private final Function<BigDecimal, BigDecimal> APY;\n    private BigDecimal accruedInterest;\n    private Date lastTransactionDate;\n\n    private final BanksData banksData;\n\n    public Bank(short id, String name, BigDecimal balance, Function<BigDecimal, BigDecimal> APY) {\n        super(id, name, balance);\n\n        this.APY = APY;\n        accruedInterest = new BigDecimal("0.0");\n\n        banksData = BanksData.instance;\n    }\n\n    public Bank(String name, Function<BigDecimal, BigDecimal> APY) {\n        this((short) -1, name, new BigDecimal("0.0"), APY);\n    }\n\n    @Override\n    public BigDecimal getBalance() {\n        return balance.add(accruedInterest);\n    }\n\n    public Function<BigDecimal, BigDecimal> getAPY() {\n        return APY;\n    }\n\n    public BigDecimal getAccruedInterest() {\n        return accruedInterest;\n    }\n\n    public void setAccruedInterest(BigDecimal accruedInterest) {\n        this.accruedInterest = accruedInterest;\n    }\n\npublic class ReservesDAO implements ReservesData {\n\n    public ReservesDAO() {\n        try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {\n            stmt.executeUpdate("""\n                            CREATE TABLE IF NOT EXISTS reserves (\n                                id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,\n                                name VARCHAR(31) NOT NULL,\n                                balance DECIMAL(10, 2) NOT NULL\n                            )"""\n            );\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to create reserves table on the database!");\n            sqlException.printStackTrace();\n        }\n    }\n\n    @Override\n    public short addReserve(Reserve reserve) {\n        try (\n                PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""\n                        INSERT INTO reserves (name, balance) VALUES (?, ?)""", Statement.RETURN_GENERATED_KEYS\n                )\n        ) {\n            pstmt.setString(1, reserve.getName());\n            pstmt.setBigDecimal(2, reserve.getBalance());\n\n            pstmt.executeUpdate();\n            ResultSet rs = pstmt.getGeneratedKeys();\n            if (rs.next())\n                return rs.getShort(1);\n            else\n                throw new RuntimeException("Auto-Generated ID was not returned from reserves!");\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to insert " + reserve.getName() + " info in the database!");\n            sqlException.printStackTrace();\n            return -1;\n        }\n    }\n\n    public Reserve getReserve(short id) {\n        try(\n                PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""\n                        SELECT * FROM reserves WHERE id = ?""")\n        ) {\n            pstmt.setShort(1, id);\n            ResultSet rs = pstmt.executeQuery();\n\n            if(rs.next())\n                return new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3));\n            else throw new RuntimeException("No reserve found on the database with the id " + id);\n\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to fetch reserve from the database!");\n            sqlException.printStackTrace();\n            return null;\n        }\n    }\n\n    public List<Reserve> getAllReserves() {\n        List<Reserve> reserves = new ArrayList<>();\n        try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {\n            ResultSet rs = stmt.executeQuery("SELECT * FROM reserves");\n            while(rs.next())\n                reserves.add(new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3)));\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to fetch reserves from the database!");\n            sqlException.printStackTrace();\n        }\n\n        return reserves;\n    }\n\n    @Override\n    public BigDecimal getTotalReserveBalance() {\n        try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {\n            ResultSet rs = stmt.executeQuery("""\n                    SELECT SUM(balance) FROM reserves""");\n            if(rs.next())\n                return rs.getBigDecimal(1);\n            return new BigDecimal("0.0");\n        } catch (SQLException sqlException) {\n            System.out.println("Could not get total reserve balance from database!");\n            sqlException.printStackTrace();\n            return null;\n        }\n    }\n\n    @Override\n    public List<Reserve> getAllWallets() {\n        List<Reserve> reserves = new ArrayList<>();\n        try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {\n            ResultSet rs = stmt.executeQuery("""\n                    SELECT reserves.* FROM reserves\n                    LEFT JOIN banks ON reserves.id = banks.id\n                    WHERE banks.id IS NULL\n                    """);\n            while(rs.next())\n                reserves.add(new Reserve(rs.getShort(1), rs.getString(2), rs.getBigDecimal(3)));\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to fetch reserves from the database!");\n            sqlException.printStackTrace();\n        }\n\n        return reserves;\n    }\n\n    @Override\n    public BigDecimal getTotalWalletBalance() {\n        try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {\n            ResultSet rs = stmt.executeQuery("""\n                    SELECT SUM(balance) FROM reserves\n                    LEFT JOIN banks ON reserves.id = banks.id\n                    WHERE banks.id IS NULL\n                    """);\n            if(rs.next())\n                return rs.getBigDecimal(1) == null ? new BigDecimal("0.0") : rs.getBigDecimal(1);\n            return new BigDecimal("0.0");\n        } catch (SQLException sqlException) {\n            System.out.println("Could not get total wallet balance from database!");\n            sqlException.printStackTrace();\n            return null;\n        }\n    }\n\n    @Override\n    public boolean updateReserve(Reserve reserve) {\n        try(PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""\n                UPDATE reserves SET name = ?, balance = ? WHERE id = ?""")\n        ) {\n            pstmt.setString(1, reserve.getName());\n            pstmt.setBigDecimal(2, reserve.getBalance());\n            pstmt.setShort(3, reserve.getId());\n            pstmt.executeUpdate();\n            return true;\n        } catch(SQLException sqlException) {\n            System.out.println("Failed to update reserves with new data!");\n            sqlException.printStackTrace();\n            return false;\n        }\n    }\n}\n\npublic class BanksDAO extends ReservesDAO implements BanksData {\n    public BanksDAO() {\n        try(\n            Statement stmt = MyConnection.getMySQLconnection().createStatement()\n        ) {\n            stmt.executeUpdate("""\n                            CREATE TABLE IF NOT EXISTS banks (\n                                id SMALLINT UNSIGNED UNIQUE NOT NULL,\n                                apy DECIMAL(4, 2) NOT NULL, // I have no way to store a logic here, so currently it only stores fixed value.\n                                accrued_interest DECIMAL(10, 4) NOT NULL,\n                                last_transaction_date DATE,\n                                FOREIGN KEY(id) REFERENCES reserves(id)\n                            )"""\n            );\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to create banks table on the database!");\n            sqlException.printStackTrace();\n        }\n    }\n\n    @Override\n    public short addBank(Bank bank) {\n        try (\n                PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""\n                        INSERT INTO banks(id, apy, accrued_interest, last_transaction_date) VALUES (?, ?, ?, ?)"""\n                )\n        ) {\n            short id = addReserve(bank);\n            pstmt.setShort(1, id);\n            pstmt.setBigDecimal(2, bank.getAPY());\n            pstmt.setBigDecimal(3, bank.getAccruedInterest());\n            pstmt.setDate(4, bank.getLastTransactionDate());\n\n            pstmt.executeUpdate();\n            return id;\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to insert " + bank.getName() + " info in the database!");\n            sqlException.printStackTrace();\n            return -1;\n        }\n    }\n\n    @Override\n    public Bank getBank(short reserve_id) {\n        try(\n            PreparedStatement pstmt = MyConnection.getMySQLconnection().prepareStatement("""\n                        SELECT * FROM reserves NATURAL JOIN banks WHERE id = ?""")\n        ) {\n            pstmt.setShort(1, reserve_id);\n            ResultSet rs = pstmt.executeQuery();\n            if(!rs.next())\n                return null;\n            Bank requestedBank = new Bank(rs.getShort(1), rs.getString(2),\n                    rs.getBigDecimal(3), rs.getBigDecimal(4));\n            requestedBank.setAccruedInterest(rs.getBigDecimal(5));\n            requestedBank.setLastTransactionDate(rs.getDate(6));\n            return requestedBank;\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to fetch bank data from the database!");\n            sqlException.printStackTrace();\n            return null;\n        }\n    }\n\n    @Override\n    public List<Bank> getAllBanks() {\n        List<Bank> allBanks = new ArrayList<>();\n        try(\n            Statement stmt = MyConnection.getMySQLconnection().createStatement()\n        ) {\n            ResultSet rs = stmt.executeQuery("SELECT * FROM reserves NATURAL JOIN banks");\n            while(rs.next()) {\n                Bank bank = new Bank(rs.getShort(1), rs.getString(2),\n                        rs.getBigDecimal(3), rs.getBigDecimal(4));\n                bank.setAccruedInterest(rs.getBigDecimal(5));\n                bank.setLastTransactionDate(rs.getDate(6));\n                allBanks.add(bank);\n            }\n\n            return allBanks;\n\n        } catch (SQLException sqlException) {\n            System.out.println("Failed to fetch bank data from the database!");\n            sqlException.printStackTrace();\n            return null;\n        }\n    }\n\n    @Override\n    public BigDecimal getTotalBankBalance() {\n        try(Statement stmt = MyConnection.getMySQLconnection().createStatement()) {\n            ResultSet rs = stmt.executeQuery("""\n                    SELECT SUM(balance) FROM reserves NATURAL JOIN banks""");\n            if(rs.next())\n                return rs.getBigDecimal(1) == null ? new BigDecimal("0.0") : rs.getBigDecimal(1);\n            return new BigDecimal("0.0");\n        } catch (SQLException sqlException) {\n            System.out.println("Could not get total bank balance from database!");\n            sqlException.printStackTrace();\n            return null;\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

现在我可以将银行初始化为:

\n
Bank bank1 = new Bank("TestBank1", balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5"));\n
Run Code Online (Sandbox Code Playgroud)\n

虽然我可以创建另一家银行:

\n
Bank bank2 = new Bank("TestBank2", balance -> balance.compareTo(new BigDecimal("8000")) == -1 ? new BigDecimal("3.5") : new BigDecimal("5.3"));\n
Run Code Online (Sandbox Code Playgroud)\n

现在,这两个存储体都是在内存中创建的,只要应用程序正在运行,它们就可以完美工作。但是,当我需要长期使用它时,我无法直接将 Function<BigDecimal, BigDecimal> 类型的变量存储到 MySQL 数据库中。

\n

许多人建议存储过程,如果它只有一个逻辑,就像balance -> balance.compareTo(new BigDecimal("10000")) == -1 ? new BigDecimal("4") : new BigDecimal("5")银行表中的每个银行一样,那么这会起作用,但这些信息每次都会改变。

\n

这意味着,如果我的银行表中有 50 个条目,我需要为银行表中的每个条目创建 50 个不同的存储过程,其中包含 50 种逻辑,以便随着余额变化不断更新 APY 字段。可能有更好的方法吗?

\n

Mar*_*inJ 5

我认为你被误解了。您可能不会问如何将逻辑移动到数据库中(存储过程就是答案),而是如何将代码中实现的逻辑存储到数据库中,以便稍后可以恢复该状态。所以这就是我回答的前提。

你与这些计算相关的设计并不是很好。您绝对应该考虑将这些 APY 计算方法表达为实现某种IAPYCalculationMethod接口的类,并提供实际计算它的方法。像您所拥有的那样的匿名 lambda 不适合此目的。

假设您确实有一个IAPYCalculatioMethod带有方法的接口CalculateAPY,然后BalanceBasedCalculationMethod看起来像这样:

// The code is not complete. It is 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

然后当您创建新银行时:

bank1 = new Bank("TestBank1", new BalanceBasedCalculationMethod(10000, 4, 5));
bank2 = new Bank("TestBank2", new BalanceBasedCalculationMethod(8000, 3.5, 5.3));
Run Code Online (Sandbox Code Playgroud)

这已经好多了。这也允许您以某种方式序列化所有这些。您可以拥有一个表,其中包含所有计算方法、银行和所使用的方法之间的关系,以及参数的 JSON(因为不同的方法可能有不同的参数;您可能还希望将它们存储在单独的表中,而不是 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
);
Run Code Online (Sandbox Code Playgroud)

为了表示我们基于余额的方法和使用它的银行,我们有:

INSERT INTO apy_calculation_method (name) VALUES ('Balance Based');
Run Code Online (Sandbox Code Playgroud)

然后是各个银行的方法:

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}');
Run Code Online (Sandbox Code Playgroud)

还有银行:

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)
Run Code Online (Sandbox Code Playgroud)

此方法可以更严格或更严格(规范化/非规范化),例如,您可以将计算方法以 JSON 形式存储在银行表中(具有{"method": "balance", "balance": 10000, .... }排序结构)。您的计算方法工厂的设计以及它如何将数据序列化到数据库或从数据库中反序列化数据取决于它,但我相信您可以弄清楚。

这一切为您提供了将计算方法序列化到数据库中的选项,而不是某种无法以任何方式引用的随机 lambda 函数。因此,您不必将实际逻辑写入数据库,而只需编写“使用的逻辑类型”和参数。

作为一个额外的好处,这创建了一个可测试的设计,您可以为 BalanceBasedCalculationMethod 编写一个测试,以确保它确实完成了它应该做的事情。您无法自动测试这些 lambda,因为不知道它们可能会做什么或不会做什么,对吗?