Простая смена трекер на покос


Проблема

У меня есть список объектов модели поко (примерно 1.000 до 10.000) и я хочу, чтобы отслеживать изменения на них:

  • Проверьте, если хотя бы один из элементов был изменен (чтобы показать пользователю, что что-то изменилось)
  • Получить все новые, удаленные и измененные объекты, чтобы обновить состояние в базе данных

Для первой версии, это достаточно, чтобы поддержать покос с примитивными типами собственность. Впрочем, наверное я продлить его в будущем для поддержки сложных типов недвижимости.

Решение

public class ChangeTracker<TEntity> where TEntity : class
{
    private static readonly PropertyInfo[] thePropertyInfos;

    private readonly List<TEntity> myEntities = new List<TEntity>();
    private readonly List<TEntity> myDeletedEntites = new List<TEntity>();
    private readonly HashSet<TEntity> myNewEntities = new HashSet<TEntity>();
    private readonly Dictionary<TEntity, object[]> myStates = new Dictionary<TEntity, object[]>();

    static ChangeTracker()
    {
        thePropertyInfos = typeof(TEntity).GetProperties().ToArray();
    }

    public void Add(TEntity entity)
    {
        if (myStates.ContainsKey(entity))
            throw new InvalidOperationException("It is not possible to add an entity twice.");

        myStates.Add(entity, new object[thePropertyInfos.Length]);
        myNewEntities.Add(entity);
        myEntities.Add(entity);
    }

    public void Delete(TEntity entity)
    {
        myEntities.Remove(entity);
        if (!myNewEntities.Remove(entity))
            myDeletedEntites.Add(entity);
    }

    public void AcceptChanges()
    {
        myNewEntities.Clear();
        myDeletedEntites.Clear();
        foreach (var entity in myEntities)
            myStates[entity] = GetState(entity);
    }

    public bool HasChanges()
    {
        return GetEntities(EntityStates.Deleted 
            | EntityStates.Modified 
            | EntityStates.New)
            .Any();
    }

    private static object[] GetState(TEntity entity)
    { 
        return thePropertyInfos
            .Select(pi => pi.GetValue(entity))
            .ToArray();
    }

    private static void SetState(TEntity entity, object[] state)
    {
        for (int i = 0; i < state.Length; i++)
            thePropertyInfos[i].SetValue(entity, state[i]);
    }

    private bool HasChanges(TEntity entity)
    {
        var currentState = GetState(entity);
        var previousState = myStates[entity];

        for (int i = 0; i < currentState.Length; i++)
        {
            if (currentState[i] == null &&
                previousState[i] == null)
                continue;

            if (currentState[i] == null ||
                !currentState[i].Equals(previousState[i]))
                return true;
        }

        return false;
    }

    public IEnumerable<TEntity> GetEntities(EntityStates states)
    {
        if (states.HasFlag(EntityStates.New))
            foreach (var entity in myNewEntities)
                yield return entity;

        if (states.HasFlag(EntityStates.Deleted))
            foreach (var entity in myDeletedEntites)
                yield return entity;

        if (!states.HasFlag(EntityStates.Modified) &&
            !states.HasFlag(EntityStates.Unmodified))
            yield break;

        foreach (var entity in myEntities.Where(e => !myNewEntities.Contains(e)))
        {
            var isModified = HasChanges(entity);
            if (states.HasFlag(EntityStates.Modified) && isModified)
                yield return entity;
            else if (states.HasFlag(EntityStates.Unmodified) && !isModified)
                yield return entity;
        }
    }
}

[Flags]
public enum EntityStates
{
    New = 1,
    Deleted = 2,
    Modified = 4,
    Unmodified = 8,
    All = ~0,
}

Использование:

public class TestItem
{
    public string Property { get; set; }
}

var changeTracker = new ChangeTracker<TestItem>();
var item1 = new TestItem();
var item2 = new TestItem();
var item3 = new TestItem();
var item4 = new TestItem();
var item5 = new TestItem();

changeTracker.Add(item1);
changeTracker.Add(item2);
changeTracker.Add(item3);
changeTracker.AcceptChanges();
changeTracker.Add(item4);
changeTracker.Add(item5);
item2.Property = "test";
changeTracker.Delete(item3);
changeTracker.Delete(item5);

Assert.IsTrue(changeTracker.HasChanges());
Assert.AreEqual(4, changeTracker.GetEntities(EntityStates.All).Count());
Assert.AreEqual(item1, changeTracker.GetEntities(EntityStates.Unmodified).Single());
Assert.AreEqual(item2, changeTracker.GetEntities(EntityStates.Modified).Single());
Assert.AreEqual(item3, changeTracker.GetEntities(EntityStates.Deleted).Single());
Assert.AreEqual(item4, changeTracker.GetEntities(EntityStates.New).Single());

Любая обратная связь приветствуется. Однако, я специально заинтересованы в альтернативу более производительным решениям (без накладных расходов на бокс/распаковка и отражения).

[Править]

Я тоже пробовал двоичная сериализация как "государство хранение стратегию", но значительно медленнее (~100-200%) и он имеет недостаток, что классы сущностей должны быть помечены как Serializable.



253
5
задан 26 января 2018 в 08:01 Источник Поделиться
Комментарии
1 ответ

Состояние массивов

Эти путай меня скорее. Я предлагаю вам не добавить "пустой" массив Addи вместо того, чтобы добавить null.

myStates.Add(entity, null);

Пустой массив - это допустимое состояние, и насколько я могу сказать вам, хочу, чтобы это стало рассматриваться как не-заданное значение; null нет, и вызовет жестокие аварии, если попытаться рассматривать ее как таковой (что хорошо).

Этот вид требует изменения HasChanges (хотя я не думаю, что это можно назвать прежде чем на новую сущность до AcceptChanges), чтобы явно проверить, если previousState это null (что может иметь некоторые преимущества, но кого это волнует). Конечно, вы можете использовать некоторые другие явные часового типа (т. е. сделать это правильно), или просто не добавить ничего myStates словарь до такого состояния существует (имеет все преимущества null без всякого страха).

GetEntities

GetEntities это немного больно читать. Я был бы склонен сохранить новые объекты в myNewEntities пока они принимаются (гораздо дешевле, в случае, когда изменения отклонены), просто как удаленные сущности не появляются в MyEntities. Это позволит упростить реализацию GetEntitiesудаление проверки новых лиц.

Вы также не очистив государств хранятся удаленные объекты (должно быть сделано в AcceptChanges).

Смешанная


  • Не большая проблема, но я бы рассмотреть вопрос о внесении thePropertyInfos в IReadOnlyList<TEntity> (хотя я, вероятно, не произвел, поскольку, надеюсь, никто не будет когда-нибудь посмотреть на этот код).

  • Это может сделать с некоторыми рядный (\\\) документы для внешнего API, и некоторые признаки того, что он не является потокобезопасным.

  • Есть ли особая причина, почему myEntities это List и не HashSet? Я бы несколько наклонных каналов в списке и просто сохранить государство словарь (нет избыточности -> меньше усилий на поддержание)

  • Нет RejectChanges? Без этого, некоторые биты могут быть существенно упрощен (например Дитч как только что-то удалить).

2
ответ дан 27 января 2018 в 11:01 Источник Поделиться