web-dev-qa-db-fra.com

Cadre d'entité 6 et unité de travail ... Où, quand? Est-ce comme des transactions sur ado.net?

Créer un nouveau projet MVC et comme l'idée de référentiels dans la couche de données, je les ai donc implémentés. J'ai également créé une couche Service pour gérer toute la logique et la validation métier, cette couche utilise à son tour le référentiel approprié. Quelque chose comme ça (j'utilise un injecteur simple pour injecter)

COUCHE DAL

public class MyRepository {

    private DbContext _context;
    public MyRepository(DbContext context) {
        _context = context;
    }    

    public MyEntity Get(int id)
    {
        return _context.Set<MyEntity>().Find(id);
    }

    public TEntity Add(MyEntity t)
    {
        _context.Set<MyEntity>().Add(t);
        _context.SaveChanges();
        return t;
    }

    public TEntity Update(MyEntity updated, int key)
    {
        if (updated == null)
            return null;

        MyEntity existing = _context.Set<MyEntity>().Find(key);
        if (existing != null)
        {
            _context.Entry(existing).CurrentValues.SetValues(updated);
            _context.SaveChanges();
        }
        return existing;
    }

    public void Delete(MyEntity t)
    {
        _context.Set<MyEntity>().Remove(t);
        _context.SaveChanges();
    }
}

COUCHE DE SERVICE

public class MyService {
    private MyRepository _repository;

    public MyService(MyRepository repository) {
        _repository = repository;    
    }

    public MyEntity Get(int id)
    {
        return _repository.Get(id);
    }

    public MyEntity Add(MyEntity t)
    {
        _repository.Add(t);

        return t;
    }

    public MyEntity Update(MyEntity updated)
    {
        return _repository.Update(updated, updated.Id);
    }

    public void Delete(MyEntity t)
    {
        _repository.Delete(t);
    }
}

Maintenant, c'est très simple, je peux donc utiliser le code suivant pour mettre à jour un objet.

MyEntity entity = MyService.Get(123);
MyEntity.Name = "HELLO WORLD";
entity = MyService.Update(entity);

Ou ceci pour créer un objet

MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);
// entity.Id is now populated

Maintenant, disons que je devais mettre à jour un élément en fonction de l'ID de création d'un autre, je pourrais utiliser le code avant tout, mais que se passe-t-il si une erreur se produit? J'ai besoin d'une sorte de transaction/annulation. Est-ce ce que le modèle d'unité de travail est censé résoudre?

Donc je suppose que j'ai besoin d'avoir DbContext dans mon objet UnitOfWork, donc je crée un objet comme ça?

public class UnitOfWork : IDisposable {

    private DbContext _context;

    public UnitOfWork(DbContext context) {
        _context = context;
    }

    public Commit() {
        _context.SaveChanges();
    }

    public Dispose() {
        _context.Dispose();
    }

}

Ok encore une fois, c'est assez simple. UnitOfWork détient également le contexte (j'utilise quand même le même contexte sur tous les référentiels) et il appelle la méthode SaveChanges (). Je supprimerais alors l'appel de méthode SaveChanges () de mon référentiel. Donc, pour ajouter, je ferais ce qui suit:

UnitOfWork uow = new UnitOfWork(new DbContext()); // i would inject this somehow

MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);

uow.Commit();

Mais que se passe-t-il si j'ai besoin de créer un objet puis de mettre à jour d'autres objets en fonction de cet ID, cela ne fonctionnera plus, car l'ID ne sera pas créé tant que je n'appellerai pas Commit sur le uow. Exemple

UnitOfWork uow = new UnitOfWork(new DbContext()); // i would inject this somehow

MyEntity entity = new MyEntity();
MyEntity.Name = "HELLO WORLD";
entity = MyService.Add(entity);
// entity.Id is NOT populated

MyEntity otherEntity = MyService.Get(123);
otherEntity.OtherProperty = entity.Id;
MyService.Update(otherEntity);

uow.Commit();  // otherEntity.OtherProperty is not linked.....?

J'ai donc le sentiment que cette classe UnitOfWork n'est pas juste ... peut-être me manque-t-il de comprendre quelque chose.

Je dois pouvoir ajouter une entité et obtenir cet identifiant et l'utiliser sur une autre entité, mais si une erreur se produit, je veux "annuler" comme le ferait une transaction ado.net.

Cette fonctionnalité est-elle possible en utilisant Entity Framework and Repositories?

32
Gillardo

Je dois d'abord dire que il n'y a pas une bonne façon unique de résoudre ce problème. Je présente juste ici ce que je ferais probablement.


La première chose est, DbContext lui-même implémente le modèle d'unité de travail. L'appel de SaveChangesdoes crée une transaction de base de données afin que chaque requête exécutée sur la base de données soit annulée en cas de problème.

Maintenant, il y a un problème majeur dans la conception actuelle que vous avez: votre référentiel appelle SaveChanges sur le DbContext. Cela signifie que vous rendez XXXRepository responsable de la validation toutes les modifications que vous avez apportées à l'unité d'oeuvre, pas seulement les modifications sur les entités XXX dont votre référentiel est responsable.

Une autre chose est que DbContext est également un référentiel. Donc, l'abstraction de l'utilisation de DbContext dans un autre référentiel crée simplement une autre abstraction sur une abstraction existante, c'est tout simplement trop de code IMO.

De plus, vous devrez peut-être accéder à des entités XXX à partir du référentiel YYY et à des entités YYY à partir du référentiel XXX, donc pour éviter les dépendances circulaires, vous vous retrouverez avec un MyRepository : IRepository<TEntity> Inutile qui ne fait que dupliquer tous les DbSet méthodes.

Je laisserais tomber toute la couche du référentiel. J'utiliserais le DbContext directement à l'intérieur de la couche de service. Bien sûr, vous pouvez factoriser toutes les requêtes complexes que vous ne souhaitez pas dupliquer dans la couche de service. Quelque chose comme:

public MyService()
{
    ...
    public MyEntity Create(some parameters)
    {
        var entity = new MyEntity(some parameters);
        this.context.MyEntities.Add(entity);

        // Actually commits the whole thing in a transaction
        this.context.SaveChanges();

        return entity;
    }

    ...

    // Example of a complex query you want to use multiple times in MyService
    private IQueryable<MyEntity> GetXXXX_business_name_here(parameters)
    {
        return this.context.MyEntities
            .Where(z => ...)
            .....
            ;
    }
}

Avec ce modèle, chaque appel public sur une classe de service est exécuté à l'intérieur d'une transaction grâce à DbContext.SaveChanges Étant transactionnel.

Maintenant, pour l'exemple que vous avez avec l'ID requis après la première insertion d'entité, une solution consiste à ne pas utiliser l'ID mais l'entité elle-même. Vous laissez donc Entity Framework et sa propre implémentation du modèle d'unité de travail s'en occuper.

Donc au lieu de:

var entity = new MyEntity();
entity = mydbcontext.Add(entity);
// what should I put here?
var otherEntity = mydbcontext.MyEntities.Single(z => z.ID == 123);
otherEntity.OtherPropertyId = entity.Id;

uow.Commit();

tu as:

var entity = new MyEntity();
entity = mydbcontext.Add(entity);

var otherEntity = mydbcontext.MyEntities.Single(z => z.ID == 123);
otherEntity.OtherProperty = entity;     // Assuming you have a navigation property

uow.Commit();

Si vous n'avez pas de propriété de navigation, ou si vous avez un cas d'utilisation plus complexe à traiter, la solution consiste à utiliser la bonne transaction or dans votre méthode de service public:

public MyService()
{
    ...
    public MyEntity Create(some parameters)
    {
        // Encapuslates multiple SaveChanges calls in a single transaction
        // You could use a ITransaction if you don't want to reference System.Transactions directly, but don't think it's really useful
        using (var transaction = new TransactionScope())
        {
            var firstEntity = new MyEntity { some parameters };
            this.context.MyEntities.Add(firstEntity);

            // Pushes to DB, this'll create an ID
            this.context.SaveChanges();

            // Other commands here
            ...

            var newEntity = new MyOtherEntity { xxxxx };
            newEntity.MyProperty = firstEntity.ID;
            this.context.MyOtherEntities.Add(newEntity);

            // Pushes to DB **again**
            this.context.SaveChanges();

            // Commits the whole thing here
            transaction.Commit();

            return firstEntity;
        }
    }
}

Vous pouvez même appeler plusieurs méthodes de services à l'intérieur d'une portée transactionnelle si nécessaire:

public class MyController()
{
    ...

    public ActionResult Foo()
    {
        ...
        using (var transaction = new TransactionScope())
        {
            this.myUserService.CreateUser(...);
            this.myCustomerService.CreateOrder(...);

            transaction.Commit();
        }
    }
}
43
ken2k