Loan 和 LoanLines 的数据库设计
Database design with Loan and LoanLines
我好几天都没有想出可行的解决方案。
我正在开发一个系统来维护物品并借出这些物品。
Loan
包含 IEnumerable<LoanLine>
,每个指向 Item
:
到目前为止一切顺利。
当不能在同一时期内借出每件物品时,棘手的部分就会暴露出来。该时期由 LoanLine.PickedUp ?? Loan.DateFrom > LoanLine.Returned ?? Loan.DateTo
定义。这意味着如果LoanLine.PickedUp
为空,则应使用Loan.DateFrom
进行比较,如果LoanLine.Returned
为空,则应使用Loan.DateTo
。
一件物品 可以 在外借范围外取回。所以会出现这些场景:
也应该可以“返回”,即。将 LoanLine.Returned
设置为空,在这种情况下 Loan.DateTo
用于再次比较。 LoanLine.PickedUp
也是如此。
也应该可以同时更新 Loan.DateFrom
和 Loan.DateTo
,前面提到的约束仍然有效。这意味着如果对 Loan
的更新导致其中一行(其中任一 DateTime
设置为 null)重叠,则约束将抛出错误。
这是创建脚本:
create table loan
(
id int primary key identity(1, 1),
datefrom date not null,
dateto date not null,
employee_id int references employee(id) not null,
recipient_id int references employee(id) null,
note nvarchar(max) not null,
constraint c_loan_chkdates check (datefrom <= dateto)
);
create table loanlineitem
(
id int primary key identity(1, 1),
loan_id int references loan(id) on delete cascade not null,
item_id int references item(id) not null,
pickedup datetime null,
returned datetime null,
constraint uq_loanlineitem unique (loan_id, item_id),
constraint c_loanlineitem_chkdates check (returned is null or pickedup <= returned)
);
这是约束条件:
create function checkLoanLineItem(@itemId int, @loanId int, @pickedup datetime, @returned datetime)
returns bit
as
begin
declare @result bit = 0;
declare @from date = @pickedup;
declare @to date = @returned;
--If either @from or @to is null, fill the ones with null from loan-table
if (isnull(@from, @to) is null)
begin
select @from = isnull(@from, datefrom),
@to = isnull(@to, dateadd(d, 1, dateto))
from loan
where id = @loanId;
end
if not exists (select top 1 lli.id from loanlineitem lli
inner join loan l on lli.loan_id = l.id
where l.id <> @loanId
and lli.item_id = @itemId
and ((isnull(lli.pickedup, l.datefrom) >= @from and isnull(lli.pickedup, l.datefrom) < @to)
--When comparing datetime with date, the date's time is 00:00:00
--so one day is added to account for this
or (isnull(lli.returned, dateadd(d, 1, l.dateto)) >= @from and isnull(lli.returned, dateadd(d, 1, l.dateto)) < @to))
)
begin
set @result = 1;
end
return @result;
end;
go;
alter table loanlineitem
add constraint c_loanlineitem_checkoverlap check (dbo.checkLoanLineItem(item_id, loan_id, pickedup, returned) = 1)
go;
我可以在 Loan
-table 上做一个类似的约束,但是我会在两个地方有类似的代码,如果可能的话,我希望避免这种情况。
所以我要问的是;我应该重新考虑我的模式来完成这个,还是有可能有一些我不熟悉的限制?
为此我们需要两件事:
- 一种跟踪物品与贷款相关状态的方法
- 在一个时间点只允许一笔活跃贷款
第一项可以通过数据模型解决(见下文),但第二项要求对数据库的任何更改都必须通过存储过程进行,并且这些存储过程必须包含逻辑以保持数据库的一致性状态。否则你会手头一团糟(或者依赖触发器,这是另一个令人头疼的问题)。
我们将通过基于时间戳的项目状态跟踪项目的物理状态,如果需要,还可以通过基于未来日期的另一种机制进行预订。
此查询将return所有物品的当前状态和外借,以及下一次预订。由此您还可以确定哪些项目已过期。
SELECT
Item.ItemId
,ItemStatus.UpdateDtm
,ItemStatus.StatusCd
,ItemStatus.LoanNumber
,Loan.StartDt
,Loan.EndDt
,Reservation.StartDt
,Reservation.EndDt
FROM
Item Item
LEFT JOIN
LoanItemStatus ItemStatus
ON ItemStatus.ItemId = Item.ItemId
AND ItemStatus.UpdateDtm =
(
SELECT
MAX(UpdateDtm)
FROM
LoanItemStatus
WHERE
ItemId = Item.ItemId
)
LEFT JOIN
Loan Loan
ON Loan.LoanNumber = ItemStatus.LoanNumber
LEFT JOIN
ItemReservation Reservation
ON Reservation.ItemId = Item.ItemId
AND Reservation.StartDt =
(
SELECT
MIN(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt >= GetDate()
)
将此逻辑强化为一个视图可能是有意义的。
查看某个项目在给定时间范围内是否有预订:
SELECT
Item.ItemId
,CASE
WHEN COALESCE(PriorReservation.EndDt,GETDATE()) <= @ReservationStartDt AND @ReservationEndDt <= COALESCE(NextReservation.StartDt,'9999-12-31') THEN 'Y'
ELSE 'N'
END AS ReservationAvailableInd
FROM
Item Item
LEFT JOIN
ItemReservation PriorReservation
ON PriorReservation.ItemId = Item.ItemId
AND PriorReservation.StartDt =
(
SELECT
MAX(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt <= @ReservationStartDt
)
LEFT JOIN
ItemReservation NextReservation
ON NextReservation.ItemId = Item.ItemId
AND NextReservation.StartDt =
(
SELECT
MIN(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt > @ReservationStartDt
)
因此,您需要将所有这些整合到您的存储过程中,因此:
- 当一个项目被借出时,它在指定的时间段内可用
- 当外借日期范围发生变化时,它不会与现有项目或未来的预订冲突
- 进行新的保留时,它们不会与现有的
程序保留冲突
- 状态转换有意义(不是 loaned/Returned -> 等待取件 -> 已取件 -> Returned/Lost)
- 您无法删除带有已提取物品或已提取物品的外借
好的,我找到了解决方案,虽然它不是最优雅或最枯燥的。
先看职业(感谢bbaird的建议,逻辑更容易理解):
create view vw_loanlineitem_occupations
as
select lli.id, loan_id, item_id, isnull(lli.pickedup, l.datefrom) as [from], isnull(lli.returned, dateadd(d, 1, l.dateto)) as [to] from loanlineitem lli inner join loan l on lli.loan_id = l.id
然后一般检查重叠 udf:
create function udf_isOverlapping(@span1Start datetime, @span1End datetime, @span2Start datetime, @span2End datetime)
returns bit
as
begin
return iif((@span1Start <= @span2End and @span1End >= @span2Start), 1, 0);
end
然后是 udf 和贷款限制:
create function udf_isLoanValid(@loanId int, @dateFrom date, @dateTo date)
returns bit
as
begin
declare @result bit = 0;
--When type 'date' is compared to 'datetime' the time-part is 00:00:00, so add one day
set @dateTo = dateadd(d, 1, @dateTo)
if not exists (
select top 1 lli.id from loanlineitem lli
inner join loan l on lli.loan_id = l.id
--Only check items that are in this loan
where lli.item_id in (select item_id from loanlineitem where loan_id = @loanId)
--Check if this span is overlapping with other lines/loans
--When type 'date' is compared to 'datetime' the time-part is 00:00:00, so add one day
and (dbo.udf_isOverlapping(
@dateFrom,
@dateTo,
isnull(lli.pickedup, iif(l.id = @loanId, @dateFrom, l.datefrom)),
isnull(lli.returned, iif(l.id = @loanId, @dateTo, dateadd(d, 1, l.dateto)))
) = 1
)
)
begin
set @result = 1
end
return @result;
end;
go;
alter table loan
add constraint c_loan_datecheck check (dbo.udf_isLoanValid(id, dateFrom, dateTo) = 1);
以及对 loanlineitem 的单独约束,不幸的是,它重复了贷款约束中的一些代码:
create function udf_isLineValid(@itemId int, @loanId int, @pickedup datetime, @returned datetime)
returns bit
as
begin
declare @result bit = 0;
declare @from date = @pickedup;
declare @to date = @returned;
--If either @from or @to is null, fill the ones with null from loan-table
if (@from is null or @to is null)
begin
select @from = isnull(@from, datefrom),
@to = isnull(@to, dateadd(d, 1, dateto))
from loan
where id = @loanId;
end
--If no lines with overlap exists, this line is valid, so set result to 1
if not exists (
select top 1 id from vw_loanlineitem_occupations
where item_id = @itemId
and loan_id <> @loanId
and dbo.udf_isOverlapping(@from, @to, [from], [to]) = 1
)
begin
set @result = 1;
end
return @result;
end;
go;
alter table loanlineitem
add constraint c_loanlineitem_checkoverlap check (dbo.udf_isLineValid(item_id, loan_id, pickedup, returned) = 1)
能用,这是最重要的部分。我不确定性能如何,但数据完整性更重要。
我好几天都没有想出可行的解决方案。
我正在开发一个系统来维护物品并借出这些物品。
Loan
包含 IEnumerable<LoanLine>
,每个指向 Item
:
到目前为止一切顺利。
当不能在同一时期内借出每件物品时,棘手的部分就会暴露出来。该时期由 LoanLine.PickedUp ?? Loan.DateFrom > LoanLine.Returned ?? Loan.DateTo
定义。这意味着如果LoanLine.PickedUp
为空,则应使用Loan.DateFrom
进行比较,如果LoanLine.Returned
为空,则应使用Loan.DateTo
。
一件物品 可以 在外借范围外取回。所以会出现这些场景:
也应该可以“返回”,即。将 LoanLine.Returned
设置为空,在这种情况下 Loan.DateTo
用于再次比较。 LoanLine.PickedUp
也是如此。
也应该可以同时更新 Loan.DateFrom
和 Loan.DateTo
,前面提到的约束仍然有效。这意味着如果对 Loan
的更新导致其中一行(其中任一 DateTime
设置为 null)重叠,则约束将抛出错误。
这是创建脚本:
create table loan
(
id int primary key identity(1, 1),
datefrom date not null,
dateto date not null,
employee_id int references employee(id) not null,
recipient_id int references employee(id) null,
note nvarchar(max) not null,
constraint c_loan_chkdates check (datefrom <= dateto)
);
create table loanlineitem
(
id int primary key identity(1, 1),
loan_id int references loan(id) on delete cascade not null,
item_id int references item(id) not null,
pickedup datetime null,
returned datetime null,
constraint uq_loanlineitem unique (loan_id, item_id),
constraint c_loanlineitem_chkdates check (returned is null or pickedup <= returned)
);
这是约束条件:
create function checkLoanLineItem(@itemId int, @loanId int, @pickedup datetime, @returned datetime)
returns bit
as
begin
declare @result bit = 0;
declare @from date = @pickedup;
declare @to date = @returned;
--If either @from or @to is null, fill the ones with null from loan-table
if (isnull(@from, @to) is null)
begin
select @from = isnull(@from, datefrom),
@to = isnull(@to, dateadd(d, 1, dateto))
from loan
where id = @loanId;
end
if not exists (select top 1 lli.id from loanlineitem lli
inner join loan l on lli.loan_id = l.id
where l.id <> @loanId
and lli.item_id = @itemId
and ((isnull(lli.pickedup, l.datefrom) >= @from and isnull(lli.pickedup, l.datefrom) < @to)
--When comparing datetime with date, the date's time is 00:00:00
--so one day is added to account for this
or (isnull(lli.returned, dateadd(d, 1, l.dateto)) >= @from and isnull(lli.returned, dateadd(d, 1, l.dateto)) < @to))
)
begin
set @result = 1;
end
return @result;
end;
go;
alter table loanlineitem
add constraint c_loanlineitem_checkoverlap check (dbo.checkLoanLineItem(item_id, loan_id, pickedup, returned) = 1)
go;
我可以在 Loan
-table 上做一个类似的约束,但是我会在两个地方有类似的代码,如果可能的话,我希望避免这种情况。
所以我要问的是;我应该重新考虑我的模式来完成这个,还是有可能有一些我不熟悉的限制?
为此我们需要两件事:
- 一种跟踪物品与贷款相关状态的方法
- 在一个时间点只允许一笔活跃贷款
第一项可以通过数据模型解决(见下文),但第二项要求对数据库的任何更改都必须通过存储过程进行,并且这些存储过程必须包含逻辑以保持数据库的一致性状态。否则你会手头一团糟(或者依赖触发器,这是另一个令人头疼的问题)。
我们将通过基于时间戳的项目状态跟踪项目的物理状态,如果需要,还可以通过基于未来日期的另一种机制进行预订。
此查询将return所有物品的当前状态和外借,以及下一次预订。由此您还可以确定哪些项目已过期。
SELECT
Item.ItemId
,ItemStatus.UpdateDtm
,ItemStatus.StatusCd
,ItemStatus.LoanNumber
,Loan.StartDt
,Loan.EndDt
,Reservation.StartDt
,Reservation.EndDt
FROM
Item Item
LEFT JOIN
LoanItemStatus ItemStatus
ON ItemStatus.ItemId = Item.ItemId
AND ItemStatus.UpdateDtm =
(
SELECT
MAX(UpdateDtm)
FROM
LoanItemStatus
WHERE
ItemId = Item.ItemId
)
LEFT JOIN
Loan Loan
ON Loan.LoanNumber = ItemStatus.LoanNumber
LEFT JOIN
ItemReservation Reservation
ON Reservation.ItemId = Item.ItemId
AND Reservation.StartDt =
(
SELECT
MIN(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt >= GetDate()
)
将此逻辑强化为一个视图可能是有意义的。
查看某个项目在给定时间范围内是否有预订:
SELECT
Item.ItemId
,CASE
WHEN COALESCE(PriorReservation.EndDt,GETDATE()) <= @ReservationStartDt AND @ReservationEndDt <= COALESCE(NextReservation.StartDt,'9999-12-31') THEN 'Y'
ELSE 'N'
END AS ReservationAvailableInd
FROM
Item Item
LEFT JOIN
ItemReservation PriorReservation
ON PriorReservation.ItemId = Item.ItemId
AND PriorReservation.StartDt =
(
SELECT
MAX(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt <= @ReservationStartDt
)
LEFT JOIN
ItemReservation NextReservation
ON NextReservation.ItemId = Item.ItemId
AND NextReservation.StartDt =
(
SELECT
MIN(StartDt)
FROM
ItemReservation
WHERE
ItemId = Item.ItemId
AND StartDt > @ReservationStartDt
)
因此,您需要将所有这些整合到您的存储过程中,因此:
- 当一个项目被借出时,它在指定的时间段内可用
- 当外借日期范围发生变化时,它不会与现有项目或未来的预订冲突
- 进行新的保留时,它们不会与现有的
程序保留冲突 - 状态转换有意义(不是 loaned/Returned -> 等待取件 -> 已取件 -> Returned/Lost)
- 您无法删除带有已提取物品或已提取物品的外借
好的,我找到了解决方案,虽然它不是最优雅或最枯燥的。
先看职业(感谢bbaird的建议,逻辑更容易理解):
create view vw_loanlineitem_occupations
as
select lli.id, loan_id, item_id, isnull(lli.pickedup, l.datefrom) as [from], isnull(lli.returned, dateadd(d, 1, l.dateto)) as [to] from loanlineitem lli inner join loan l on lli.loan_id = l.id
然后一般检查重叠 udf:
create function udf_isOverlapping(@span1Start datetime, @span1End datetime, @span2Start datetime, @span2End datetime)
returns bit
as
begin
return iif((@span1Start <= @span2End and @span1End >= @span2Start), 1, 0);
end
然后是 udf 和贷款限制:
create function udf_isLoanValid(@loanId int, @dateFrom date, @dateTo date)
returns bit
as
begin
declare @result bit = 0;
--When type 'date' is compared to 'datetime' the time-part is 00:00:00, so add one day
set @dateTo = dateadd(d, 1, @dateTo)
if not exists (
select top 1 lli.id from loanlineitem lli
inner join loan l on lli.loan_id = l.id
--Only check items that are in this loan
where lli.item_id in (select item_id from loanlineitem where loan_id = @loanId)
--Check if this span is overlapping with other lines/loans
--When type 'date' is compared to 'datetime' the time-part is 00:00:00, so add one day
and (dbo.udf_isOverlapping(
@dateFrom,
@dateTo,
isnull(lli.pickedup, iif(l.id = @loanId, @dateFrom, l.datefrom)),
isnull(lli.returned, iif(l.id = @loanId, @dateTo, dateadd(d, 1, l.dateto)))
) = 1
)
)
begin
set @result = 1
end
return @result;
end;
go;
alter table loan
add constraint c_loan_datecheck check (dbo.udf_isLoanValid(id, dateFrom, dateTo) = 1);
以及对 loanlineitem 的单独约束,不幸的是,它重复了贷款约束中的一些代码:
create function udf_isLineValid(@itemId int, @loanId int, @pickedup datetime, @returned datetime)
returns bit
as
begin
declare @result bit = 0;
declare @from date = @pickedup;
declare @to date = @returned;
--If either @from or @to is null, fill the ones with null from loan-table
if (@from is null or @to is null)
begin
select @from = isnull(@from, datefrom),
@to = isnull(@to, dateadd(d, 1, dateto))
from loan
where id = @loanId;
end
--If no lines with overlap exists, this line is valid, so set result to 1
if not exists (
select top 1 id from vw_loanlineitem_occupations
where item_id = @itemId
and loan_id <> @loanId
and dbo.udf_isOverlapping(@from, @to, [from], [to]) = 1
)
begin
set @result = 1;
end
return @result;
end;
go;
alter table loanlineitem
add constraint c_loanlineitem_checkoverlap check (dbo.udf_isLineValid(item_id, loan_id, pickedup, returned) = 1)
能用,这是最重要的部分。我不确定性能如何,但数据完整性更重要。