Archiwa kategorii: Bazy danych

[EF] Obsługa zmian w DbContext

Mechanizm śledzenia zmian

Dopóki nie ma wyraźniej potrzeby, to nie należy niczego zmieniać. Jakakolwiek modyfikacja, prawdopodobnie wniesie mało (jeśli w ogóle) korzyści, przy czym mamy impelementując samoodzielnie taki mechanizm jest duże prawdobieństwo, że gdzieś popełnimy błąd.

Jednak warto rozumieć mechanizm śledzenia zmian Entity Framework, aby efektywnie pisać kod.

Automatycznie wywoływane DetectChanges

Domyślnie sprawdzanie zmian encji jest dokonywane poprzez DetectChanges, który porównuje aktualny stan poszczególnych encji z zapamiętanym snapshotem.

DetectChanges jest automatycznie wywoływany podczas korzystania z metod:

  • DbSet.Find
  • DbSet.Local
  • DbSet.Remove
  • DbSet.Add
  • DbSet.Attach
  • DbContext.SaveChanges
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries

Walidacja

ValidateEntry jest wykorzystywany do przeprowadzania własnych walidacji podczas GetValidationErrors lub SaveChanges.

Przykład zwiększania efektywności dokonywania zmian w EF

Zamiast:

public void AddPosts(List posts)
{
using (var context = new AnotherBlogContext())
{
posts.ForEach(p => context.Posts.Add(p));
context.SaveChanges();
}
}

Można napisać wg arykułu:

public void AddPosts(List posts)
{
    using (var context = new AnotherBlogContext())
    {
        try
        {
            context.Configuration.AutoDetectChangesEnabled = false;
            posts.ForEach(p => context.Posts.Add(p));
        }
        finally
        {
            context.Configuration.AutoDetectChangesEnabled = true;
        }
        context.SaveChanges();
    }
}

Chociaż osobiście do scenariuszów tego typu stworzyłem klasę ChangesScope implementującą IDisposable:

public class ChangesScope : IDisposable
{
    private DbContext dbContext;

    public ChangesScope(DbContext dbContext)
    {
        this.dbContext = dbContext;
        dbContext.Configuration.AutoDetectChangesEnabled = false;
    }

    public void Dispose()
    {
        dbContext.Configuration.AutoDetectChangesEnabled = true;
        dbContext.ChangeTracker.DetectChanges();
    }
}

Wtedy wspomniany przykład można by napisać w następujący sposób:

public void AddPosts(List posts)
{
    using (var context = new AnotherBlogContext())
    {
        using (new ChangesScope(context))
        {
            posts.ForEach(p => context.Posts.Add(p));
        }
        context.SaveChanges();
    }
}

Niestety w tym wypadku DetectChanges() będzie tak naprawdę wywołane 2 razy, ale wydaje mi się, że kod jest bardziej czytelny (bliżej mu do jednego poziomu abstrakcji).

Bardzo dobrze umówione aspekty zwiększania efektywności zmian encji są opisane w http://blog.oneunicorn.com/2012/03/12/secrets-of-detectchanges-part-3-switching-off-automatic-detectchanges/

Zasady

Zasady zachowania stanu:

Skąd wiadomo, czy można wyłączyć automatyczne DectectChanges na dłuższy okres. Ważna jest kontrola dwóch zasad:

1. Żadne wywołanie kodu EF nie zostawi kontekstu w stanie, w którym DetectChanges będzie potrzebne do wywołania, jeśli wcześniej wywołanie nie było wymagane

2. Za każdym razem kiedy nieEF kod zmieni jakąś właściwość encji albo złożony obiekt – wtedy DetectChanges musi być wywołany

Zasady zarządzania stanem:

1. Nie wyłączaj automatycznego DetectChanges jeśli naprawdę tego nie potrzebujesz

2. Jeśli musisz wyłączyć DetechChanges, to najlepiej rób to lokalnie w blokach try/finalny

3. Stosuj DbContext property API, aby zmieniać encje bez konieczności wywoływania DetectChanges

Perełki:

1. Traktuj właściwości byte[] jako pola immutable. Zawsze jak chcesz zmienić taką właściwość, to przypisuj jej nową instancję.

Cofanie zmian

W obecnej wersji EntityFramework metodę cofającą zmiany można zaimplementować w następujący sposób:

public virtual void RejectChanges()
{
    foreach (var entry in ChangeTracker.Entries())
    {
        if (entry.State == EntityState.Modified)
        {
            entry.State = EntityState.Unchanged;
        }
        else if (entry.State == EntityState.Added)
        {
            entry.State = EntityState.Detached;
        }
        else if (entry.State == EntityState.Deleted)
        {
            entry.Reload();
        }
    }

    SaveChanges();
}

Niestety metoda Reload używana dla encji, które zostały usunięte polega na tym, że zostaje odświeżony stan encji na podstawie bazy danych (czyli łączymy się z bazą danych).

Chociaż tak naprawdę lepiej unikać korzystania z takiej metody i po prostu tworzyć nową instancję DbContext i wszystko odświeżać na jej podstawie.

Zarządzanie życiem DbContext

Nie powinno się stosować DbContextu w taki sposób, że operujemy na jednej instancji przez cały czas. Wszystkie operacje powinny być wykonywane na nowych instancjach DbContext. Operacje takie jak odświeżanie bądź cofanie, powinny być wykonywane jako stworzenie nowej instancji DbContext.

Ważna literatura: http://stackoverflow.com/questions/3653009/entity-framework-and-connection-pooling/3653392#3653392

Odnośniki

[EF] Sekwencje w DbContext

Wstęp

DbContext (FluentAPI) nie wspiera mapowania właściwości do sekwencji bazodanowych. Z tego powodu, aby mieć możliwość z korzystania ze sekwencji należy zastosować jedno z możliwych obejść.

Przykładowe rozwiązania

public partial class CustomerEntities : DbContext
{
    public override int SaveChanges()
    {
        Database.Connection.Open();
        CustomerIdSequence();
        return base.SaveChanges(); ;
    }
        
    /// <summary>
    /// Wykonanie akcji podczas dodawania encji określonego typu.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="action"></param>
    protected void OnEntityAdd<T>(Action<T> action) 
        where T : class
    {
        var addedEntities = ChangeTracker.Entries<T>().Where(e => e.State == System.Data.EntityState.Added);
        foreach (var entity in addedEntities)
        {
            action(entity.Entity);
        }
    }

    /// <summary>
    /// Uzupełnia Id nowych encji.
    /// </summary>
    private void CustomerIdSequence()
    {
        OnEntityAdd<Customer>(customer => customer.Id = GetSequence("SYSADM.SEQ_CUSTOMER_PK"));
    }

    /// <summary>
    /// Zwraca kolejną wartość danej sekwencji.
    /// </summary>
    /// <param name="dbConnection"></param>
    /// <returns></returns>
    private string GetSequence(string sequenceName)
    {
        using (var command = Database.Connection.CreateCommand())
        {
            command.CommandText = string.Format("SELECT {0}.NEXTVAL FROM DUAL", sequenceName);
            return command.ExecuteScalar().ToString();
        }
    }
}

Uwaga

Na marginesie dodam, że sekwencje nie podlegają mechanizmowi transakcji, dlatego nie warto próbować, aby pobieranie wartości sekwencji było we wspólnej transakcji wraz z innymi operacjami.

LINQ to SQL – problemy

Nie działający designer dla SQL Compact

Jednym z problemów LINQ to SQL jest brak designera do generowania DataContext dla bazy SQL Compact.
Przedstawię sposób w jaki osobiście poradziłem sobie z tym problemem:

SqlMetal /dbml:DBDataContext.dbml /namespace:Pellared.Data /pluralize /password:CompactDB DB.sdf

Wygenerowany plik dbml dołączam do projektu (Add Existing Item)

Kilka relacji pomiędzy tabelami

SqlMetal nie poradził sobie z dwoma kluczami obcymi do jednej tabeli. Sposób na naprawienie błędu:
W relacji, gdzie generuje błąd w właściowości Memeber niepotrzebnie dodaje prefiks „Element_”.
Przykładowy błędny wiersz:

<Association Name="Element_SubElement2" Member="Element_SubElement2" ThisKey="ID" OtherKey="SubElementID" Type="SubElement" DeleteRule="NO ACTION" />

Poprawiony:

<Association Name="Element_SubElement2" Member="SubElement2" ThisKey="ID" OtherKey="SubElementID" Type="SubElement" DeleteRule="NO ACTION" />

Exception: Row not found or changed

Jeszcze spotkałem się z bardzo nieprzyjemnym problemem. Dokładny opis: http://stackoverflow.com/questions/805968/system-data-linq-changeconflictexception-row-not-found-or-changed . Podejrzewam, że może on wynikać z własnej logiki tworzenia klucza głównego…

Wartości domyślne dla kolumn NOT NULL

LINQ to SQL nie obsługuje wartości domyślnych dla kolumn NOT NULL (przynajmniej w .NET 3.5).
Żeby osiągnąć oczekiwany efekt należy przenieść wartości domyślne do logiki np. korzystając z metod częsciowych. Przykład:

public partial class Element
{
    private const long OrderIndexDefault = -1;
    private const long ValueDefault = -1;

    /// <summary>
    /// Ustawia wartości domyślne, których LINQ to SQL nie obsługuje.
    /// </summary>
    partial void OnCreated()
    {
        OrderIndex = OrderIndexDefault;
        Value = ValueDefault;
    }
}