Un modèle de données pour Big Table.
Mardi 17 mars 2009Ce billet fait partie d’une série : Un Digg-like sous App Engine.
La première chose à définir dans notre application sera le modèle de données sous le DataStore d’App Engine. Pour un digg-like on a besoin de seulement trois tables :
- Item : ça représentera une URL ou un commentaire.
- User : ça représentera l’utilisateur.
- Vote : ça permettra de stocker les votes des user sur les items.
Contrairement à ce qu’on pourrait lire en survolant très rapidement pas mal d’articles sur App Engine. Il n’est pas interdit de concevoir des modèles relationnels. Dans certains cas, il est tout simplement impossible de faire autrement. Dans mon exemple, j’aurais très bien pu me passer de la table vote en stockant les votes à la fois dans la table item et dans la table user. Mais à mon niveau, je n’ai pas trouvé de réels intérêts à appliquer cette pratique. Si néanmoins cela vous interesse dans savoir plus sur les modèles de données idéal pour permettre une bonne scalabilité, je vous recommande cet article de Bret Taylor : How FriendFeed uses MySQL to store schema-less data.
Relation One-To-Many
Dans le cadre de la relation User/Item, nous avons une relation one to many (un user peut être l’auteur de plusieurs items). Le meilleur moyen de gérer ça sous App Engine et d’utiliser la propriété SelfReference, exemple :
class User(db.Model):
nickname = db.StringProperty(required=True)
class Item(db.Model):
title = db.StringProperty()
author = db.ReferenceProperty(User, collection_name='items')
De cette manière je peux non seulement accèder à l’objet User via l’objet Item de cette façon :
user = User(nickname="Toto") item = Item(author=user, title="Nouvel item") print item.author.nickname >> Toto
Mais cela permet aussi de récupérer dans l’autre sens, la liste des items dont le user est l’auteur :
user = User(nickname="Toto") item = Item(author=user, title="Nouvel item 1") item = Item(author=user, title="Nouvel item 2") item = Item(author=user, title="Nouvel item 3") for item in user.items: print item.title >> Nouvel item 1 >> Nouvel item 2 >> Nouvel item 3
Relation Many-To-Many
Je n’ai pas dans mon projet besoin d’utiliser cette relation mais à titre d’information, pour mettre en place correctement une relation many to many, il est conseillé de stocker une liste de clé du côté de l’objet qui risque de comporter le moins de relation vers l’autre objet. Exemple :
class Contact(db.Model):
name = db.StringProperty()
groups = db.ListProperty(db.Key)
class Group(db.Model):
name = db.StringProperty()
@property
def members(self):
return Contact.gql("WHERE groups = :1", self.key())
Dans ce cas, il est plus probable qu’un contact appartienne à deux ou trois groupes, plutôt qu’un groupe ne contienne que 2 ou 3 users, un groupe aura plus de chance d’avoir 50, 100 users. Il est donc plus opportun de stocker la liste côté user que côté groupe.
Custom Property
L’extrait de code ci-dessus m’amène à parler d’un dernier point dans cet article. L’utilisation de @property pour définir des attributs personnalisés à nos objets Model. Cela va nous permettre de créer des attributs qui ne sont pas forcement liés à un champ en base de données. Exemple :
class class Item(db.Model):
type = db.StringProperty(required=True, choices=set(["link", "comment"]))
author = db.ReferenceProperty(User, collection_name='items')
url = db.StringProperty()
host = db.StringProperty()
title = db.StringProperty()
text = db.TextProperty()
score = db.IntegerProperty()
@property
def score_number(self):
score_num = self.score
if score_num == 1 or score_num == 0:
return str(score_num)+" point"
else:
return str(score_num)+" points"
item = Item(type="link", ..., score=5)
print item.score_number
>> 5 points
L’attribut score_number n’est relié à aucun champ en base de données. C’est un attribut custom qui me permet d’afficher le nombre de points pour l’item en tenant compte de la gestion du pluriel. On peut imaginer énormement de possibilité grâce à ce système.
Conclusion
Au final, pour mon application, et en tenant compte de ce que je vous ai présenté ci-dessus, voilà le modèle que j’ai mis en place :
class Item(db.Model):
type = db.StringProperty(required=True, choices=set(["link", "comment"]))
author = db.ReferenceProperty(User, collection_name='items')
time = db.IntegerProperty()
datetime = db.DateTimeProperty(auto_now_add=True)
url = db.StringProperty()
host = db.StringProperty()
title = db.StringProperty()
text = db.TextProperty()
score = db.IntegerProperty()
deleted = db.BooleanProperty()
flag = db.BooleanProperty()
parent_direct = db.SelfReferenceProperty()
parent_top = db.SelfReferenceProperty(collection_name='allkids')
@property
def kids(self):
return self.gql("WHERE parent_direct = :1", self.key())
@property
def hours(self):
hours = tools.time_elapsed(self.time)
if hours 24:
days = int(hours/24)
if days == 1 or days == 0:
return str(days)+" jour"
else:
return str(days)+" jours"
else:
hrs = int(hours)
if hrs == 1 or hrs == 0:
return str(hrs)+" heure"
else:
return str(hrs)+" heures"
@property
def comments_number(self):
# Issue if more than 1000 comments....
comments_num = len(self.allkids.fetch(1000))
if comments_num == 1 or comments_num == 0:
return str(comments_num)+" commentaire"
else:
return str(comments_num)+" commentaires"
@property
def score_number(self):
score_num = self.score
if score_num == 1 or score_num == 0:
return str(score_num)+" point"
else:
return str(score_num)+" points"
@property
def id(self):
return self.key().id()
@property
def can_edit(self):
now = int(time.time())
#more than 30 minutes no edit
if (self.time + 1800) < now:
return True
else:
return False
class User(db.Model):
nickname = db.StringProperty(required=True)
created = db.DateTimeProperty(auto_now_add=True)
type = db.StringProperty(required=True, choices=set(["admin", "editor", "user"]))
karma = db.IntegerProperty()
weight = db.IntegerProperty()
email = db.EmailProperty(required=True)
password = db.StringProperty(required=True)
@property
def can_downvote(self):
if self.karma > 99:
return True
else:
return False
@property
def created_since(self):
from datetime import datetime
import time
hours = tools.time_elapsed(time.mktime(self.created.timetuple()))
if hours 24:
days = int(hours/24)
if days == 1 or days == 0:
return str(days)+" jour"
else:
return str(days)+" jours"
else:
hrs = int(hours)
if hrs == 1 or hrs == 0:
return str(hrs)+" heure"
else:
return str(hrs)+" heures"
class Vote(db.Model):
item = db.ReferenceProperty(Item)
user = db.ReferenceProperty(User)
time = db.IntegerProperty()
datetime = db.DateTimeProperty(auto_now_add=True)
type = db.StringProperty(required=True, choices=set(["up", "down"]))
Si vous avez des questions ou des remarques, n’hésitez pas, les commentaires sont là pour ça. Dans la prochaine partie, je parlerais du framework web utilisé par App Engine.
NB: Je souhaitais publier un article tous les soirs sur l’avancement de mon application, mais je n’ai vraiment pas le temps, donc mes articles arrivent avec un peu de retard. Etant donné que l’application est déjà terminée, j’en profite pour améliorer quelques points, sachant qu’à terme, le code sera disponible. J’en reparlerai bientôt.
J’espère aussi être assez clair, je ne rentre pas trop dans la profondeur afin de garder un article assez clair. Mais si vous vous avez des questions un peu plus pointus, les commentaires sont aussi là pour ça.
