【问题标题】:Database design with Loan and LoanLines使用 Loan 和 LoanLines 进行数据库设计
【发布时间】:2020-11-06 11:15:35
【问题描述】:

我已经好几天没能想出一个可行的解决方案了。

我正在开发一个系统来维护和借出这些物品。

Loan 包含IEnumerable<LoanLine>,每个都指向一个Item

到目前为止一切顺利。 当每件物品不能在同一时期借出时,棘手的部分就暴露出来了。该时期由LoanLine.PickedUp ?? Loan.DateFrom > LoanLine.Returned ?? Loan.DateTo 定义。这意味着如果LoanLine.PickedUp 为空,则应使用Loan.DateFrom 进行比较,如果LoanLine.Returned 为空,则应使用Loan.DateTo。 物品可以在外借范围之外被拾取和归还。所以可能会出现这些情况:

也应该可以“返回”,即。将LoanLine.Returned 设置为null,在这种情况下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 进行类似的限制,但我会在两个地方有类似的代码,如果可能的话,我希望避免。


所以我要问的是;我应该重新考虑我的架构来实现这一点,还是有一些我不熟悉的约束?

【问题讨论】:

    标签: sql sql-server database-design check-constraints


    【解决方案1】:

    为此,我们需要两件事:

    1. 一种跟踪贷款项目状态的方法
    2. 在一个时间点只允许一笔有效的贷款

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

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

    此查询将返回所有项目的当前状态和贷款,以及下一次预订。从中您还可以确定哪些项目已逾期。

    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. 状态转换有意义(未借出/归还 -> 等待取货 -> 取货 -> 归还/丢失)
    5. 您不能删除已取件或已取件的外借

    【讨论】:

    • 感谢您的详细解释!我也考虑过存储过程,但是如果我出于某种原因直接使用更新/插入语句进行更改,那么数据库仍然存在“不同步”的风险。我希望约束保持数据库始终处于有效状态,无论我如何尝试破坏它。
    • 我想出了一个解决方案,虽然它不是最优雅的解决方案。
    • 您可以将所有插入/更新/删除锁定到管理员之外不可用的单个角色。然后,您可以在每个存储过程中使用 EXECUTE AS 来执行所需的操作,而无需授予直接插入/更新/删除访问权限。
    【解决方案2】:

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

    首先是职业的观点(感谢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)
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-02-02
      • 1970-01-01
      • 1970-01-01
      • 2016-12-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-05
      相关资源
      最近更新 更多