2 votes

Comment gérer une classe utilisée dans plusieurs contextes de bases de données pour une base de données en EF code-first ?

Je suis en train de créer des API de bibliothèque de classes de module qui utilisent EF dans ma couche de répertoire. Pour que tout cela fonctionne, j'ai besoin d'une classe dbcontext dans chaque bibliothèque de classes. Mais que se passe-t-il lorsque j'ai besoin qu'une classe soit référencée dans chaque module? Prenons par exemple un module utilisateurs dont les contextes db incluent :

  • Utilisateurs
  • Groupes
  • Rôles

Ensuite, j'ai un module d'emplacements qui inclut :

  • Bâtiments
  • Emplacements
  • Pièces

Ensuite, disons un troisième module pour l'équipement qui contient :

  • Équipements
  • Types d'équipement
  • Ordres de travail

Les deux derniers modules ont toujours besoin de références aux utilisateurs, ce qui est pratiquement essentiel pour chaque module. Mais je ne peux pas ajouter deux classes utilisateur distinctes à deux contextes pointant vers la même db, il est possible qu'elles deviennent désynchronisées. La solution évidente serait donc que les deux derniers modules requièrent le module utilisateur, et toute classe dans ces modules qui a besoin de l'utilisateur fait référence à l'ID utilisateur. Cela casserait cependant la normalisation car ce ne serait pas une clé étrangère, je ne suis donc pas sûr de la pertinence de cette idée.

Une autre possibilité qui m'est venue à l'esprit était que le dbcontext de chaque module utilise une interface, et permette à la personne utilisant le module de déclarer son propre dbcontext et de mettre en œuvre tous ces membres, mais je ne suis pas sûr que cela fonctionnera non plus.

Je veux essentiellement créer une collection de modules de bibliothèque de classes qui définissent un ensemble commun de classes et d'appels API disponibles pour d'autres programmeurs, tout en utilisant EF comme base avec l'intention que tout cela soit stocké dans une seule DB. Mais je ne sais pas trop comment y parvenir avec le fonctionnement des DbContexts. Que se passe-t-il lorsque plusieurs modules nécessitent le même objet?

4voto

ZenLulz Points 1126

Les trois contextes que vous représentez correspondent généralement aux Bounded Contexts tels que conçus dans la conception pilotée par le domaine, comme l'a judicieusement souligné Steeve.

Évidemment, il existe plusieurs façons d'implémenter ce scénario et chacune a ses avantages et ses inconvénients.

Je propose deux approches pour respecter les meilleures pratiques de la conception pilotée par le domaine et avoir une grande flexibilité.

Approche #1 : Séparation souple

Je définis une classe User dans le premier contexte borné et une interface représentant une référence à un utilisateur dans le deuxième contexte borné.

Définissons l'utilisateur :

class User
{
    [Key]
    public Guid Id { get; set; }

    public string Name { get; set; }
}

D'autres modèles qui référencent un utilisateur implémentent IUserRelated :

interface IUserRelated
{
    [ForeignKey(nameof(User))]
    Guid UserId { get; }
}

Le modèle recommande de ne pas lier directement deux entités de deux contextes bornés séparés, mais de stocker leur référence respective à la place.

La classe Building ressemble à ceci :

class Building : IUserRelated
{
    [Key]
    public Guid Id { get; set; }

    public string Location { get; set; }
    public Guid UserId { get; set; }
}

Comme vous pouvez le voir, le modèle Building ne connaît que la référence d'un User. Néanmoins, l'interface agit comme une clé étrangère et contraint la valeur insérée dans cette propriété UserId.

Définissons maintenant les contextes de base...

class BaseContext : DbContext where TContext : DbContext
{
    static BaseContext()
    {
        Database.SetInitializer(null);
    }

    protected BaseContext() : base("Demo")
    {

    }
}

class UserContext : BaseContext
{
    public DbSet Users { get; set; }
}

class BuildingContext : BaseContext
{
    public DbSet Buildings { get; set; }
}

Et le contexte de base pour initialiser la base de données :

class DatabaseContext : DbContext
{
    public DbSet Buildings { get; set; }
    public DbSet Users { get; set; }

    public DatabaseContext() : base("Demo")
    {
    }
}

Et enfin, le code qui crée un utilisateur et un bâtiment :

// Définit quelques constantes
const string userName = "James";
var userGuid = Guid.NewGuid();

// Initialise la base de données
using (var db = new DatabaseContext())
{
    db.Database.Initialize(true);
}

// Créer un utilisateur
using (var userContext = new UserContext())
{
    userContext.Users.Add(new User {Name = userName, Id = userGuid});
    userContext.SaveChanges();
}

// Créer un bâtiment lié à un utilisateur
using (var buildingContext = new BuildingContext())
{
    buildingContext.Buildings.Add(new Building {Id = Guid.NewGuid(), Location = "Switzerland", UserId = userGuid});
    buildingContext.SaveChanges();
}

Approche #2 : Séparation rigide

Je définis une classe User dans chacun des contextes bornés. Une interface impose les propriétés communes. Cette approche est illustrée par Martin Fowler comme suit :

saisissez ici la description de l'image

Contexte borné de l'utilisateur :

public class User : IUser
{
    [Key]
    public Guid Id { get; set; }

    public string Name { get; set; }
}

public class UserContext : BaseContext
{
    public DbSet Users { get; set; }
}

Contexte borné du bâtiment :

public class User : IUser
{
    [Key]
    public Guid Id { get; set; }
}

public class Building
{
    [Key]
    public Guid Id { get; set; }

    public string Location { get; set; }

    public virtual User User { get; set; }
}

public class BuildingContext : BaseContext
{
    public DbSet Buildings { get; set; }

    public DbSet Users { get; set; }
}

Dans ce cas, il est tout à fait acceptable d'avoir une propriété Users dans le BuildingContext, car un utilisateur existe également dans le contexte d'un bâtiment.

Utilisation :

    // Définit quelques constantes
    const string userName = "James";
    var userGuid = Guid.NewGuid();

    // Créer un utilisateur
    using (var userContext = new UserContext())
    {
        userContext.Users.Add(new User { Name = userName, Id = userGuid });
        userContext.SaveChanges();
    }

    // Créer un bâtiment lié à un utilisateur
    using (var buildingContext = new BuildingContext())
    {
        var userReference = buildingContext.Users.First(user => user.Id == userGuid);

        buildingContext.Buildings.Add(new Building { Id = Guid.NewGuid(), Location = "Switzerland", User = userReference });
        buildingContext.SaveChanges();
    }

Travailler avec les migrations EF est vraiment facile. Le script de migration pour le contexte borné de l'utilisateur (généré par EF) :

public partial class Initial : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Users",
            c => new
                {
                    Id = c.Guid(nullable: false),
                    Name = c.String(),
                })
            .PrimaryKey(t => t.Id);

    }

    public override void Down()
    {
        DropTable("dbo.Users");
    }
}

Le script de migration pour le contexte borné du bâtiment (généré par EF). Je dois supprimer la création de la table Users, car l'autre contexte borné a la responsabilité de la créer. Vous pouvez toujours vérifier si la table n'existe pas avant de la créer pour une approche modulaire :

public partial class Initial : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Buildings",
            c => new
                {
                    Id = c.Guid(nullable: false),
                    Location = c.String(),
                    User_Id = c.Guid(),
                })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.Users", t => t.User_Id)
            .Index(t => t.User_Id);
    }

    public override void Down()
    {
        DropForeignKey("dbo.Buildings", "User_Id", "dbo.Users");
        DropIndex("dbo.Buildings", new[] { "User_Id" });
        DropTable("dbo.Users");
        DropTable("dbo.Buildings");
    }
}

Appliquez le Upgrade-Database pour les deux contextes et votre base de données est prête !

MODIFICATION à la demande de l'auteur concernant l'ajout de nouvelles propriétés dans la classe User.

Lorsqu'un contexte borné ajoute une nouvelle propriété à la classe User, il ajoute de manière incrémentielle une nouvelle colonne en interne. Il ne redéfinit pas toute la table. C'est pourquoi cette implémentation est également très polyvalente.

Voici un exemple de script de migration où une nouvelle propriété Accreditation est ajoutée à la classe User dans le contexte borné Building :

public partial class Accreditation : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.Users", "Accreditation", c => c.String());
    }

    public override void Down()
    {
        DropColumn("dbo.Users", "Accreditation");
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X