Календарь Повторяющихся / Повторяющихся Событий-Лучший Способ Хранения
Я создаю систему пользовательских событий, и если у вас есть повторяющееся событие, которое выглядит так:
событие повторяется каждые 4 дня, начиная с 3 марта 2011
или
событие B повторяется каждые 2 недели во вторник, начиная с 1 марта 2011 года
Как я могу хранить это в базе данных таким образом, чтобы сделать его простым для поиска. Я не хочу проблем с производительностью, если есть большое количество событий, и я должен пройти через каждый из них при отображении календаря.
11 ответов:
Хранение" Простых " Повторяющихся Шаблонов
для моего календаря на основе PHP/MySQL я хотел максимально эффективно хранить информацию о повторяющихся/повторяющихся событиях. Я не хотел иметь большое количество строк, и я хотел легко искать все события, которые будут иметь место в определенную дату.
метод ниже отлично подходит для хранения повторяющейся информации, которая происходит через регулярные промежутки времени, например, каждый день, каждый n дней, каждую неделю, каждый месяц каждый год, etc etc. Это включает в себя каждый вторник и четверг тип модели, а также, потому что они хранятся отдельно, как каждую неделю, начиная со вторника и каждую неделю, начиная с четверга.
если у меня есть две таблицы, одна называется
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 идет с repeat_start идентификатора 1. Поэтому, если у меня есть событие, которое повторяется каждый вторник и каждый четверг, 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это в сочетании с вышеуказанным методом может быть объединено для покрытия большинства повторяющихся / повторяющихся шаблонов событий. Если я что-то пропустил, пожалуйста, оставьте комментарий.
в то время как в настоящее время принятый ответ был огромной помощью для меня, я хотел поделиться некоторыми полезными изменениями, которые упрощают запросы, а также повышают производительность.
" Простое " Повторение Событий
для обработки событий, которые повторяются с регулярными интервалами, например:
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 timestamp без времени (1369008000 соответствует 20 мая 2013 года), и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) для каждой даты в вашем календаре.
обратите внимание на использование знака по модулю ( % ). Этот символ похож на регулярное деление, но возвращает "остаток" вместо частного, и как таковой равен 0 всякий раз, когда текущая дата является точным кратным repeat_interval из repeat_start.
Сравнение Производительности
это значительно быстрее, чем ранее предложенный ответ на основе "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если вы запустите объяснить этот запрос, вы заметите, что он требует использования соединения буфер:
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+ | 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данные.это делает выбор обоих типов одновременно очень простой. Просто пройдите через каждый день и заполните правильные значения, (1370563200 для 7 июня 2013 года, а затем год, месяц, день, номер недели и день недели следующим образом):
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 )это возвращает все события, которые повторяются в пятницу 2-й недели,также как любые события, которые повторяются каждую пятницу, поэтому он возвращает идентификатор события 1 и 2:
ID NAME 1 Sample Event 2 Another Event*Sidenote в приведенном выше SQL я использовал PHP дата индексы дней недели по умолчанию, поэтому " 5 " для пятницы
надеюсь, что это помогает другим так же, как оригинальный ответ помог мне!
для всех вас, кто заинтересован в этом, теперь вы можете просто скопировать и вставить, чтобы начать в течение нескольких минут. Я принял совет в комментариях, как мог. Дайте мне знать, если я что-то упускаю.
"СЛОЖНАЯ ВЕРСИЯ":
событий
+----------+----------------+ | ID | NAME | +----------+----------------+ | 1 | Sample event 1 | | 2 | Second event | | 3 | Third event | +----------+----------------+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 export (для быстрого доступа)
PHP пример кода индекс.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 пример кода подключения.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 код доступен здесь (для лучшей читаемости):
.php
и
подключиться.php
Теперь настройка этого должна занять несколько минут. Не час. :)
улучшение: заменить метку времени на дату
в качестве небольшого дополнения к принятому ответу, который был впоследствии уточнен ahoffner-можно использовать формат даты, а не метку времени. Преимущества:
- читабельный даты в базе данных
- нет проблем с годами > 2038 и меткой времени
- удаляет нужно быть осторожным с метками времени, которые основаны на сезонно скорректированных датах т. е. в Великобритании 28 июня начинается один час раньше, чем 28 декабря, поэтому получение метки времени из даты может нарушить алгоритм рекурсии.
для этого измените БД
repeat_startдля хранения в виде типа 'дата' иrepeat_intervalтеперь держите дни, а не секунды. т. е. 7 для повторения 7 дней.изменить строку sql: Где ((1370563200-repeat_start) % repeat_interval = 0)
to: Где (DATEDIFF ('2013-6-7', event_start) % repeat_interval = 0)
все остальное остаться прежним. Дурочка!
в то время как предлагаемые решения работают, я пытался реализовать с полным календарем, и для каждого представления потребуется более 90 вызовов базы данных (поскольку он загружает текущий, предыдущий и следующий месяц), что меня не слишком взволновало.
Я нашел рекурсивную библиотеку https://github.com/tplaner/When где вы просто храните правила в базе данных и один запрос, чтобы вытащить все соответствующие правила.
надеюсь, это поможет кому-то еще, так как я потратил так много часами пытаюсь найти хорошее решение.
Edit: эта библиотека предназначена для 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)]
надеюсь, что это помогает.
Я бы следовал этому руководству: https://github.com/bmoeskau/Extensible/blob/master/recurrence-overview.md
также убедитесь, что вы используете формат iCal, чтобы не изобретать велосипед и помнить Правило № 0: не храните отдельные повторяющиеся экземпляры событий в виде строк в базе данных!
звучит очень похоже на события 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:
Я разработал эзотерический язык программирования только для этого случая. Самое приятное в этом то, что он менее схематичен и не зависит от платформы. Вам просто нужно написать программу селектора для вашего расписания, синтаксис которой ограничен набором правил, описанных здесь -
https://github.com/tusharmath/sheql/wiki/Rules
правила расширяемы, и вы можете добавить любую настройку на основе логики повторения, которую вы хотите проанализировать, не беспокоясь о миграции схемы и т. д.
Это совершенно другой подход и может иметь некоторые свои недостатки.
@ Rogue Coder
Это здорово!
вы можете просто использовать операцию по модулю (MOD или % в mysql), чтобы сделать ваш код простым в конце:
вместо:
AND ( ( CASE ( 1299132000 - EM1.`meta_value` ) WHEN 0 THEN 1 ELSE ( 1299132000 - EM1.`meta_value` ) END ) / EM2.`meta_value` ) = 1Do:
$current_timestamp = 1299132000 ; AND ( ('$current_timestamp' - EM1.`meta_value` ) MOD EM2.`meta_value`) = 1")чтобы сделать это дальше, можно было бы включить события, которые не повторяются навсегда.
можно добавить что-то вроде "repeat_interval_1_end" для обозначения даты последнего "repeat_interval_1". Это, однако, делает запрос больше сложно, и я не могу понять, как это сделать ...
может, кто-то может помочь!
Если, однако, вам нужно поддерживать такие вещи, как
событие a повторяется каждый месяц 3-го числа месяца начиная с 3 марта 2011 года
или
событие a повторяется во вторую пятницу месяца, начиная с 11 марта 2011 года
тогда это гораздо более сложная картина.
Comments