Правильный способ обновления записей в шаблоне MVVM для максимальной эффективности - PullRequest
5 голосов
/ 14 марта 2012

Это скорее концептуальный вопрос. Вот мое текущее затруднительное положение; Я пишу WPF-приложение vb.net и использую шаблон MVVM (очень нравится! Сопровождаемость просто удивительна). В настоящее время весь код написан от руки, и NHibernate или Entity Framework не используются, поскольку бэкэнд является базой данных доступа (из-за политики я не могу использовать NH, а EF не поддерживает базы данных JET, в какой-то момент мы можем перейти на MSSQL но это может быть через некоторое время).

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

В настоящее время метод заключается в добавлении логического значения к записи в заданной части модели на «грязный», затем при нажатии на обновление мы перебираем все «грязные» записи и используем команду oledb (выполнить с параметрами) sql операторы для обновления.

Это создает отличное разделение интересов, но если это неправильный способ, я хотел бы узнать альтернативы (обратите внимание на тип базы данных и связанные с этим недостатки, такие как то, что она не работает с EF).

Спасибо!

Финальный код в VB.NET после комментариев и т. Д .:

Public Class Car
Implements ICloneable

Public Property Make() As String
    Get
        Return m_Make
    End Get
    Set(ByVal value As String)
        m_Make = value
    End Set
End Property
Private m_Make As String

Public Property Model() As String
    Get
        Return m_Model
    End Get
    Set(ByVal value As String)
        m_Model = value
    End Set
End Property
Private m_Model As String

Public Function Clone() As Object Implements System.ICloneable.Clone
    Return New Car() With { _
     .Make = Me.Make, _
     .Model = Me.Model _
    }
End Function
End Class



Public Class CarEqualityComparer
Implements IEqualityComparer(Of Car)

Public Overloads Function Equals(ByVal x As Car, ByVal y As Car) As Boolean Implements System.Collections.Generic.IEqualityComparer(Of Car).Equals
    Return x.Make = y.Make AndAlso x.Model = y.Model
End Function

Public Overloads Function GetHashCode(ByVal obj As Car) As Integer Implements System.Collections.Generic.IEqualityComparer(Of Car).GetHashCode
    Return 1 'http://blogs.msdn.com/b/jaredpar/archive/2008/06/03/making-equality-easier.aspx
End Function

End Class

Public Class CarRepository
    Private _carComparator As New CarEqualityComparer

    Private _cars As New ChangeTracker(Of Car)(_carComparator)

    Public Function GetCars() As IEnumerable(Of Car)
        'TODO: JET/ADO code here, you would obviously do in a for/while loop
        Dim dbId1 As Integer = 1
        Dim make1 As String = "Ford"
        Dim model1 As String = "Focus"

        Dim dbId2 As Integer = 2
        Dim make2 As String = "Hyundai"
        Dim model2 As String = "Elantra"

        'TODO: create or update car objects
        Dim car1 As Car
        If Not _cars.IsTracking(dbId1) Then
            car1 = New Car()
        Else
            car1 = _cars.GetItem(dbId1)
        End If

        car1.Make = make1
        car1.Model = model1

        If Not _cars.IsTracking(dbId1) Then
            _cars.StartTracking(dbId1, car1)
        End If


        Dim car2 As Car
        If Not _cars.IsTracking(dbId2) Then
            car2 = New Car()
        Else
            car2 = _cars.GetItem(dbId2)
        End If

        car2.Make = make2
        car2.Model = model2

        If Not _cars.IsTracking(dbId2) Then
            _cars.StartTracking(dbId2, car2)
        End If

        Return _cars.GetTrackedItems()
    End Function

    Public Sub SaveCars(ByVal cars As IEnumerable(Of Car))

        'TODO: JET/ADO code here to update the item
        Console.WriteLine("Distinct " & cars.Distinct.Count.ToString)

        For Each changedItem As Car In _cars.GetChangedItems().Intersect(cars)
            Console.Write("Saving: ")
            Console.WriteLine(changedItem.Make)
        Next

        For Each newItem As Car In cars.Except(_cars.GetTrackedItems())
            Console.Write("Adding: ")
            Console.WriteLine(newItem.Make)
            Dim newId As Integer = CInt(Math.Ceiling(Rnd() * 5000)) 'Random right now but JET/ADO to get the id later....
            _cars.StartTracking(newId, newItem)
        Next

        Dim removalArray As New ArrayList
        For Each deletedItem As Car In _cars.GetTrackedItems().Except(cars)
            Console.Write("Removing: ")
            Console.WriteLine(deletedItem.Make)
            removalArray.Add(_cars.GetId(deletedItem)) 'Cannot remove right as iterating through array - clearly that would be problematic....
        Next
        For Each dbId As Integer In removalArray
            _cars.StopTracking(dbId)
        Next

        _cars.SetNewCheckpoint()

    End Sub
End Class

Public Class ChangeTracker(Of T As {ICloneable})
    'item "checkpoints" that are internal to this list
    Private _originals As New Dictionary(Of Integer, T)()
    Private _originalIndex As New Dictionary(Of T, Integer)()

    'the current, live-edited objects
    Private _copies As New Dictionary(Of Integer, T)()
    Private _copyIndex As New Dictionary(Of T, Integer)()

    Private _comparator As System.Collections.Generic.IEqualityComparer(Of T)

    Public Sub New(ByVal comparator As System.Collections.Generic.IEqualityComparer(Of T))
        _comparator = comparator
    End Sub

    Public Function IsChanged(ByVal copy As T) As Boolean
        Dim original = _originals(_copyIndex(copy))

        Return Not _comparator.Equals(copy, original)

    End Function

    Public Function GetChangedItems() As IEnumerable(Of T)
        Dim items As IEnumerable(Of T)
        items = _copies.Values.Where(Function(c) IsChanged(c))
        Return items
    End Function

    Public Function GetTrackedItems() As IEnumerable(Of T)
        Return _copies.Values
    End Function

    Public Sub SetNewCheckpoint()
        For Each copy In Me.GetChangedItems().ToList()
            Dim dbId As Integer = _copyIndex(copy)
            Dim oldOriginal = _originals(dbId)
            Dim newOriginal = DirectCast(copy.Clone(), T)

            _originals(dbId) = newOriginal
            _originalIndex.Remove(oldOriginal)
            _originalIndex.Add(newOriginal, dbId)
        Next
    End Sub

    Public Sub StartTracking(ByVal dbId As Integer, ByVal item As T)
        Dim newOriginal = DirectCast(item.Clone(), T)
        _originals(dbId) = newOriginal
        _originalIndex(newOriginal) = dbId

        _copies(dbId) = item
        _copyIndex(item) = dbId
    End Sub

    Public Sub StopTracking(ByVal dbId As Integer)
        Dim original = _originals(dbId)
        Dim copy = _copies(dbId)

        _copies.Remove(dbId)
        _originals.Remove(dbId)
        _copyIndex.Remove(copy)
        _originalIndex.Remove(original)
    End Sub

    Public Function IsTracking(ByVal dbId As Integer) As Boolean
        Return _originals.ContainsKey(dbId)
    End Function

    Public Function IsTracking(ByVal item As T) As Boolean
        Return _copyIndex.ContainsKey(item)
    End Function

    Public Function GetItem(ByVal dbId As Integer) As T
        Return _copies(dbId)
    End Function

    Public Function GetId(ByVal item As T) As Integer
        Dim dbId As Integer = (_copyIndex(item))
        Return dbId
    End Function

End Class

1 Ответ

3 голосов
/ 14 марта 2012

Поскольку вы используете кнопку «Обновить / Сохранить» для фиксации изменений в базе данных, я бы рекомендовал использовать шаблон, похожий на репозиторий, где репозиторий отслеживает изменения всякий раз, когда он выполняет операции сохранения.

Это похоже на то, как Entity Framework реализует объекты самоконтроля (STE). В EF STE для каждой сущности, которую вы хотите отслеживать, создается объект отслеживания, который прослушивает события, подобные PropertyChanged, чтобы определить, является ли объект «грязным».

Основным преимуществом этого подхода является то, что вы можете выполнять пакетное обновление / удаление без необходимости сохранять какие-либо постоянные состояния с вашими моделями или моделями представления или без необходимости всегда сохранять все, что у вас есть, в БД. Это обеспечивает еще большее разделение интересов (DAL против M против VM против V). Я считаю, что MVVM и шаблон репозитория отлично сочетаются друг с другом.

Вот общий подход:

  1. Вы загружаете элементы из базы данных из репозитория. Когда вы загружаете элементы, вы сохраняете их в объекте «трекер», который сохраняет копию объекта, как она была изначально сохранена в базе данных, а также связь с «живым» (редактируемым) объектом. Мы называем этот процесс «созданием контрольной точки».
  2. Вы используете редактируемые объекты в MVVM как обычно, позволяя пользователю вносить любые изменения, которые он хочет. Вам не нужно отслеживать какие-либо изменения.
  3. Когда пользователь нажимает кнопку «Сохранить», вы отправляете все объекты на экране обратно в хранилище для сохранения.
  4. Репозиторий проверяет каждый объект на соответствие оригинальным копиям и определяет, какие элементы являются «грязными».
  5. Только грязные элементы сохраняются в базе данных.
  6. После успешного сохранения вы создаете новую контрольную точку.

Вот пример кода, который я написал:

Во-первых, вот пример класса Car, который мы будем использовать в нашем репозитории. Обратите внимание, что на объекте нет свойства Dirty.

public class Car : IEquatable<Car>, ICloneable
{
    public string Make { get; set; }
    public string Model { get; set; }

    public bool Equals(Car other)
    {
        return other.Make == this.Make &&
               other.Model == this.Model;
    }

    public object Clone()
    {
        return new Car { Make = this.Make, Model = this.Model };
    }
}

Далее, вот CarRepository, который вы будете использовать для инициализации объектов из вашей базы данных:

public class CarRepository
{
    private ChangeTracker<Car> _cars = new ChangeTracker<Car>();

    public IEnumerable<Car> GetCars()
    {
        //TODO: JET/ADO code here, you would obviously do in a for/while loop
        int dbId1 = 1;
        string make1 = "Ford";
        string model1 = "Focus";

        //TODO: create or update car objects
        Car car1;
        if (!_cars.IsTracking(dbId1))
            car1 = new Car();
        else
            car1 = _cars.GetItem(dbId1);

        car1.Make = make1;
        car1.Model = model1;

        if (!_cars.IsTracking(dbId1))
            _cars.StartTracking(dbId1, car1);

        return _cars.GetTrackedItems();
    }

    public void SaveCars(IEnumerable<Car> cars)
    {
        foreach (var changedItem in _cars.GetChangedItems().Intersect(cars))
        {
            //TODO: JET/ADO code here to update the item
        }

        foreach (var newItem in cars.Except(_cars.GetTrackedItems()))
        {
            //TODO: JET/ADO code here to add the item to the DB and get its new ID
            int newId = 5;
            _cars.StartTracking(newId, newItem);
        }            

        _cars.SetNewCheckpoint();
    }
}

Наконец, существует вспомогательный класс, который репозиторий использует для отслеживания изменений и установки контрольных точек, называемый ChangeTracker.

public class ChangeTracker<T> where T : IEquatable<T>, ICloneable
{
    //item "checkpoints" that are internal to this list
    private Dictionary<int, T> _originals = new Dictionary<int, T>();
    private Dictionary<T, int> _originalIndex = new Dictionary<T, int>();

    //the current, live-edited objects
    private Dictionary<int, T> _copies = new Dictionary<int, T>();
    private Dictionary<T, int> _copyIndex = new Dictionary<T, int>();

    public bool IsChanged(T copy)
    {
        var original = _originals[_copyIndex[copy]];
        return original.Equals(copy);
    }

    public IEnumerable<T> GetChangedItems()
    {
        return _copies.Values.Where(c => IsChanged(c));
    }

    public IEnumerable<T> GetTrackedItems()
    {
        return _copies.Values;
    }

    public void SetNewCheckpoint()
    {
        foreach (var copy in this.GetChangedItems().ToList())
        {
            int dbId = _copyIndex[copy];
            var oldOriginal = _originals[dbId];
            var newOriginal = (T)copy.Clone();

            _originals[dbId] = newOriginal;
            _originalIndex.Remove(oldOriginal);
            _originalIndex.Add(newOriginal, dbId);
        }
    }

    public void StartTracking(int dbId, T item)
    {
        var newOriginal = (T)item.Clone();
        _originals[dbId] = newOriginal;
        _originalIndex[newOriginal] = dbId;

        _copies[dbId] = item;
        _copyIndex[item] = dbId;
    }

    public void StopTracking(int dbId)
    {
        var original = _originals[dbId];
        var copy = _copies[dbId];

        _copies.Remove(dbId);
        _originals.Remove(dbId);
        _copyIndex.Remove(copy);
        _originalIndex.Remove(original);
    }

    public bool IsTracking(int dbId)
    {
        return _originals.ContainsKey(dbId);
    }

    public bool IsTracking(T item)
    {
        return _copyIndex.ContainsKey(item);
    }

    public T GetItem(int dbId)
    {
        return _liveCopies[dbId];
    }
}

И вот как вы можете использовать свой репозиторий в программе:

static void Main(string[] args)
{
    var repository = new CarRepository();

    var cars = repository.GetCars().ToArray();

    //make some arbitrary changes...
    cars[0].Make = "Chevy";
    cars[1].Model = "Van";

    //when we call SaveCars, the repository will detect that
    //both of these cars have changed, and write them to the database
    repository.SaveCars(cars);
}

Эта наивная реализация опирается на IEquatable и ICloneable, хотя они определенно не нужны и, вероятно, существуют более эффективные способы выполнения действий, или у вас может быть более эффективный способ определения того, изменился ли элемент. (Например, идея создания копий объектов не совсем благоприятна для памяти). Вам также придется иметь дело с удаленными элементами, но это было бы легко добавить к примеру выше.

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