Bonjour à tous, je poste ce sujet ici, mais j'aurais surement pu le créer plutôt dans la forum postgreSQL ? Excusez moi d'avance si je suis un peu hors sujet.
Je n'ai vraiment pas réussi à trouver une réponse définitive à mon problème.
Je développe dans le cadre de mon stage une application Python, destinée à tourner comme un service windows, qui interroge et remplit une base de données postgreSQL.
J'ai déjà développé plusieurs outils en Python, et je tiens à faire les choses bien (respect des normes PEP8, utilisation de classes organisées, aussi, si vous voyez un problème, n'hésitez pas à me faire vos remarques).
Je me demandais donc comment gérer des accès à la base de données de multiples clients simultanés (cas que je ne rencontre pas vraiment, mais je veux quelque chose de propre et évolutif), et ce dans un cas bien particulier.
Je dispose de plusieurs methodes sur ce modèle :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
def add_message(self, message):
try:
self.cursor.execute("""SELECT *
FROM messages
WHERE content = %s""", (message,))
rows = self.cursor.fetchall()
if len(rows) == 0:
self.cursor.execute("""INSERT INTO messages (content)
VALUES (%s) returning ID""", (message,))
id_message = self.cursor.fetchone()[0]
else:
id_message = rows[0][0]
if len(rows) > 1:
logging.warning("Found several messages occurrences in "
"the dataBase for : " + message)
return id_message
except psycopg2.ProgrammingError as e1:
raise DAOProgrammingError("addMessage : SQL Trace " + str(e1))
except psycopg2.Error as e2:
raise DAOError("Can't add message the syslog event : " + str(e2)) |
Cette méthode fonctionne très bien, elle permet de vérifier si le message existe, dans le cas contraire l'insère, et dans tous les cas renvoie l'id du message ajouté (la table comporte 2colonnes avec une clé auto incrémentée : id et message tout bêtement). Elle permet également de signaler dans les logs les éventuelles "incohérences".
Problème : J'envisage le cas où deux clients utilisant cette méthode seraient exécutés en même temps, et qui pour une raison X ou Y auraient le comportement suivant, chronologiquement :
___Client 1 :
SELECT * FROM messages WHERE content = 'nouveau message 1';
=> Le client voit que ce message n'existe pas
___Client 2
SELECT * FROM messages WHERE content = 'nouveau message 1';
=> Le client voit que ce message n'existe pas
___Client 1 continue son exécution :
Le message n'existe pas, donc je l'insère
INSERT INTO messages (content) VALUES ('nouveau message 1') returning ID
___Client 2 continue son exécution:
Le message n'existe pas, donc je l'insère
INSERT INTO messages (content) VALUES ('nouveau message 1') returning ID
Comme vous pouvez vous en douter, les deux clients au même moment se rendent compte que la valeur n'existe pas et donc la créés. La question peut paraître basique, mais comment empêcher suite à ce traitement d'obtenir :
id + message
1 | 'nouveau message 1'
2 | 'nouveau message 1'
Et donc un doublon...
En soit, je pourrais me débrouiller pour utiliser un "INSERT" avec une close "WHERE NOT" qui aurait le même effet, et qui se ferait donc en une seule exécution requête, supprimant ainsi l'étape intermédiaire qui pause problème, mais cette solution ne me satisfait pas (en effet, peut être qu'un jour, je n'aurai pas le choix...)
J'ai vu également qu'il était possible de mettre le niveau d'isolation des requêtes sur "Serialized" (il est ici en autocommit), mais ceci semble générer des exceptions en cas d'échec, ce qui complexifie le programme...
Ma question est donc, quelle est la "Best Practice" dans ce genre de cas ?
A noter que j'utilise la librairie psycopg2 pour mes requêtes ( http://initd.org/psycopg/docs/ ) avec python 2.7.5.
Merci beaucoup d'avance pour vous réponses.
EDIT : J'ai réussi à éviter l'erreur via :
1 2 3 4 5 6 7 8 9 10 11 12
|
BEGIN;
LOCK TABLE messages IN ACCESS EXCLUSIVE MODE;
INSERT INTO messages (content)
SELECT 'nouveau message 1'
WHERE NOT EXISTS (
SELECT content
FROM messages
WHERE content = 'nouveau message 1'
)
returning id;
COMMIT; |
Je me garde cette solution sous le coude, est ce la seule à votre avis?
Partager