2015-12-02 5 views
2

Запросы базы данных, как правило, такие простые, но иногда такие сложные. (мозговой тренер)Получение Недоступных дат для аренды продукта с запасами

У меня есть продукты, акции и rentStockOrders. Эти продукты можно взять напрокат в течение нескольких дней. У запасов также есть дата, когда они доступны. Если новый продукт (запас) может быть арендован, зависит от уже арендованных запасов этого продукта.

  • Товар не может быть арендован перед его доступной датой.
  • A rentStockOrder (между заказом и запасами) содержит заказы, таким образом rentStartDate и rentEndDate.
  • Продукт можно взять напрокат за несколько дней, если дата начала не указана. Продукт выбирается, и после этого выбирается дата выбора времени, чтобы выбрать день начала аренды.
  • Прилагается общая минимальная и максимальная дата (примерно на год вперед).

Идея заключается в том, что пользователь не выбрал дату начала еще до того, как пользователь может делать, что я хочу, чтобы отключить некоторые даты в DateTimePicker, которые не могут быть использованы в качестве даты начала, поскольку нет запасов доступный для периода аренды продукта.

Чтобы выразить это в контексте: выбран один продукт, пользователю предоставляется возможность указать длину в днях, которые он хочет взять в аренду для этого продукта (1 неделя, 2 недели или 3 недели). Когда пользователь выбрал это, они должны выбрать дату начала. Вместо того, чтобы каждый раз показывать ошибку, что эта дата недоступна, я скорее отключу даты начала до начала.

Поскольку чаще всего продукт можно арендовать, а затем нет, я думаю, что лучше отправить список отсутствующих выбранных дат на мой сайт, а не весь список доступных дат. Таким образом, дни, которые недоступны, не могут быть нажаты на дату выбора времени.

Большинство примеров, которые я нашел до сих пор, включают входной параметр для даты начала и окончания, которого у меня нет, все, что у меня есть в течение нескольких дней, когда продукт хочет арендовать и сколько акций уже сдано в аренду на определенное время кадры.

EDIT:

В соответствии с просьбой, тестовые данные и таблицы:

Акции

+---------+-----------+-------------------+ 
| stockId | productId | availableFromDate | 
+---------+-----------+-------------------+ 
|  1 |   1 | 1-01-2016   | 
|  2 |   1 | 1-01-2016   | 
+---------+-----------+-------------------+ 

RentStockOrders

+------------------+---------+----------------+----------------+ 
| rentStockOrderId | stockId | beginRentDate | endRentDate | 
+------------------+---------+----------------+----------------+ 
|    1 |  1 | 15-1-2016  | 14-2-2016  | 
|    2 |  2 | 30-1-2016  | 20-2-2016  | 
|    3 |  2 | 26-2-2016  | 7-3-2016  | 
|    4 |  1 | 29-2-2016  | 14-3-2016  | 
+------------------+---------+----------------+----------------+ 

На основании этих записей я хочу сгенерировать список недоступен. Для упрощения я выделил несколько столбцов

Ввод - это день и продукт. Так что, если я был бы вход для дней: 14 и ProductID: 1 я бы некоторые из нижеследующих ожидаемых результатов:

  • 25-01-2016 (stockId 1 уже забронированы, и акции 2 получает желтую карточку в ближайшее время, 14 дней не представляется возможным.
  • 30-01-2016 (как предупреждение)
  • 13-02-2016 (шток 1 еще не вернулся)
  • 17-02-2016 (приклад 2 уже забронированы , запас 1 будет арендован за 13 дней, этого недостаточно для 14).
  • .. и намного больше, где обе акции уже сданы в аренду.

Чего я бы не ожидал, например, 15-02-2016, потому что запас 1 будет доступен в течение следующих 14 дней.

Если это сложно, возможно, получение доступных дат проще, и я переключу это на код. В этом примере из базы данных будет меньше данных, но на самом деле существует около 250 единиц одного продукта, поэтому, возможно, лучше получить недоступные даты.

Я пробовал this answer, чтобы получить доступные даты, без успеха, без ошибок, просто не возвращает данных.

declare @startDate datetime, @endDate datetime, @days int 
select @startDate = '2016/01/01', @endDate='2016/03/31', @days=2 

select stockId, min(endRentDate) 
from 
    (
    select stockId ,endRentDate, 
      (select top 1 endRentDate 
      from RentStockOrders sInner 
      where sInner.endRentDate > sOuter.beginRentDate 
        and sInner.stockId = sOuter.stockId 
        and sInner.endRentDate between @startDate and @endDate 
      order by sInner.endRentDate) as nextAvailableDate 
    from RentStockOrders sOuter 
    where sOuter.beginRentDate between @startDate and @endDate 
    ) sub 
group by stockId, nextAvailableDate 
having dateDiff(d, min(endRentDate), isNull(nextAvailableDate,dateAdd(d,1,@endDate))) >= @days 
+5

[Вредные привычки пинать: использование старого стиля JOIN и] (http://sqlblog.com/blogs/aaron_bertrand/archive/2009/10/08/bad-habits-to-kick-using-old -style-joins.aspx) - стиль старого стиля * разделенный запятыми список таблиц * был заменен на * правильный * ANSI 'JOIN' синтаксис в ANSI - ** 92 ** SQL Standard (** более 20 лет ** назад), и его использование обескуражено –

+0

Хорошо, спасибо за это, всегда было интересно, почему .. Получил решение для этого вопроса? :) – CularBytes

+1

Справа возникает связанный с этим вопрос: http://stackoverflow.com/questions/15796846/fnd-consecutive-dates-withing-a-defined-span-where-a-trainer-is-available-to- sch? rq = 1 – Turophile

ответ

1

Этот вопрос довольно сложно, если вы хотите просто использовать SQL, если не невозможно. Я сделал довольно оптимизированное/эффективное решение с использованием Oracle PL/SQL и SQL. Вы можете легко перевести его в TSQL. Функция возвращает набор/набор дат. Я также сделал еще одну версию, которая возвращает одну большую строку с разделительными значениями времени, такими как «01-01-2016 02-01-2016 03-01-2016» и т. Д. Вы также можете сделать версию, которая возвращает все отдельные даты как даты периода, например, «01-01-2016/10-01-2016 15-01-2016/25-01-2016», который вы можете легко отправить и проанализировать в своей заявке.

set serveroutput on; 

drop table Product cascade constraints; 
drop table Stocks cascade constraints; 
drop table RentStockOrders cascade constraints; 

create table Product (
    productId  number primary key, 
    description varchar2(255) 
); 

create table Stocks (
    stockId   number primary key, 
    productId   number references Product(productId), 
    availableFromDate date 
); 

create table RentStockOrders (
    rentStockOrderId number primary key, 
    stockId   number references Stocks(stockId), 
    beginRentDate  date, 
    endRentDate  date 
); 

insert into Product values (1,'product 1'); 
insert into Product values (2,'product 2'); 

insert into Stocks values (1,1,to_date('01-01-2016','dd-mm-yyyy')); 
insert into Stocks values (2,1,to_date('01-01-2016','dd-mm-yyyy')); 
insert into Stocks values (3,2,to_date('01-01-2016','dd-mm-yyyy')); 
insert into Stocks values (4,2,to_date('01-01-2016','dd-mm-yyyy')); 

insert into RentStockOrders values (1,1,to_date('15-01-2016','dd-mm-yyyy'),to_date('14-02-2016','dd-mm-yyyy')); 
insert into RentStockOrders values (2,2,to_date('30-01-2016','dd-mm-yyyy'),to_date('20-02-2016','dd-mm-yyyy')); 
insert into RentStockOrders values (3,2,to_date('26-02-2016','dd-mm-yyyy'),to_date('07-03-2016','dd-mm-yyyy')); 
insert into RentStockOrders values (4,1,to_date('29-02-2016','dd-mm-yyyy'),to_date('14-03-2016','dd-mm-yyyy')); 

--insert into RentStockOrders values (5,3,to_date('15-01-2016','dd-mm-yyyy'),to_date('14-02-2016','dd-mm-yyyy')); 
insert into RentStockOrders values (6,4,to_date('20-01-2016','dd-mm-yyyy'),to_date('25-01-2016','dd-mm-yyyy')); 
--insert into RentStockOrders values (7,4,to_date('01-01-2016','dd-mm-yyyy'),to_date('01-04-2016','dd-mm-yyyy')); 
insert into RentStockOrders values (8,3,to_date('17-01-2016','dd-mm-yyyy'),to_date('25-01-2016','dd-mm-yyyy')); 


--stocks with productId X which are rented for coming year from date Y with rentPeriode Z 
select * 
from RentStockOrders rso, Stocks s 
where rso.stockId=s.stockId 
and s.productId=1 
and rso.beginRentDate>=to_date('01-01-2016','dd-mm-yyyy')-14 
and rso.endRentDate<=to_date('01-01-2016','dd-mm-yyyy')+365 
order by beginRentDate; 


create or replace package my_globals 
as 
    --type has to be globally declared to be used as a return type 
    type t_dates is table of date INDEX BY pls_integer; 

    cursor c_searchRentData(p_productid number, p_beginDate date, p_endDate date, p_rentPeriod pls_integer) is 
    select beginRentDate,endRentDate 
    from RentStockOrders rso, Stocks s 
    where rso.stockId=s.stockId 
    and s.productId=p_productid 
    and rso.beginRentDate>=p_beginDate-p_rentPeriod 
    and rso.endRentDate<=p_endDate 
    order by beginRentDate; 
end; 

/

--helper function tot return more future (or larger) date of two dates 
create or replace function maxDate (p_date1 date, p_date2 date) 
return date 
is 
begin 
    if p_date1>=p_date2 then 
    return p_date1; 
    else 
    return p_date2; 
    end if; 
end; 

/

create or replace function getBlockedDates (p_productid number, p_beginDate date, p_endDate date, p_rentPeriod pls_integer) 
return my_globals.t_dates 
as 
    v_dates  my_globals.t_dates; 
    v_begindate date; 
    v_enddate date; 
    i   pls_integer; 
begin 
    i:=1; --collection counts from 1 
    v_enddate:=p_beginDate-1; 
    for r_date in my_globals.c_searchRentData(p_productid, p_beginDate, p_endDate, p_rentPeriod) 
    loop 
    if (v_enddate < r_date.beginRentDate) or (v_enddate < r_date.endRentDate) 
    then 
     --if previous enddate is bigger use that one 
     v_begindate:=maxDate(r_date.beginRentDate-p_rentPeriod,v_enddate+1); --first date of blocked period 
     v_enddate:=maxDate(r_date.endRentDate,v_enddate+1); --last date of blocked period 

     for j in 1..v_enddate-v_begindate+1 loop 
     v_dates(i):=v_begindate+j-1; 
     i:=i+1; 
     end loop; 
    end if; 
    end loop; 
    return v_dates; 
end; 

/

create or replace function getBlockedDatesAsStr (p_productid number, p_beginDate date, p_endDate date, p_rentPeriod pls_integer) 
return varchar2 
as 
    v_dates  varchar2(4096) := ''; --should be sufficient for one year of blocked dates 
    v_begindate date; 
    v_enddate date; 
    i   pls_integer; 
begin 
    i:=1; --collection counts from 1 
    v_enddate:=p_beginDate-1; 
    for r_date in my_globals.c_searchRentData(p_productid, p_beginDate, p_endDate, p_rentPeriod) 
    loop 
    if (v_enddate < r_date.beginRentDate) or (v_enddate < r_date.endRentDate) 
    then 
     --if previous enddate is bigger use that one 
     v_begindate:=maxDate(r_date.beginRentDate-p_rentPeriod,v_enddate+1); --first date of blocked period 
     v_enddate:=maxDate(r_date.endRentDate,v_enddate+1); --last date of blocked period 

     for j in 1..v_enddate-v_begindate+1 loop 
     v_dates:=v_dates||' '||to_char(v_begindate+j-1,'dd-mm-yyyy'); 
     i:=i+1; 
     end loop; 
    end if; 
    end loop; 
    return ltrim(v_dates); 
end; 

/

create or replace function FindAndSplit(haystack in out varchar2, needle in varchar2) 
    return varchar2 
is 
    s2 varchar2(1000); 
    idx pls_integer; 
begin 
    --dbms_output.put_line('in:'||haystack); 
    idx:=instr(haystack,needle); 
    if (idx=0) then 
    --return full haystack when needle not found 
    s2:=haystack; 
    --remaining haystack is empty 
    haystack:=''; 
    return s2; 
    end if; 
    --find string left at idx 
    s2:=substr(haystack,1,idx-1); 
    --dbms_output.put_line('out:'||s2); 
    --remaining haystack is string right at idx 
    haystack:=substr(haystack,idx+1,length(haystack)-idx); 
    --dbms_output.put_line('return:'||haystack); 
    return s2; 
end; 

/

--testcases 
declare 
v_dates my_globals.t_dates; 
i   pls_integer; 
begin 
    --store the result of stored function in local collection 
    v_dates:=getBlockedDates(1, to_date('01-01-2016','dd-mm-yyyy'), to_date('01-01-2016','dd-mm-yyyy')+365, 1); 
    --iterate through collection 
    FOR i IN 1 .. v_dates.count LOOP 
     dbms_output.put_line('Blocked date: '||v_dates(i)); 
    end loop; 

    dbms_output.put_line(''); 

    --store the result of stored function in local collection 
    v_dates:=getBlockedDates(2, to_date('01-01-2016','dd-mm-yyyy'), to_date('01-01-2016','dd-mm-yyyy')+365, 1); 
    --iterate through collection 
    FOR i IN 1 .. v_dates.count LOOP 
     dbms_output.put_line('Blocked date: '||v_dates(i)); 
    end loop; 
end; 

/

declare 
v_dates varchar2(4096); 
v_date  varchar2(10); 
i   pls_integer; 
begin 
    --store the result of stored function in local string 
    v_dates:=getBlockedDatesAsStr(1, to_date('01-01-2016','dd-mm-yyyy'), to_date('01-01-2016','dd-mm-yyyy')+365, 1); 
    dbms_output.put_line(v_dates); 
    --iterate through string 
    loop 
     v_date:=FindAndSplit(v_dates,' '); 
     dbms_output.put_line('Blocked date: '||v_date); 
     exit when v_dates is null; 
    end loop; 

    dbms_output.put_line(''); 

    --store the result of stored function in local string 
    v_dates:=getBlockedDatesAsStr(2, to_date('01-01-2016','dd-mm-yyyy'), to_date('01-01-2016','dd-mm-yyyy')+365, 1); 
    --iterate through string 
    loop 
     v_date:=FindAndSplit(v_dates,' '); 
     dbms_output.put_line('Blocked date: '||v_date); 
     exit when v_dates is null; 
    end loop; 
end; 
+0

Просто чтобы вы знали, что это возможно. Этот вопрос был также размещен на http://dba.stackexchange.com/ Я написал [ответ там] (http://dba.stackexchange.com/a/122869/57105). Он использует таблицу «Календарь». –

+0

Хорошее решение, хотя мне нравится мое решение лучше, потому что оно быстрее, проще и компактнее, нет необходимости в дополнительной таблице, проще понять и изменить/повторить. Вам действительно нужна только хранимая функция getBlockedDates и 1 запрос, остальные - функции create/insert/helper/testcases. Но, возможно, я предвзятый :) – Jan

+0

Обычно явные курсоры и пользовательские функции существенно медленнее, чем SQL на основе набора (в SQL Server, не знаю об Oracle), но вы должны проверять и сравнивать с вашими реальными данными и оборудованием , –