Organiser les sample_sets par projet pour accélérer les requêtes
Actuellement on a un système qui est très flexible en termes de permissions et d'associations de samples à plusieurs sets, voire plusieurs groupes. Mais c'est aussi plutôt lent à cause du système de permissions que j'ai créé.
Avec la notion de hierarchie de groupes, on se retrouve avec des requêtes plutôt complexes pour déterminer si l'utilisateur a accès à un élément.
Il faut chercher si l'utilisateur appartient à un groupe qui a la permission
access
ou à un groupe dont le parent a la permissionaccess
Donc lister les sets d'un type particulier auxquels un utilisateur a accès revient au code suivant:
def vidjil_accessible_query(self, name, table, user_id=None):
"""
Returns a query with all accessible records for user_id or
the current logged in user
this method does not work on GAE because uses JOIN and IN
This is an adaptation of accessible_query that better fits
the current auth system with group associations
:param: name: The name of the query (eg. 'read')
:param: table: The table for accessibility
:param: user_id: ID of the user
Example:
Use as::
db(auth.vidjil_accessible_query('read', db.mytable, 1)).select(db.mytable.ALL)
"""
if not user_id:
user_id = self.user_id
db = self.db
if isinstance(table, str) and table in self.db.tables():
table = self.db[table]
elif isinstance(table, (Set, Query)):
# experimental: build a chained query for all tables
if isinstance(table, Set):
cquery = table.query
else:
cquery = table
tablenames = db._adapter.tables(cquery)
for tablename in tablenames:
cquery &= self.vidjil_accessible_query(name, tablename,
user_id=user_id)
return cquery
if not isinstance(table, str) and\
self.has_permission(name, table, 0, user_id):
return table.id > 0
membership = self.table_membership()
permission = self.table_permission()
perm_groups = self.get_permission_groups(name, user_id)
query = (table.id.belongs(
db(((membership.user_id == user_id) &
(membership.group_id.belongs(perm_groups)) &
(membership.group_id == permission.group_id) &
(permission.name == PermissionEnum.access.value) &
(permission.table_name == table)))._select(permission.record_id)) |
table.id.belongs(
db(((membership.user_id == user_id) &
(membership.group_id.belongs(perm_groups)) &
(membership.group_id == db.group_assoc.second_group_id) &
(db.group_assoc.first_group_id == permission.group_id) &
(permission.name == PermissionEnum.access.value) &
(permission.table_name == table)))._select(permission.record_id)))
if self.settings.everybody_group_id:
query |= table.id.belongs(
db(permission.group_id == self.settings.everybody_group_id)
(permission.name == name)
(permission.table_name == table)
._select(permission.record_id))
return query
Ce qui produit la requête SQL suivante:
SELECT `patient`.`id`, `patient`.`first_name`, `patient`.`last_name`, `patient`.`birth`, `patient`.`info`, `patient`.`id_label`, `patient`.`creator`, `patient`.`sample_set_id` FROM `patient`
WHERE (
(`patient`.`id` IN
(SELECT `auth_permission`.`record_id` FROM `auth_permission`, `auth_membership`
WHERE (
(
(
(
(`auth_membership`.`user_id` = 2)
AND (`auth_membership`.`group_id` IN (4))
)
AND (`auth_membership`.`group_id` = `auth_permission`.`group_id`)
) AND (`auth_permission`.`name` = 'access')
) AND (`auth_permission`.`table_name` = 'patient')
))
)
OR (`patient`.`id` IN
(SELECT `auth_permission`.`record_id` FROM `auth_permission`, `auth_membership`, `group_assoc`
WHERE (
(
(
(
(
(`auth_membership`.`user_id` = 2)
AND (`auth_membership`.`group_id` IN (4))
)
AND (`auth_membership`.`group_id` = `group_assoc`.`second_group_id`)
) AND (`group_assoc`.`first_group_id` = `auth_permission`.`group_id`)
) AND (`auth_permission`.`name` = 'access')
) AND (`auth_permission`.`table_name` = 'patient')
)
)
));
On a donc une requête composée de deux sous-requêtes chacune contenant un IN
aussi long que le nombre de groupes auxquels l'utilisateur appartient, l'une avec un join
et l'autre avec deux join
(ici les joins sont implicites).
Et les autres primitives du model VidjilAuth
sont composées de plusieurs joins
et souvent plusieurs requêtes.
Avec une organisation par projet (avec une nouvelle table, et une clef étrangère dans la table sample_set
) on pourrait se débarrasser complètement de la permission access
, et par la même occasion on laisse tomber cette anbiguïté qu'on a avec les groupes de projets/hôpitaux et les groupes de rôles/permissions.
La requête "patient" deviendrait:
def get_project_sets(type, project_id):
return db(
(db.sample_set.project_id == project_id) &
(db.sample_set.sample_type == type) &
(db[type].sample_set_id == db.sample_set.id)
).select(db[type].*)
Avec auparavant avoir vérifié que l'utilisateur ait accès à un groupe/rôle apparetenant au projet (2 joins sur relativement peu de données). Ce qui nous donne un total de 4 joins sur deux requêtes par rapport à un total de 3 joins répartis sur 3 requêtes, et on perd les IN
Il y a plusieurs autres endroits où des gains de jointures ou de requêtes seraient aussi envisageables (get_permission(...)
, can_view_sample_set(...)
, ...).
Ainsi que des endroits où les requêtes seraient plus complexes ?
J'aurais tendance à vouloir limiter les associations de samples à des sets du même projet car celà simplifierai certaines requêtes, comme l'autocompletiondes tags, et ainsi que celle des samples_sets à l'ajout d'un sample.
Ca éliminerait également l'impacte des grosses requêtes auxquelles nous les admins soumettons la BDD. Et donc on n'occuperait plus un (voire plus) de processus uwsgi pendant des durées prolongées (par exemple, plus d'une minute pour un compare patient).
Cette moification demanderait beaucoup de travail, donc ça ne serait pas pour tout de suite, mais je pense que celà réduirait le nombre de timeouts subis par nos utilisateurs. Celà demanderait peut-être plus d'analyse, de tests et de benchmarks.