web-dev-qa-db-fra.com

Événements récurrents / répétitifs du calendrier - Meilleure méthode de stockage

Je construis un système d'événements personnalisé, et si vous avez un événement répétitif qui ressemble à ceci:

L'événement A se répète tous les 4 jours à partir du 3 mars 2011

ou

L'événement B se répète toutes les deux semaines le mardi à compter du 1er mars 2011

Comment puis-je stocker cela dans une base de données de manière à simplifier les recherches? Je ne veux pas de problèmes de performances s’il ya un grand nombre d’événements et je dois les parcourir tous lors du rendu du calendrier.

283
Brandon Wamboldt

Stocker des motifs répétitifs "simples"

Pour mon calendrier basé sur PHP/MySQL, je voulais stocker les informations sur les événements récurrents/récurrents aussi efficacement que possible. Je ne voulais pas avoir un grand nombre de lignes et je voulais rechercher facilement tous les événements qui se dérouleraient à une date donnée.

La méthode ci-dessous est idéale pour stocker des informations répétitives qui se produisent à intervalles réguliers, comme tous les jours, tous les n jours, toutes les semaines, tous les mois, etc. elles sont également stockées séparément chaque semaine à partir du mardi et toutes les semaines à partir du jeudi.

En supposant que j'ai deux tables, une appelée events comme ceci:

ID    NAME
1     Sample Event
2     Another Event

Et une table appelée events_meta comme ceci:

ID    event_id      meta_key           meta_value
1     1             repeat_start       1299132000
2     1             repeat_interval_1  432000

Repeat_start étant une date sans heure comme horodatage unix, et repeat_interval un montant en secondes entre les intervalles (432000 correspond à 5 jours).

repeat_interval_1 correspond à repeat_start de l'ID 1. Ainsi, si j'ai un événement qui se répète tous les mardis et tous les jeudis, repeat_interval serait de 604800 (7 jours), et il y aurait 2 repeat_starts et 2 repeat_intervals. Le tableau ressemblerait à ceci:

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

Ensuite, si vous avez un calendrier qui parcourt tous les jours en saisissant les événements du jour auquel il se présente, la requête se présentera comme suit:

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

Remplacement de {current_timestamp} par l’horodatage Unix de la date actuelle (Moins l’heure, les valeurs d’heure, de minute et de seconde sont donc réglées sur 0).

J'espère que cela aidera quelqu'un d'autre aussi!


Stockage de motifs répétitifs "complexes"

Cette méthode convient mieux au stockage de motifs complexes tels que

Event A repeats every month on the 3rd of the month starting on March 3, 2011

ou

Event A repeats Friday of the 2nd week of the month starting on March 11, 2011

Je recommanderais de combiner cela avec le système ci-dessus pour plus de flexibilité. Les tables pour ceci devraient ressembler à:

ID    NAME
1     Sample Event
2     Another Event

Et une table appelée events_meta comme ceci:

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 représente la semaine du mois en cours, qui pourrait être compris entre 1 et 5 potentiellement. repeat_weekday dans le jour de la semaine, du 1 au 7.

En supposant maintenant que vous parcouriez les jours/semaines pour créer une vue mensuelle dans votre calendrier, vous pouvez composer une requête comme celle-ci:

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

Ceci combiné avec la méthode ci-dessus pourrait être combiné pour couvrir la plupart des modèles d'événements répétitifs/récurrents. Si j'ai oublié quelque chose s'il vous plaît laissez un commentaire.

195
Brandon Wamboldt

Bien que la réponse actuellement acceptée m'aide énormément, je voulais partager quelques modifications utiles qui simplifient les requêtes et augmentent également les performances.


Evénements de répétition "simples"

Pour gérer des événements qui se répètent à intervalles réguliers, tels que:

Repeat every other day 

ou

Repeat every week on Tuesday 

Vous devriez créer deux tables, l'une appelée events, comme ceci:

ID    NAME
1     Sample Event
2     Another Event

Et une table appelée events_meta comme ceci:

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

Avec repeat_start étant une date d'horodatage Unix sans heure (1369008000 correspond au 20 mai 2013), et repeat_interval un montant en secondes entre les intervalles (604800 correspond à 7 jours).

En parcourant chaque jour du calendrier, vous pouvez obtenir des événements répétés en utilisant cette requête simple:

SELECT EV.*
FROM `events` EV
RIGHT JOIN `events_meta` EM1 ON EM1.`event_id` = EV.`id`
WHERE  (( 1299736800 - repeat_start) % repeat_interval = 0 )

Remplacez simplement l'horodatage unix (1299736800) par chaque date de votre calendrier.

Notez l'utilisation du modulo (signe%). Ce symbole ressemble à une division normale, mais renvoie le '' reste '' au lieu du quotient, et vaut 0 si la date actuelle est un multiple exact de repeat_interval du repeat_start.

Comparaison de performance

Ceci est nettement plus rapide que la réponse précédemment suggérée "méta_keys", qui était la suivante:

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

Si vous exécutez EXPLAIN, cette requête nécessitera l’utilisation d’un tampon de jointure:

+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+
| 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 |
+----+-------------+-------+--------+---------------+---------+---------+------------------+------+--------------------------------+

La solution à 1 jointure ci-dessus ne nécessite aucun tampon de ce type.


Patterns "complexes"

Vous pouvez ajouter un support pour des types plus complexes pour prendre en charge ces types de règles de répétition:

Event A repeats every month on the 3rd of the month starting on March 3, 2011

ou

Event A repeats second Friday of the month starting on March 11, 2011

Votre table d'événements peut avoir exactement le même aspect:

ID    NAME
1     Sample Event
2     Another Event

Ensuite, pour prendre en charge ces règles complexes, ajoutez des colonnes à events_meta comme ceci:

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    

Notez que vous devez simplement spécifier un repeat_interval ou un ensemble de repeat_year, repeat_month, repeat_day, repeat_week et repeat_weekday données.

Cela rend la sélection des deux types simultanément très simple. Il vous suffit de parcourir chaque jour et de renseigner les valeurs correctes (1370563200 pour le 7 juin 2013, puis l'année, le mois, le jour, le numéro de la semaine et le jour de la semaine comme suit):

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
  )

Ceci renvoie tous les événements qui se répètent le vendredi de la deuxième semaine ainsi que tous les événements qui se répètent chaque vendredi. Il renvoie donc à la fois l'ID d'événement 1 et 2:

ID    NAME
1     Sample Event
2     Another Event

* Sidenote dans le code SQL ci-dessus j'ai utilisé date de PHP index par défaut du jour de la semaine, donc "5" pour vendredi


J'espère que cela aide les autres autant que la réponse originale m'a aidé!

171
ahoffner

Amélioration: remplacer l'horodatage par la date

Comme une petite amélioration à la réponse acceptée qui a ensuite été affinée par ahoffner - il est possible d'utiliser un format de date plutôt que l'horodatage. Les avantages sont:

  1. dates lisibles dans la base de données
  2. aucun problème avec les années> 2038 et l'horodatage
  3. supprime la nécessité d’être prudent avec les horodatages basés sur des dates désaisonnalisées, c’est-à-dire que le 28 juin au Royaume-Uni débute une heure plus tôt que le 28 décembre.

pour ce faire, modifiez la base de données _ repeat_start afin qu'elle soit stockée sous le type 'date' et repeat_interval maintenant des jours plutôt que des secondes. soit 7 pour une répétition de 7 jours.

changer la ligne sql:

WHERE (( 1370563200 - repeat_start) % repeat_interval = 0 )

à:

WHERE ( DATEDIFF( '2013-6-7', repeat_start ) % repeat_interval = 0)

tout le reste reste le même. Simples!

23
user3781087

Pour tous ceux qui sont intéressés par ceci, maintenant vous pouvez simplement copier et coller pour commencer en quelques minutes. J'ai pris le conseil dans les commentaires aussi bien que j'ai pu. Dites-moi si je manque quelque chose.

"VERSION COMPLEXE":

événements

 + ---------- + ---------------- + 
 | ID | NOM | 
 + ---------- + ---------------- + 
 | 1 | Exemple d'événement 1 | 
 | 2 | Deuxième événement | 
 | 3 | Troisième événement | 
 + ---------- + ---------------- + 

événements_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 | 
 + ---- + ---------- + -------------- + ----------- ------- + ------------- + -------------- + ------------ + ------------- + ---------------- + 

Code 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');

également disponible en tant que exportation MySQL (pour un accès facile)

Exemple de code 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 />";
    }
?>

Exemple de code 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
// ----------------------------------------------------------------------------------------------------
?>

Le code php est également disponible ici (pour une meilleure lisibilité):
index.php
et
connect.php
La mise en place devrait prendre quelques minutes. Pas des heures. :)

23
Alex

Je voudrais suivre ce guide: https://github.com/bmoeskau/Extensible/blob/master/recurrence-overview.md

Assurez-vous également que vous utilisez le format iCal afin de ne pas réinventer la roue et rappelez-vous la règle n ° 0: Ne stockez PAS d'instances d'événement récurrentes individuelles sous forme de lignes dans votre base de données!

19
Gal Bracha

Pendant que les solutions proposées fonctionnaient, j’essayais d’implémenter Full Calendar et il faudrait plus de 90 appels de base de données pour chaque vue (lorsqu’il charge les versions actuelle, précédente et le mois prochain), ce qui ne me passionne pas trop.

J'ai trouvé une bibliothèque de récursion https://github.com/tplaner/When où vous stockez simplement les règles dans la base de données et une requête pour extraire toutes les règles pertinentes.

J'espère que cela aidera quelqu'un d'autre, car j'ai passé tant de temps à essayer de trouver une bonne solution.

Edit: Cette bibliothèque est pour PHP

15
Tim Ramsey

Pourquoi ne pas utiliser un mécanisme similaire aux tâches Apache Cron? http://en.wikipedia.org/wiki/Cron

Pour le calendrier et la planification, j'utiliserais des valeurs légèrement différentes pour les "bits" afin de prendre en charge les événements de réapparition du calendrier standard - au lieu de [jour de la semaine (0 - 7), mois (1 - 12), jour du mois (1 - 31), heure (0 - 23), min (0 - 59)]

- J'utiliserais quelque chose comme [Année (répéter tous les N ans), mois (1 - 12), jour du mois (1 - 31), semaine du mois (1-5), jour de la semaine (0 - 7) ]

J'espère que cela t'aides.

14
Vladimir

J'ai développé un langage de programmation ésotérique juste pour ce cas. La meilleure partie de cela est qu’il est moins schématique et indépendant de la plate-forme. Il vous suffit d'écrire un programme de sélection, pour votre emploi du temps, dont la syntaxe est contrainte par l'ensemble des règles décrites ici -

https://github.com/tusharmath/sheql/wiki/Rules

Les règles sont extensibles et vous pouvez ajouter n'importe quel type de personnalisation en fonction du type de logique de répétition que vous souhaitez effectuer, sans vous soucier des migrations de schéma, etc.

Cette approche est complètement différente et pourrait présenter certains inconvénients.

4
tusharmath

Cela ressemble beaucoup aux événements MySQL qui sont stockés dans des tables système. Vous pouvez regarder la structure et déterminer quelles colonnes ne sont pas nécessaires:

   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:
4
Valentin Kuzub

@Rogue Coder

C'est bien!

Vous pouvez simplement utiliser l'opération modulo (MOD ou% dans mysql) pour simplifier votre code à la fin:

Au lieu de:

AND (
    ( CASE ( 1299132000 - EM1.`meta_value` )
        WHEN 0
          THEN 1
        ELSE ( 1299132000 - EM1.`meta_value` )
      END
    ) / EM2.`meta_value`
) = 1

Faire:

$current_timestamp = 1299132000 ;

AND ( ('$current_timestamp' - EM1.`meta_value` ) MOD EM2.`meta_value`) = 1")

Pour aller plus loin, on pourrait inclure des événements qui ne se reproduisent plus jamais.

Quelque chose comme "repeat_interval_1_end" pour indiquer la date du dernier "repeat_interval_1" pourrait être ajouté. Cela rend toutefois la requête plus compliquée et je ne peux pas vraiment comprendre comment faire cela ...

Peut-être que quelqu'un pourrait aider!

3
dorogz

Les deux exemples que vous avez donnés sont très simples. ils peuvent être représentés comme un simple intervalle (le premier étant de quatre jours, le second de 14 jours). Votre modélisation dépendra entièrement de la complexité de vos récurrences. Si ce que vous avez ci-dessus est vraiment aussi simple que cela, stockez une date de début et le nombre de jours dans l'intervalle de répétition.

Si, toutefois, vous devez soutenir des choses comme

L'événement A se répète tous les mois le 3 du mois à compter du 3 mars 2011

Ou

L'événement A se répète le deuxième vendredi du mois à compter du 11 mars 2011

Ensuite, c'est un modèle beaucoup plus complexe.

1
Adam Robinson