我试图为用户构建简单的应用程序来执行资金操作(相互转移硬币等)。我已经决定将帐户信息与用户数据分开,所以此刻我的架构 looks like this
现在我遇到了一个问题:用户应该能够始终看到他当前的平衡。此外,他还可以查看与他相关的操作列表(收入和支出),列表中的每个项目必须包含有关用户余额状态(操作后的硬币数量)的信息。
我只想出了两个解决方案,但从我的角度来看,每个解决方案看起来都很糟糕:
重做整个'操作'包含以下字段的表:user_1, user_2,金额,类型(收入/支出),user_1_balance
关键是,例如,两个用户A和B有80个硬币 每个,并且用户A决定向用户B发送30个硬币,接下来的两个 '操作中的行'将创建表:
A B 30 expense 50
B A 30 income 110
但是这个解决方案只是复制了除了之外的所有信息 平衡信息,也与解决方案编号1具有相同的问题。
有没有更简单,更好的方式来做我想要的?我错过了什么吗?
答案 0 :(得分:1)
您在示例中描述的是交易表,它是常见的双重记账方案,对于2个实体之间的任何交易,或者帐户有借记< / strong>在一个帐户中输入,在另一个帐户中输入相应的信用,从而在交易表中生成2个条目。您肯定应该以某种方式将这两行链接在一起,或者使用相同的时间戳,或者通过事务头或物理事务的唯一内容,因此您可能有一个记录操作的Header表(可能称为操作)和Transaction表保存操作表的链接。这种结构可以轻松支持单一货币交易事件,该事件从单个账户获取资金并分配给许多账户。
理论上,您现在可以将任何给定帐户的交易表相加以查找当前余额,因此您无需将余额记录在任何位置作为特定存储字段
你很自然地找到了解决这个问题的方法,我不想在这个回答中过于技术化,因为这是一个非常广泛的主题。
理论上,如上所述,您可能不需要存储“当前”信息。平衡,但在实践中,除非你有一个好的RDBMS引擎,良好的索引和良好的必要语法命令,否则也可以使用相应的字段来保存累积的平衡值,而不是为每个查询重新计算这个值。
如果您尝试存储余额,则应确保您可以控制表的所有输入,触发器可能有用,应用程序逻辑可接受,但要小心计算字段,请确保它们已编译为它们是在写入操作上进行评估,而不是读取操作...假设读取与写入次数会更多。
更新:关于each of the items in the list must contain information about the state of users balance (how much coins) after that operation.
根据您选择的RDBMS,应该有一些标准机制来计算读取的运行总计,这样您就不需要记录它们。您必须权衡存储运行总计或计算它之间对性能,存储成本和维护的影响,以下是为SQL Server 2012(及更高版本)设计的示例,并使用窗口函数计算运行总计,保持
即使您存储了运行总计,您也可以找到窗口函数来定期回填或审核事务,以确保平衡字段正确。
-- Example Operation Header Table
DECLARE @Operation as Table
(
Id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
TxDate DateTimeOffset(7) NOT NULL DEFAULT(SysDateTimeOffset()),
[Description] char(120) NOT NULL
)
-- Example Transaction Table
DECLARE @Transaction as Table
(
Id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
OperationId bigint NULL,
TxDate DateTimeOffset(7) NOT NULL DEFAULT(SysDateTimeOffset()),
[Description] char(120) NOT NULL,
Account char(1) NOT NULL,
Amount MONEY NOT NULL
)
-- Insert Starting balances
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('A','Initial Balance',100.00);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('B','Initial Balance',100.00);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('C','Initial Balance',0.00);
-- INSERT Some Transactions
DECLARE @opId bigint;
INSERT INTO @Operation ([Description]) VALUES ('A pays B and C $25.60 for services rendered')
SELECT @opId = SCOPE_IDENTITY()
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'A','A pays B and C $25.60 for services rendered',-51.20);
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'B','A pays B and C $25.60 for services rendered',25.60);
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'C','A pays B and C $25.60 for services rendered',25.60);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('C','Buy lunch',-8.20);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('A','Buy petrol',40.00);
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('A','Sell Goods',120.00);
INSERT INTO @Operation ([Description]) VALUES ('B lends $50 to C')
SELECT @opId = SCOPE_IDENTITY()
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'B','B lends $50 to C',-50);
INSERT INTO @Transaction (OperationId, Account,[Description],Amount) VALUES (@opId,'C','B lends $50 to C',50);
-- Example of checking current balance on the spot
DECLARE @balance MONEY = (SELECT SUM(Amount) FROM @Transaction WHERE Account = 'C')
SELECT @balance as 'C Account Balance'
if(@balance > 80)
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('C','Go out for dinner',-80);
else
INSERT INTO @Transaction (Account,[Description],Amount) VALUES ('C','Get a pizza',-10);
SELECT * from @Operation
SELECT * from @Transaction
-- Current Balance of all accounts
SELECT Account, Balance = SUM(Amount)
FROM @Transaction
GROUP BY Account
-- Running Balance with Transactions
SELECT t.*, Balance = SUM(Amount) OVER(Partition By Account ORDER BY Id ROWS UNBOUNDED PRECEDING)
FROM @Transaction t
ORDER BY Id
-- Running Balance, just for Account=C
SELECT t.*, Balance = SUM(Amount) OVER(Partition By Account ORDER BY Id ROWS UNBOUNDED PRECEDING)
FROM @Transaction t
WHERE Account = 'C'
ORDER BY Id
上面的脚本可以安全运行,它使用表变量,所以不应该留下任何东西:)
最终,这会导致所有事务的以下输出(我省略了TxDate列,在此示例中日期都相同,因此没用):
+----+-------------+---------------------------------------------+---------+--------+---------+ | Id | OperationId | Description | Account | Amount | Balance | +----+-------------+---------------------------------------------+---------+--------+---------+ | 1 | NULL | Initial Balance | A | 100.00 | 100.00 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 2 | NULL | Initial Balance | B | 100.00 | 100.00 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 3 | NULL | Initial Balance | C | 0.00 | 0.00 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 4 | 1 | A pays B and C $25.60 for services rendered | A | -51.20 | 48.80 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 5 | 1 | A pays B and C $25.60 for services rendered | B | 25.60 | 125.60 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 6 | 1 | A pays B and C $25.60 for services rendered | C | 25.60 | 25.60 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 7 | NULL | Buy lunch | C | -8.20 | 17.40 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 8 | NULL | Buy petrol | A | 40.00 | 88.80 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 9 | NULL | Sell Goods | A | 120.00 | 208.80 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 10 | 2 | B lends $50 to C | B | -50.00 | 75.60 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 11 | 2 | B lends $50 to C | C | 50.00 | 67.40 | +----+-------------+---------------------------------------------+---------+--------+---------+ | 12 | NULL | Get a pizza | C | -10.00 | 57.40 | +----+-------------+---------------------------------------------+---------+--------+---------+