Ségrégation des données des applications multi-tenantes

écrit par yannick.nguedong publié le mercredi 12 décembre 2018
Dans un précédent article, nous avons mis en place la structure de base d'une application multi-tenante. Dans cet article nous allons voir comment faire une séparation ou ségrégation de données. Les trois approches (je ne sais pas s'il y'en a d'autres :) ) pour gérer/stocker les données des applications multi-tenantes sont les suivantes:
  • Base de données unique, Schémas partagés (avec une colonne de table comme discriminant)
  • Base de données unique, Schémas séparés
  • Bases de données séparées
Voyons en détails leurs avantages et inconvénients.

Avantages et inconvénients des différentes approches

1. Base de données unique, Schémas partagés

Avantages
  • Facile d'utilisation (toutes les données sont centralisées)
  • Migrations du modèle de données aisées
  Inconvénients
  • Risque énorme de mélanger les données des clients
  • Développement plus complexe car demande un haut niveau d'abstraction pour cloisonner les données

2. Base de données unique, Schémas séparés

Avantages
  • Facilité de mise en oeuvre
  • Facilité de requêtage pour retrouver les données
  Inconvénients
  • La séparation des données n'est pas nette
  • Maintenance de la base de données très difficile

3. Bases de données séparées

Avantages
  • Facilité pour ajouter, modifier et supprimer de nouveaux clients
  • Facilité de requêtage pour retrouver les données
Inconvénients
  • Plus onéreux car demande autant de bases de données qu'il y a de clients
  Dans notre implémentation, nous allons partir avec une approche où les bases de données sont séparées. Cela offre une meilleure sécurité en terme de séparations de données.

Isolation des données : Implémentation

J'ai modifié la librairie citée dans l'article précédent pour ajouter la notion de configuration et de collection de ConnectionStrings par tenant. Elle est disponible sur le repository NuGet Puzzle.Core.Multitenancy et sur Github à ce lien. Ainsi, nous pouvons maintenant avoir ceci dans le fichier de configuration :
{
  "MultitenancyOptions": {
    ...
    "Tenants": [
      {
        "Name": "Tenant 1",
        "Hostnames": [
          "localhost:47887",
          "localhost:44301",
          "localhost:60000"
        ],
        "Theme": "{DS}",
        "ConnectionStrings": {
          "DefaultConnection": {
            "ConnectionString": "server=localhost;database=db;username=user;password=pass;",
            "ProviderName": "System.Data.SqlClient"
          },
          "Test1": {
            "ConnectionString": "server=localhost;database=db;username=user;password=pass;",
            "ProviderName": "System.Data.SqlClient"
          },
          "Test2": {
            "ConnectionString": "server=localhost;database=db2;username=user2;password=pass2;",
            "ProviderName": "System.Data.SqlClient"
          }
        },
        ...,
      {
        "Name": "Tenant 2",
        "Hostnames": [
          "localhost:44302",
          "localhost:60001"
        ],
        "Theme": "",
        "ConnectionString": ""
      },
      {
        "Name": "Tenant 3",
        "Hostnames": [
          "localhost:44304",
          "localhost:44305"
        ],
        "Theme": "",
        "ConnectionString": ""
      },
      {
        "Name": "Tenant 4",
        "Hostnames": [
          "localhost:51261",
          "localhost:51262"
        ],
        "Theme": "",
        "ConnectionString": "xxx2898988"
      }
    ]
  }
}
Il nous suffit de modifier la méthode ConfigurePerTenantServices dans notre fichier Startup.cs pour prendre en considération ces nouvelles informations. Nous aurons donc :
/// <summary>
        /// Configures the services for specific tenant to add to the ASP.NET Core Injection of Control (IoC) container. This method gets
        /// called by the ASP.NET runtime. See
        /// http://blogs.msdn.com/b/webdev/archive/2014/06/17/dependency-injection-in-asp-net-vnext.aspx.
        /// </summary>
        /// <param name="services">The services collection or IoC container.</param>
        /// <param name="tenant">The tenant object.</param>
        public void ConfigurePerTenantServices(
            IServiceCollection services,
            AppTenant tenant,
            IConfiguration tenantConfiguration,
            ConnectionStringSettingsCollection ConnectionStrings)
        {
            
        }
Avec ces informations, nous pouvons maintenant nous connecter à n'importe quel type de base de données avec ou sans ORM. Exemple avec EntityFramework Core : Ajoutons le package EF Core à notre projet et configurons le DbContext :
services.AddDbContextPool<MyDbContext>(
        options => options
            .UseSqlServer(
                [Ici chaine de connection ]
                x => x.EnableRetryOnFailure())
            .ConfigureWarnings(x => x.Throw(RelationalEventId.QueryClientEvaluationWarning))
            .EnableSensitiveDataLogging(this.hostingEnvironment.IsDevelopment())
            .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking))
Regroupons le tout pour obtenir ceci :
 public void ConfigurePerTenantServices(
            IServiceCollection services,
            AppTenant tenant,
            IConfiguration tenantConfiguration,
            ConnectionStringSettingsCollection connectionStringsCollection)
        {
            services.AddDbContextPool<MyDbContext>(
        options => options
            .UseSqlServer(
                connectionStringsCollection.ConnectionStrings("DefaultConnection"),
                x => x.EnableRetryOnFailure())
            .ConfigureWarnings(x => x.Throw(RelationalEventId.QueryClientEvaluationWarning))
            .EnableSensitiveDataLogging(this.hostingEnvironment.IsDevelopment())
            .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking))
        }
 

Conclusion

Nous avons pu voir que la mise en place de la séparation de données des applications multi-tenantes est facile dès lors que nous avons une base solide d'abstractions de tenants. De plus, cette implémentation est indépendante des ORM. Pour aller plus loin, nous pouvons aussi réfléchir à un système de migrations de structures et de données pour faciliter l'ajout d'un nouveau Tenant ou Client.

Liens utilisés pour mes recherches et pour rédiger cet article

  • https://blog.wescale.fr/2015/08/24/single-tenant-vs-multi-tenant/
  • https://medimagh.wordpress.com/comprendre-l-architecture-multitenant/
  • https://docs.jboss.org/hibernate/orm/4.2/devguide/en-US/html/ch16.html
  • https://www.baeldung.com/hibernate-5-multitenancy
  • https://www.codingame.com/playgrounds/5440/multi-tenant-asp-net-core-2---implementing-database-based-tenant-provider
  • http://benfoster.io/blog/aspnet-core-multi-tenancy-data-isolation-with-entity-framework