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.DateFromLoan.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 上做一个类似的约束,但是我会在两个地方有类似的代码,如果可能的话,我希望避免这种情况。


所以我要问的是;我应该重新考虑我的模式来完成这个,还是有可能有一些我不熟悉的限制?

为此我们需要两件事:

  1. 一种跟踪物品与贷款相关状态的方法
  2. 在一个时间点只允许一笔活跃贷款

第一项可以通过数据模型解决(见下文),但第二项要求对数据库的任何更改都必须通过存储过程进行,并且这些存储过程必须包含逻辑以保持数据库的一致性状态。否则你会手头一团糟(或者依赖触发器,这是另一个令人头疼的问题)。

我们将通过基于时间戳的项目状态跟踪项目的物理状态,如果需要,还可以通过基于未来日期的另一种机制进行预订。

此查询将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
              )

因此,您需要将所有这些整合到您的存储过程中,因此:

  1. 当一个项目被借出时,它在指定的时间段内可用
  2. 当外借日期范围发生变化时,它不会与现有项目或未来的预订冲突
  3. 进行新的保留时,它们不会与现有的程序保留冲突
  4. 状态转换有意义(不是 loaned/Returned -> 等待取件 -> 已取件 -> Returned/Lost)
  5. 您无法删除带有已提取物品或已提取物品的外借

好的,我找到了解决方案,虽然它不是最优雅或最枯燥的。

先看职业(感谢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)

能用,这是最重要的部分。我不确定性能如何,但数据完整性更重要。