我正在构建一个自定义事件系统,如果你有一个重复的事件,看起来像这样:
事件A从2011年3月3日开始每4天重复一次
or
赛事B从2011年3月1日开始,每两周在周二举行一次
我如何将其存储在数据库中,使其易于查找。如果有大量的事件,我不希望出现性能问题,而且在呈现日历时,我必须遍历每一个事件。
我正在构建一个自定义事件系统,如果你有一个重复的事件,看起来像这样:
事件A从2011年3月3日开始每4天重复一次
or
赛事B从2011年3月1日开始,每两周在周二举行一次
我如何将其存储在数据库中,使其易于查找。如果有大量的事件,我不希望出现性能问题,而且在呈现日历时,我必须遍历每一个事件。
当前回答
虽然建议的解决方案是可行的,但我试图用Full Calendar实现,它需要为每个视图调用超过90个数据库(因为它加载当前、以前和下个月),这让我不太兴奋。
我找到了一个递归库https://github.com/tplaner/When,您只需将规则存储在数据库中,并使用一个查询来提取所有相关规则。
希望这能帮助到其他人,因为我花了这么多小时试图找到一个好的解决方案。
编辑:这个库是PHP的
其他回答
你举的两个例子很简单;它们可以表示为一个简单的间隔(第一个是4天,第二个是14天)。如何建模完全取决于递归的复杂度。如果上面的内容真的很简单,那么就存储一个开始日期和重复间隔的天数。
然而,如果你需要支持一些事情,比如
事件A从2011年3月3日开始,每月3日重复发生
Or
事件A从2011年3月11日开始在每月的第二个星期五发生
这是一个更复杂的模式。
存储“简单”重复模式
对于我的基于PHP/MySQL的日历,我希望尽可能有效地存储重复/重复的事件信息。我不希望有大量的行,我希望能够轻松地查找将在特定日期发生的所有事件。
下面的方法可以很好地存储定期发生的重复信息,比如每天、每n天、每周、每个月、每年等等。这也包括每周二和周四类型模式,因为它们分别存储为从周二开始的每周和从周四开始的每周。
假设我有两个表,其中一个是这样命名的events:
ID NAME
1 Sample Event
2 Another Event
和一个叫events_meta的表,像这样:
ID event_id meta_key meta_value
1 1 repeat_start 1299132000
2 1 repeat_interval_1 432000
其中repeat_start是一个没有时间的日期,作为unix时间戳,repeat_interval是间隔之间以秒为单位的数量(432000是5天)。
repeat_interval_1对应ID为1的repeat_start。因此,如果我有一个事件,重复每周二和每周四,repeat_interval将是604800(7天),将有2个repeat_starts和2个repeat_interval。表格看起来是这样的:
ID event_id meta_key meta_value
1 1 repeat_start 1298959200 -- This is for the Tuesday repeat
2 1 repeat_interval_1 604800
3 1 repeat_start 1299132000 -- This is for the Thursday repeat
4 1 repeat_interval_3 604800
5 2 repeat_start 1299132000
6 2 repeat_interval_5 1 -- Using 1 as a value gives us an event that only happens once
然后,如果你有一个每天循环的日历,抓取当天的事件,查询将像这样:
SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
RIGHT JOIN `events_meta` EM2 ON EM2.`meta_key` = CONCAT( 'repeat_interval_', EM1.`id` )
WHERE EM1.meta_key = 'repeat_start'
AND (
( CASE ( 1299132000 - EM1.`meta_value` )
WHEN 0
THEN 1
ELSE ( 1299132000 - EM1.`meta_value` )
END
) / EM2.`meta_value`
) = 1
LIMIT 0 , 30
将{current_timestamp}替换为当前日期的unix时间戳(减去时间,因此小时、分钟和秒的值将被设置为0)。
希望这也能帮助到其他人!
存储“复杂”重复模式
此方法更适合存储复杂的模式,例如
事件A从2011年3月3日开始,每月3日重复发生
or
事件A从2011年3月11日开始在每月第二周的星期五重复发生
我建议将此系统与上述系统相结合,以获得最大的灵活性。这个表应该是这样的:
ID NAME
1 Sample Event
2 Another Event
和一个叫events_meta的表,像这样:
ID event_id meta_key meta_value
1 1 repeat_start 1299132000 -- March 3rd, 2011
2 1 repeat_year_1 *
3 1 repeat_month_1 *
4 1 repeat_week_im_1 2
5 1 repeat_weekday_1 6
Repeat_week_im表示当前月份的星期,可能在1到5之间。在星期几中重复_weekday, 1-7。
现在假设你正在循环使用日/周来创建日历中的月视图,你可以像这样编写一个查询:
SELECT EV . *
FROM `events` AS EV
JOIN `events_meta` EM1 ON EM1.event_id = EV.id
AND EM1.meta_key = 'repeat_start'
LEFT JOIN `events_meta` EM2 ON EM2.meta_key = CONCAT( 'repeat_year_', EM1.id )
LEFT JOIN `events_meta` EM3 ON EM3.meta_key = CONCAT( 'repeat_month_', EM1.id )
LEFT JOIN `events_meta` EM4 ON EM4.meta_key = CONCAT( 'repeat_week_im_', EM1.id )
LEFT JOIN `events_meta` EM5 ON EM5.meta_key = CONCAT( 'repeat_weekday_', EM1.id )
WHERE (
EM2.meta_value =2011
OR EM2.meta_value = '*'
)
AND (
EM3.meta_value =4
OR EM3.meta_value = '*'
)
AND (
EM4.meta_value =2
OR EM4.meta_value = '*'
)
AND (
EM5.meta_value =6
OR EM5.meta_value = '*'
)
AND EM1.meta_value >= {current_timestamp}
LIMIT 0 , 30
这种方法与上述方法相结合,可以覆盖大多数重复/重复事件模式。如果我遗漏了什么,请留下评论。
虽然建议的解决方案是可行的,但我试图用Full Calendar实现,它需要为每个视图调用超过90个数据库(因为它加载当前、以前和下个月),这让我不太兴奋。
我找到了一个递归库https://github.com/tplaner/When,您只需将规则存储在数据库中,并使用一个查询来提取所有相关规则。
希望这能帮助到其他人,因为我花了这么多小时试图找到一个好的解决方案。
编辑:这个库是PHP的
为什么不使用类似Apache cron作业的机制呢?http://en.wikipedia.org/wiki/Cron
对于日历调度,我将使用稍微不同的“位”值来适应标准的日历重复事件-而不是 [星期几(0 - 7),月(1 - 12),月(1 - 31),小时(0 - 23),分钟(0 - 59)]
——我会用 [年(每N年重复一次),月(1- 12),月中的第一天(1- 31),月中的第一周(1-5),周中的第一天(0 - 7)]
希望这能有所帮助。
虽然目前接受的答案对我来说有很大的帮助,但我想分享一些有用的修改,它们可以简化查询并提高性能。
“简单”重复事件
处理定期发生的事件,例如:
Repeat every other day
or
Repeat every week on Tuesday
你应该创建两个表,一个叫events,像这样:
ID NAME
1 Sample Event
2 Another Event
和一个叫events_meta的表,像这样:
ID event_id repeat_start repeat_interval
1 1 1369008000 604800 -- Repeats every Monday after May 20th 2013
1 1 1369008000 604800 -- Also repeats every Friday after May 20th 2013
其中repeat_start是没有时间的unix时间戳日期(1369008000对应于2013年5月20日),repeat_interval是间隔之间以秒为单位的数量(604800是7天)。
通过循环日历中的每一天,你可以使用这个简单的查询来获得重复的事件:
SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE (( 1299736800 - repeat_start) % repeat_interval = 0 )
只需为日历中的每个日期替换unix时间戳(1299736800)。
注意模数(%符号)的使用。此符号类似于常规除法,但返回“余数”而不是商,因此当当前日期是repeat_interval与repeat_start的精确倍数时,则返回0。
性能比较
这比之前建议的基于“meta_keys”的答案要快得多,如下所示:
SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
RIGHT JOIN `events_meta` EM2 ON EM2.`meta_key` = CONCAT( 'repeat_interval_', EM1.`id` )
WHERE EM1.meta_key = 'repeat_start'
AND (
( CASE ( 1299132000 - EM1.`meta_value` )
WHEN 0
THEN 1
ELSE ( 1299132000 - EM1.`meta_value` )
END
) / EM2.`meta_value`
) = 1
如果你运行EXPLAIN这个查询,你会注意到它需要使用一个连接缓冲区:
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
| 1 | SIMPLE | EM1 | ALL | NULL | NULL | NULL | NULL | 2 | Using where |
| 1 | SIMPLE | EV | eq_ref | PRIMARY | PRIMARY | 4 | bcs.EM1.event_id | 1 | |
| 1 | SIMPLE | EM2 | ALL | NULL | NULL | NULL | NULL | 2 | Using where; Using join buffer |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
上面有一个连接的解决方案不需要这样的缓冲区。
“复杂”模式
您可以添加对更复杂类型的支持,以支持这些类型的重复规则:
Event A repeats every month on the 3rd of the month starting on March 3, 2011
or
Event A repeats second Friday of the month starting on March 11, 2011
你的事件表可以看起来完全相同:
ID NAME
1 Sample Event
2 Another Event
然后添加对这些复杂规则的支持到events_meta中,如下所示:
ID event_id repeat_start repeat_interval repeat_year repeat_month repeat_day repeat_week repeat_weekday
1 1 1369008000 604800 NULL NULL NULL NULL NULL -- Repeats every Monday after May 20, 2013
1 1 1368144000 604800 NULL NULL NULL NULL NULL -- Repeats every Friday after May 10, 2013
2 2 1369008000 NULL 2013 * * 2 5 -- Repeats on Friday of the 2nd week in every month
请注意,您只需要指定一个repeat_interval或一组repeat_year、repeat_month、repeat_day、repeat_week和repeat_weekday数据。
这使得同时选择两种类型非常简单。只需要循环每天并填写正确的值(1370563200表示2013年6月7日,然后是年、月、日、周数和工作日,如下所示):
SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE (( 1370563200 - repeat_start) % repeat_interval = 0 )
OR (
(repeat_year = 2013 OR repeat_year = '*' )
AND
(repeat_month = 6 OR repeat_month = '*' )
AND
(repeat_day = 7 OR repeat_day = '*' )
AND
(repeat_week = 2 OR repeat_week = '*' )
AND
(repeat_weekday = 5 OR repeat_weekday = '*' )
AND repeat_start <= 1370563200
)
它返回所有在第二周的周五重复的事件,以及每个周五重复的事件,因此它同时返回事件ID 1和2:
ID NAME
1 Sample Event
2 Another Event
*旁注在上面的SQL我使用PHP日期的默认工作日索引,所以“5”为星期五