我正在建立一个自定义事件系统,如果您有一个重复的事件,看起来像这样:
从2011年3月3日开始,活动A每4天重复一次
要么
从2011年3月1日开始,活动B每2周重复一次
我如何以一种易于查找的方式将其存储在数据库中。如果存在大量事件,我不希望出现性能问题,并且在渲染日历时必须仔细检查每个事件。
我正在建立一个自定义事件系统,如果您有一个重复的事件,看起来像这样:
从2011年3月3日开始,活动A每4天重复一次
要么
从2011年3月1日开始,活动B每2周重复一次
我如何以一种易于查找的方式将其存储在数据库中。如果存在大量事件,我不希望出现性能问题,并且在渲染日历时必须仔细检查每个事件。
Answers:
对于基于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_intervals。该表将如下所示:
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)。
希望这也会对其他人有所帮助!
此方法更适合存储复杂的模式,例如
Event A repeats every month on the 3rd of the month starting on March 3, 2011
要么
Event A repeats Friday of the 2nd week of the month starting on March 11, 2011
我建议将其与上述系统结合使用,以获得最大的灵活性。表格如下所示:
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之间。repeat_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
可以与上述方法结合使用,以覆盖大多数重复/重复事件模式。如果我错过了任何事情,请发表评论。
AND ( ( CASE ( 1299132000 - EM1.meta_value ) WHEN 0 THEN 1 ELSE ( 1299132000 - EM1.meta_value) END ) / EM2.meta_value ) = 1
此/ EM2.meta_value
放置错误吗?
86400
一天中的几秒钟),因为它不考虑夏令时。动态地计算这些东西并存储interval = daily
and interval_count = 1
或interval = monthly
and 更合适interval_count = 1
。
尽管当前接受的答案对我有很大帮助,但我想分享一些有用的修改,以简化查询并提高性能。
处理定期发生的事件,例如:
Repeat every other day
要么
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-timestamp(1299736800)替换日历中的每个日期即可。
注意模数(%符号)的使用。该符号类似于常规除法,但返回“余数”而不是商,因此,当当前日期是repeat_start的repeat_interval的精确倍数时,该符号为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 |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
上面具有1个连接的解决方案不需要此类缓冲区。
您可以添加对更复杂类型的支持,以支持以下类型的重复规则:
Event A repeats every month on the 3rd of the month starting on March 3, 2011
要么
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
数据。
这使得同时选择两种类型非常简单。只需遍历每一天并填写正确的值即可(2013年6月7日为1370563200,然后是年,月,日,周号和工作日,如下所示):
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 Date的默认工作日索引作为旁注,因此星期五为“ 5”
希望这对其他人的帮助与原始答案对我的帮助一样大!
repeat_interval
列并在随后的列中表示(repeat_year
例如,等等)。对于第一行,可以通过在2013年5月20日之后的每个星期一重复的情况来表示repeat_weekday
和*
在其他列。
*
。因此,对于“ 3月的每个月”,您只需将其设置repeat_day
为3,将其余repeat
字段设置为*(保留为repeat_interval
null),然后将repeat_start设置为2011年3月3日的unix时间代码作为您的定位日期。
作为对后来被ahoffner完善的答案的一个小增强-可以使用日期格式而不是时间戳。优点是:
为此,请更改数据库repeat_start
以将其存储为“日期”类型,并repeat_interval
保留天数而不是秒数。即7重复7天。
更改sql行:
WHERE (( 1370563200 - repeat_start) % repeat_interval = 0 )
至:
WHERE ( DATEDIFF( '2013-6-7', repeat_start ) % repeat_interval = 0)
其他一切都保持不变。简单!
我将遵循此指南:https : //github.com/bmoeskau/Extensible/blob/master/recurrence-overview.md
还要确保使用iCal格式,以免重新发明轮子,并记住规则0: 请勿将单个重复事件实例存储为数据库中的行!
attendedEvent
带有baseInstanceId
和的实体instanceStartDate
-例如,您从中创建了重复规则日历视图并使用开始日期来指定该特定实例上的信息的基础事件,那么该实体也可以有这样的东西attendedListId
导致另一个表id
,attendedUserId
对于所有对此感兴趣的人,现在您只需几分钟即可复制并粘贴上手。我尽我所能接受了建议。让我知道我是否想念一些东西。
“复杂版本”:
大事记
+ ---------- + ---------------- + | ID | NAME | + ---------- + ---------------- + | 1 | 样本事件1 | | 2 | 第二事件 | 3 | 第三事件 + ---------- + ---------------- +
events_meta
+ ---- + ---------- + -------------- + ------------------ + ------------- + -------------- + ------------ + ------- ------ + ---------------- + | ID | event_id | repeat_start | repeat_interval | repeat_year | repeat_month | repeat_day | repeat_week | repeat_weekday | + ---- + ---------- + -------------- + ------------------ + ------------- + -------------- + ------------ + ------- ------ + ---------------- + | 1 | 1 | 2014-07-04 | 7 | NULL | NULL | NULL | NULL | NULL | | 2 | 2 | 2014-06-26 | NULL | 2014 | * | * | 2 | 5 | | 3 | 3 | 2014-07-04 | NULL | * | * | * | * | 5 | + ---- + ---------- + -------------- + ------------------ + ------------- + -------------- + ------------ + ------- ------ + ---------------- +
SQL代码:
CREATE TABLE IF NOT EXISTS `events` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`NAME` varchar(255) NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=7 ;
--
-- Dumping data for table `events`
--
INSERT INTO `events` (`ID`, `NAME`) VALUES
(1, 'Sample event'),
(2, 'Another event'),
(3, 'Third event...');
CREATE TABLE IF NOT EXISTS `events_meta` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`event_id` int(11) NOT NULL,
`repeat_start` date NOT NULL,
`repeat_interval` varchar(255) NOT NULL,
`repeat_year` varchar(255) NOT NULL,
`repeat_month` varchar(255) NOT NULL,
`repeat_day` varchar(255) NOT NULL,
`repeat_week` varchar(255) NOT NULL,
`repeat_weekday` varchar(255) NOT NULL,
PRIMARY KEY (`ID`),
UNIQUE KEY `ID` (`ID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=6 ;
--
-- Dumping data for table `events_meta`
--
INSERT INTO `events_meta` (`ID`, `event_id`, `repeat_start`, `repeat_interval`, `repeat_year`, `repeat_month`, `repeat_day`, `repeat_week`, `repeat_weekday`) VALUES
(1, 1, '2014-07-04', '7', 'NULL', 'NULL', 'NULL', 'NULL', 'NULL'),
(2, 2, '2014-06-26', 'NULL', '2014', '*', '*', '2', '5'),
(3, 3, '2014-07-04', 'NULL', '*', '*', '*', '*', '1');
也可以作为MySQL导出(易于访问)
PHP示例代码index.php:
<?php
require 'connect.php';
$now = strtotime("yesterday");
$pushToFirst = -11;
for($i = $pushToFirst; $i < $pushToFirst+30; $i++)
{
$now = strtotime("+".$i." day");
$year = date("Y", $now);
$month = date("m", $now);
$day = date("d", $now);
$nowString = $year . "-" . $month . "-" . $day;
$week = (int) ((date('d', $now) - 1) / 7) + 1;
$weekday = date("N", $now);
echo $nowString . "<br />";
echo $week . " " . $weekday . "<br />";
$sql = "SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE ( DATEDIFF( '$nowString', repeat_start ) % repeat_interval = 0 )
OR (
(repeat_year = $year OR repeat_year = '*' )
AND
(repeat_month = $month OR repeat_month = '*' )
AND
(repeat_day = $day OR repeat_day = '*' )
AND
(repeat_week = $week OR repeat_week = '*' )
AND
(repeat_weekday = $weekday OR repeat_weekday = '*' )
AND repeat_start <= DATE('$nowString')
)";
foreach ($dbConnect->query($sql) as $row) {
print $row['ID'] . "\t";
print $row['NAME'] . "<br />";
}
echo "<br /><br /><br />";
}
?>
PHP示例代码connect.php:
<?
// ----------------------------------------------------------------------------------------------------
// Connecting to database
// ----------------------------------------------------------------------------------------------------
// Database variables
$username = "";
$password = "";
$hostname = "";
$database = "";
// Try to connect to database and set charset to UTF8
try {
$dbConnect = new PDO("mysql:host=$hostname;dbname=$database;charset=utf8", $username, $password);
$dbConnect->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
echo 'ERROR: ' . $e->getMessage();
}
// ----------------------------------------------------------------------------------------------------
// / Connecting to database
// ----------------------------------------------------------------------------------------------------
?>
另外,这里还提供了php代码(以提高可读性):
index.php
和
connect.php
现在进行设置应该花费您几分钟。不是几个小时。:)
当建议的解决方案工作时,我尝试使用完整日历来实现,并且每个视图都需要进行90多次数据库调用(因为它加载了当前,上个月和下个月),对此我并不感到兴奋。
我发现了一个递归库https://github.com/tplaner/当您将规则简单地存储在数据库中以及一个查询以提取所有相关规则时。
希望这对其他人有帮助,因为我花了很多时间试图找到一个好的解决方案。
编辑:此库是为PHP
When
You必须将所有递归日期存储在数据库中,或者获取所有递归事件并在数据库中的php no中生成日期。我对吗?
When
生成所有日期-这些日期是从初始存储的日期/规则中填充的。
为什么不使用类似于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) ]
希望这可以帮助。
我为这种情况开发了一种深奥的编程语言。关于它的最好的部分是它较少架构且与平台无关。您只需要编写一个选择器程序即可,因为您的日程安排的语法受此处描述的规则集约束-
https://github.com/tusharmath/sheql/wiki/规则
这些规则是可扩展的,您可以根据要执行的重复逻辑的种类添加任何类型的自定义,而无需担心架构迁移等。
这是一种完全不同的方法,可能有其自身的一些缺点。
听起来很像存储在系统表中的MySQL事件。您可以查看结构并找出不需要的列:
EVENT_CATALOG: NULL
EVENT_SCHEMA: myschema
EVENT_NAME: e_store_ts
DEFINER: jon@ghidora
EVENT_BODY: SQL
EVENT_DEFINITION: INSERT INTO myschema.mytable VALUES (UNIX_TIMESTAMP())
EVENT_TYPE: RECURRING
EXECUTE_AT: NULL
INTERVAL_VALUE: 5
INTERVAL_FIELD: SECOND
SQL_MODE: NULL
STARTS: 0000-00-00 00:00:00
ENDS: 0000-00-00 00:00:00
STATUS: ENABLED
ON_COMPLETION: NOT PRESERVE
CREATED: 2006-02-09 22:36:06
LAST_ALTERED: 2006-02-09 22:36:06
LAST_EXECUTED: NULL
EVENT_COMMENT:
RRULE标准正是针对此要求而构建的,即保存和了解重复发生。微软和谷歌都在日历事件中使用它。请仔细阅读本文档以获取更多详细信息。 https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
@流氓编码器
这很棒!
您可以简单地使用模运算(在mysql中为MOD或%)来使代码最后变得简单:
代替:
AND (
( CASE ( 1299132000 - EM1.`meta_value` )
WHEN 0
THEN 1
ELSE ( 1299132000 - EM1.`meta_value` )
END
) / EM2.`meta_value`
) = 1
做:
$current_timestamp = 1299132000 ;
AND ( ('$current_timestamp' - EM1.`meta_value` ) MOD EM2.`meta_value`) = 1")
为了进一步做到这一点,可以包括不会永远发生的事件。
可以添加诸如“ repeat_interval_1_end”之类的来表示最后一个“ repeat_interval_1”的日期。但是,这使查询更加复杂,我无法真正弄清楚该怎么做...
也许有人可以帮忙!
您所给出的两个例子非常简单。它们可以表示为一个简单的时间间隔(第一天是四天,第二天是14天)。如何建模将完全取决于您的重复操作的复杂性。如果以上内容确实如此简单,请在重复间隔中存储开始日期和天数。
但是,如果您需要支持诸如
从2011年3月3日开始,活动A在每月的3号每月重复一次
要么
从2011年3月11日开始,活动A重复该月的第二个星期五
那是一个更复杂的模式。
1299132000
要硬编码吗?如果我需要获取给定结束日期的发生日期和用户,该怎么办?