Soit la situation suivante : vous avez une classe qui modélise une table de votre base de données. Cette classe possède un champ de type date.
Comment faire pour pouvoir utiliser côté Perl, un timestamp et stocker en base une date formatée pour la base de données ?
C’est le problème que je me suis posé récemment avec Coat::Persistent. Plus exactement, ce problème s’est posé presque de lui-même pendant ma présentation aux Journées Perl 2009, suite à une question du public.
En fait, je n’avais que la moitié de la solution, et depuis, je me suis pris d’un défi pour résoudre correctement ce problème avec Coat::Persistent.
Je vous propose de voir ensemble comment faire.
Objectifs
- Utiliser le champ date comme un entier dans le code Perl
- Ne pas avoir a se soucier de son format de stockage
Dans sa version actuelle, Coat::Persistent ne fait pas de différence entre la valeur assignée a un attribut d’un objet et celle stockée en base. Il nous est donc impossible de réaliser notre objectif de manière élégante sans modifier Coat::Persistent.
La bonne façon de permettre cette fonctionnalité serait donc de dire qu’un attribut peut avoir un type propre (isa) et un type de stockage. On aurait donc quelquechose comme ça :
has_p created_at => (
is => 'rw',
isa => 'Int',
store_as => 'DateTime',
);
Un attribut ainsi déclaré serait donc conscient que sa valeur mémoire (celle de l’objet instancié) est différente de celle stockée en base. Toute la logique de conversion qu’elle soit dans un sens ou dans l’autre serait donc gérée par Coat::Persistent, et non pas par l’utilisateur.
Tout cela est réalisable en utilisant une coercition bi-directionnelle. Derrière ce mot barbare se cache un principe finalement assez simple : une valeur x doit être convertible d’un type A vers un type B, et reciproquement.
Coat permet de définir des coeriction via le mécanisme de types utilisateurs. La subtilité est donc de :
- Avoir une coercition de définie pour pouvoir convertir une valeur du type de l’attribut vers une valeur du type de stockage (qui interviendra avant un
save) - Avoir une coercition de définie pour pouvoir convertir une valeur du type de stockage vers une valeur du type de l’attribut (qui interviendra après un
find)
Voyons maintenant comment écrire ces types et leur règle de coercition respectives pour le coupe Int,DateTime
Nous allons commencer par définir le type DateTime que nous voulons utiliser pour représenter le format de stockage des date dans une table MySQL (YYYY-MM-DD HH:MM:SS).
subtype 'DateTime'
=> as 'Str'
=> where { /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/ };
Un attribut de type DateTime est donc un attribut de type Str et dont la valeur respecte la regexp fournie.
Maintenant il nous faut écire la règle de conversion d’une valeur Int vers une valeur DateTime :
coerce 'DateTime'
=> from 'Int'
=> via {
my ($sec, $min, $hour, $day, $mon, $year) =
localtime($_);
$year += 1900;
$mon++;
$day = sprintf('%02d', $day);
$mon = sprintf('%02d', $mon);
$hour = sprintf('%02d', $hour);
$min = sprintf('%02d', $min);
$sec = sprintf('%02d', $sec);
return "$year-$mon-$day $hour:$min:$sec";
};
Le type Int est un type standard, nous n’avons donc pas besoin de le définir. Nous avons seulement besoin de mettre en place une coercition depuis le type DateTime vers le type Int :
coerce 'Int'
=> from 'DateTime'
=> via {
my ($year, $mon, $day, $hour, $min, $sec) =
/^(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/;
$year -= 1900;
$mon--;
return mktime(
int($sec), int($min), int($hour),
$day, $mon, $year);
};
Bien, tout ce code est intéressant, mais est-ce réellement à l’utilisateur de l’ORM de le définir ? Je ne crois pas, sa place serait idéale dans un jeu de types prédéfinis.
Pourquoi pas proposer une module Coat::Persistent::Types avec tous les types nécessaires ? Cela me semble bien plus élégant.
Imaginons donc que les types et coercitions définis ci-dessus seraient présents dans, Coat::Persistent::Types::MySQL. Le type pourrait même se nommer 'MySQL:DateTime' au lieu de 'DateTime'.
L’utilisateur pourrait donc faire tout simplement :
package Stuff;
use Coat;
use Coat::Persistent;
use Coat::Persistent::Types::MySQL;
has_p created_at => (
isa => 'Int',
store_as => 'MySQL:DateTime',
);
Et le tour serait joué !
Il ne resterait alors qu’une seule chose à faire : patcher la mécanique de sauvegarde de Coat::Persistent pour que les valeurs utilisées dans le SQL puissent être converties si nécessaire.
Cela peut se faire très simplement en introduisant la notion de valeur de stockage. Cette valeur serait égale à celle de l’attribut si aucun store_as n’est défini, elle serait égale à la coercition adéquate sinon.
Bien, maintenant que ce problème est résolu, il ne me reste plus qu’à patcher Coat::Persistent et à publier une nouvelle version avec toutes ces bonnes calories intellectuelles…
Hello
J’étais persuadé que ta présentation avec les questions qui ont suivies apporteraient quelque chose à coat::persistent.
En voici la preuve.
Oui en effet, ça donne de la motivation pour avancer. Du coup j’aimerai bien avoir le mail de la personne qui m’a posé cette fameuse colle sur les timestamp pendant la présentation.
Te souviens-tu de qui c’était, et si oui connais-tu son nom/email ?
Merci !
Hello
Je ne me souviens plus très bien de la personne qui est intervenue.
Désolé.
Seb