私はカスタムイベントシステムを構築していますが、次のような繰り返しイベントがある場合:
イベントAは、2011年3月3日から4日ごとに繰り返されます
または
イベントBは、2011年3月1日から始まる火曜日に2週間ごとに繰り返されます
検索を簡単にする方法でそれをデータベースに保存するにはどうすればよいですか。多数のイベントがある場合、パフォーマンスの問題は望ましくありません。また、カレンダーをレンダリングするときに、すべてのイベントを確認する必要があります。
PHP/MySQLベースのカレンダーでは、繰り返し/繰り返しのイベント情報を可能な限り効率的に保存したかったのです。行の数を増やしたくありませんでした。特定の日付に発生するすべてのイベントを簡単に検索したかったのです。
以下の方法は、毎日、毎週n日、毎週、毎月など、定期的に発生する繰り返し情報を保存するのに最適です。これには、毎週火曜日と木曜日のタイプパターンが含まれます。同様に、毎週火曜日から始まり、毎週木曜日から始まるように別々に保存されます。
次のようなevents
と呼ばれる2つのテーブルがあると仮定します。
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日間)になり、repeat_startsが2つとrepeat_intervalsが2つになります。テーブルは次のようになります。
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
という2つのテーブルを作成する必要があります。
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
)
これにより、2週目の金曜日に繰り返されるすべてのイベント、、および毎週金曜日に繰り返されるすべてのイベントが返されるため、イベントID 1と2:
ID NAME
1 Sample Event
2 Another Event
*上記のSQLのサイドノートでは、 PHP Date's デフォルトの曜日インデックスを使用したため、金曜日は「5」
これが元の答えが私を助けたのと同じくらい他の人を助けることを願っています!
Ahoffnerによってその後改良された、受け入れられた回答に対する小さな拡張として-タイムスタンプではなく日付形式を使用することが可能です。利点は次のとおりです。
これを行うには、DB repeat_start
を 'date'タイプとして保存するように変更し、repeat_interval
が秒ではなく日を保持するようにします。つまり、7日間の繰り返しの場合は7です。
sql行を変更します。
WHERE (( 1370563200 - repeat_start) % repeat_interval = 0 )
に:
WHERE ( DATEDIFF( '2013-6-7', repeat_start ) % repeat_interval = 0)
他のすべては同じままです。シンプル!
これに興味のあるすべての人は、コピーして貼り付けるだけで数分で開始できます。私はできる限りコメントでアドバイスを受けました。何か足りない場合は教えてください。
「複雑なバージョン」:
イベント
+ ---------- + ---------------- + | ID | NAME | + ---------- + ---------------- + | 1 |サンプルイベント1 | | 2 | 2番目のイベント| | 3 | 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 export としても利用可能(簡単にアクセスできるように)
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
この設定には数分かかります。時間ではありません。 :)
私はこのガイドに従います: https://github.com/bmoeskau/Extensible/blob/master/recurrence-overview.md
また、ホイールを再発明しないようにiCal形式を使用し、ルール#0を忘れないようにしてください:個々の繰り返しイベントインスタンスをデータベースの行として保存しないでください!
提案されたソリューションは機能しますが、フルカレンダーで実装しようとしていましたが、各ビューに90を超えるデータベース呼び出しが必要になりました(現在、前、および次の月を読み込むため)。
再帰ライブラリを見つけました https://github.com/tplaner/When ルールをデータベースに保存し、1つのクエリですべての関連ルールを取得します。
うまくいけば、これが他の人の助けになることを願っています。私が何時間もかけて、良い解決策を見つけようとしたからです。
編集:このライブラリは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/tusharmath/sheql/wiki/Rules
ルールは拡張可能であり、スキーマの移行などを気にせずに、実行する繰り返しロジックの種類に基づいて任意の種類のカスタマイズを追加できます。
これは完全に異なるアプローチであり、独自の欠点がある場合があります。
システムテーブルに保存されている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:
@Rogue Coder
これは素晴らしい!
モジュロ演算(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」の日付を示す「repeat_interval_1_end」などを追加できます。しかし、これはクエリをより複雑にし、これを行う方法を本当に理解することはできません...
たぶん誰かが助けることができます!
挙げた2つの例は非常に単純です。それらは単純な間隔として表すことができます(最初は4日間、2番目は14日間)。これをどのようにモデル化するかは、繰り返しの複雑さに完全に依存します。上記が本当に単純なものである場合は、開始日と繰り返し間隔の日数を保存します。
ただし、次のようなサポートが必要な場合
イベントAは、2011年3月3日から毎月3日に繰り返されます。
または
イベントAは、2011年3月11日に始まる月の第2金曜日を繰り返します
それはもっと複雑なパターンです。