Page tree
Skip to end of metadata
Go to start of metadata

Ce document décrit la gestion de la dépendance entre les domaines de cytomine, et en particulier la gestion de la suppression en cascade.

Problème

Exemple:

Class A {
  B b
}
 
Class B {
}

Si B doit être supprimé, que faire avec A?

La solution habituelle est de refuser la suppression ou d'utiliser le "Delete on cascade SQL". Cette dernière solution est incompatible avec notre système de commande, nous devions le gérer nous même.

  1. La suppression en cascade était mal gérée: la suppression d'un domain entrainait souvent des erreurs de contraintes du a d'autres domaines qui dépendandait de lui. 
    Chaque fois qu'on rajoutait un nouveau domaine avec des dépendances, on oubliait de gérer la suppression en casacde (ex: AnnotationFilter, LastConnection,...).
     
  2. Le code de suppression en cascade, lorsqu'il était écrit, était peu élégant et difficilement réutilisable. 
    Si on supprime une ontologie: on supprime les termes, qui eux même doivent supprimer leurs relations.
    Si on supprime un terme: on supprime les relations.
    Idéalement, le domaine à supprimer doit supprimer ses dépendances directes (ontologie supprime termes). Les dépendances indirectes seront supprimées lors de la suppression des dépendances directes (ontologie supprime terme, terme supprime relation).

  3. Pour avoir un undo/redo fonctionnel, il est impératif d'avoir une bonne méthodologie (faire: controller+service+gestion des CRUD via command+sécurité+tests+,...) et une architecture homogène.

Un système de gestion de dépendance a été mis en place afin de résoudre ces 3 problèmes.  

 

Solution 1: Le delete en cascade

L'idée du mécanisme était de:

  • Forcer l'écriture de code de suppression pour les dépendances afin de ne pas l'oublier.
  • Mettre un place un mécanisme pour gérer les suppressions par imbrications sans se répetere et le plus automatiser possible.
  • Etre souple lors des suppressions en cascade: on peut supprimer, refuser la suppression, modifier,...
  • Chaque domaine doit gérer ses propres suppressions de dépendances: pour la suppression d'un domain "root" (ex: ontologie), on supprime juste chaque domaine directement lié (terme), chacun de ces domaines devra supprimer ses propres dépendances (relation-term,...).

 

Fonctionnement

Script d'analyse

Un script sera exécuté dans le bootstrap et/ou dans un test. Il analysera, pour chaque domaine, ses dépendances vers d'autres objets Cytomine.
Exemple: Un terme à un attribut "name" (type String) et un attribut "ontology" (type Ontology) => dépendance: ontology de la classe Ontology

Le script est de type

//browse each domain	        
grailsApplication.getDomainClasses().each { domain ->
			
	 if(!domainToSkip.contains(domain.name)) {
		 //analyze properties, just keep dependence with type/refrence "be.cytomine." (cytomine domain, no string/int/data/...dependence)
         def columnDep = getDependencyColumn(domain,["be.cytomine."],["be.cytomine."])
         allErrors.addAll(checkServiceMethod(domain,columnDep,fixDomainName))
 
		 //analyze hasMany properties, just keep dependence with type java.util.Set/SortedSet/List (just HasMany) and withrefrence "be.cytomine." (no HasMany String for example)
         def columnDepHasMany = getDependencyColumn(domain,["java.util.Set","java.util.SortedSet","java.util.List"],["be.cytomine."])
         allErrors.addAll(checkServiceMethodInverse(domain,columnDepHasMany,fixDomainName))
    }
}
 

La liste "allErrors" contiendra toutes les dépendances qui n'ont pas été gérée par le développeur.
Chaque fois que le développeur ajouter/modifiera un domaine avec des dépendances supplémentaires, il devra donc ajouter une méthode deleteDependent.
Il aura une info de type:
"Service OntologyService must implement deleteDependentTerm(Ontology,transaction)!!!"
"Service OntologyService must implement deleteDependentProject(Ontology,transaction)!!!"
...

 

Fonction de suppression de dépendance

Si A dépend de B, et que B dispose d'une méthode "delete" dans son service, alors le service de B devra avoir une méthode deleteDependentA(B b, Transaction transaction)). La méthode "delete" de B appellera automatiquement toutes les méthodes deleteDependentX du service de B.

Exemple: Si term dépend de Ontology, si il existe une méthode delete(Ontology) dans le service Ontology, alors on devra écrire une méthode deleteDependentTerm(Ontology ontology, Transaction transaction)) dans OntologyService. 

Suppression d'une Ontology
=> requête web 
=> ontologyService.delete(json, sec)
=> transaction.start() 
=> ontologyService.delete(ontology,transaction) 
=> deleteDependentTerm(ontology,transaction) 
===> deleteDependentAlgoAnnotationTerm(term, transaction) //delete en cascade d'autres domaines...
===> deleteDependentAnnotationTerm(term, transaction) //delete en cascade d'autres domaines...
===> ...
=> deleteDependentProject(ontology,transaction) //pour l'instant, on refuse la suppression si y a un projet...sinon on pourrait techniquement deleter les projets en cascade
=> delete ontology


L'implémentation des méthodes deleteDependentX est libre:

  • supprimer les domaines via des commandes (pour les retrouver dans un undo/redo) => si je fais un undo de delete ontology, je veux récupérer ses termes aussi.
  • supprimer les domaines sans passer par des commandes (perdu en cas de undo/redo) => les infos de dernière connexion sur une projet pourraient être oubliées en cas de delete (undo/redo) de projet.
  • changer des infos sur le domain dépendant => si on supprime un user, on pourrait associer ses dépendances à quelqu'un d'autres.
  • refuser la suppression => refuser la suppression d'une ontologie avec des projets par exemple.


Par exemple, dans ontologyService:

Class OntologyService {

	...
 
	//=> appelée lorsqu'on fait une requête web de suppression d'ontologie 
	def delete(def json, SecurityCheck security) throws CytomineException {
     	return delete(retrieve(json),transactionService.start())
	}
 
	//=> suppression de l'ontologie via le système de commande
	def delete(Ontology ontology, Transaction transaction = null, boolean printMessage = true) {
		SecUser currentUser = cytomineService.getCurrentUser()
		def json = JSON.parse("{id: ${ontology.id}}")
		return executeCommand(new DeleteCommand(user: currentUser,transaction:transaction), json) //appellera implicitement les méthodes deleteDependent du service
	}
 
	def deleteDependentTerm(Ontology ontology, Transaction transaction) {
    	Term.findAllByOntology(ontology).each {
        termService.delete(it,transaction, false) //va deleter en cascade les dépendances des terms...
    	}
	}

	def deleteDependentProject(Ontology ontology, Transaction transaction) {
    	if(Project.findByOntology(ontology)) {
        	throw new ConstraintException("Some projects use this ontology. Cannot delete ontology!")
    	}
	}
}

Solution 2: Soft delete

(In english)

A good way to avoid dependency issues is to avoid delete. A simple way to do that is to have a "Date deleted" attributes (null=not deleted).
All domain have their own deleted attributes. This means that we can delete an instance by setting its deleted field.

	def deleteDependentProject(Ontology ontology, Transaction transaction) {
    	if(Project.findByOntology(ontology)) {
        	it.deleted = new Date()
			it.save()
    	}
	}

The complex part of this mecanism is located in the listing services. All instances with "deletes IS NOT NULL" must be removed from the result. 

 

 

 

 

 

 

 

  • No labels