Мой Kbyte.Ru
Рассылка Kbyte.Ru
Группы на Kbyte.Ru
Партнеры Kbyte.Ru
Реклама
Сделано руками
Сделано руками
> Статьи - Алексей Немиро -

Visual Basic .NET - Базы данных

Все статьи / Базы данных

Ведение журнала изменений объектов в .NET Framework

Автор: Алексей Немиро | добавлено: 23.05.2010, 15:34 | просмотров: 5674 (0+) | комментариев: 4 | рейтинг: *x2
  В процессе разработки какой-либо системы, может потребоваться вести журнал изменений пользовательских объектов (классов). Пользовательский объект может представлять собой, например, учетную запись пользователя, электронный заказ, либо иную запись или комплекс записей в базе данных или файле. В зависимости от реализации программного кода, эта задача может превратиться в рутину. В данной статье будет рассмотрено создание универсального класса для работы с СУБД MS SQL Server с интегрированной возможностью ведения журнала изменений данных, с использованием языка программирования Visual Basic .NET.

 

Задача


В базе данных MS SQL Server есть таблицы users и computers. Таблица users предназначена для хранения данных о пользователях, и содержит следующие поля:
id – уникальный идентификатор пользователя (ключевое поле, типа Int32);
first_name – имя пользователя;
last_name – фамилия пользователя;
sex – пол пользователя;
age – возраст пользователя.

Таблица computers предназначена для хранения некой информации о компьютерах пользователей и содержит следующие поля:
id – уникальный идентификатор записи (ключевое поле, типа Guid);
user_id – идентификатор пользователя;
cpu – тип процессора;
os – тип операционной системы.

Необходимо отслеживать изменения полей таблиц users и computers, и записывать их в таблицу log. В соответствии с этим, таблица log, как минимум, должна содержать следующие поля:
id – уникальный идентификатор записи журнала (ключевое поле);
object – тип объекта;
object_id – идентификатор объекта (типа String);
field – имя свойства объекта;
value – измененное значение;
date – дата события.
 

Проблемы


Все таблицы у нас, за исключением таблицы log, будут иметь объектное представление. Другими словами, для каждой таблицы будет создан отдельный класс, воплощающий в себе таблицу, а поля таблицы будут свойствами этого класса. К чему такие сложности? Если проект большой и хорошо продуман, то подобной подход поможет упростить процесс разработки и решить поставленную задачу с минимальными временными затратами. В тоже время, если работа ведется в стиле импровизации, то использование этого метода может усложнить достижение конечной цели, особенно, если создание таких объектов производится вручную. Хотя «усложнить» – это относительно, описанный в данной статье подход позволит полностью отделить объектное представление данных от, непосредственно, взаимодействия с базой данных. Так что, если автоматизировать процесс создания классов для таблиц, то проблем вообще никаких не будет.

Каждый класс должен иметь методы для загрузки, сохранения и удаления строки данных из базы данных прообразом которой он является. Поскольку у нас две таблицы (users и computers), а в перспективе их может быть десятки, а то и сотни, то правильней будет сделать один универсальный класс. Этот класс должен содержать необходимые методы взаимодействия с базой данных, и от него, в свою очередь, будут наследоваться классы таблиц. Класс должен быть полностью самодостаточным, т.е. чтобы не пришлось постоянно писать отдельный код для методов работы с БД у каждого наследника (класса, который наследуется от основного). Для реализации этого, придется определять, какие свойства есть у текущего класса и по ним формировать необходимые SQL-запросы. Благо механизм .NET Reflection позволяет это сделать без особых проблем. Помимо этого, при сохранении класса в базу данных, необходимо отслеживать сделанные изменения. Это можно осуществить непосредственно перед сохранением данных, поскольку на этот момент информация в БД находится в том виде, в котором была после загрузки в объект. Однако для этого потребуется делать лишний запрос к базе данных, а это не очень хорошо и может негативно отразиться на производительности. Поэтому правильней будет при загрузке объекта сохранять в память оригинальные данные. Хранить данные проще всего в коллекции вида ключ-значение (Dictionary).

 Также следует учесть, что может потребоваться отслеживать изменения только определенных полей, а не всех подряд. Более того, нужно принять во внимание, что некоторые свойства классов таблиц могут вообще не иметь никакого отношения к базе данных. Чтобы помечать, какие свойства классов нужны для БД, а какие – нет, мы будем использовать атрибуты. Для большей гибкости, атрибуты также будут устанавливаться и на классы таблиц.
 

Реализация


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

Для работы нам потребуется база данных, и чтобы не подымать этот вопрос в дальнейшем, необходимо прописать строку соединения с БД. В консольном приложении, для хранения строки соединения, в основном модуле программы можно создать глобальную переменную, назовем её _ConnectionString (листинг 1).

 

Листинг 1. Строка соединения с базой данных.
Листинг 1. Строка соединения с базой данных.


Примечание. Строку соединения можно прописать и в настройках программы и получать через My.Settings. В web-приложениях для этого лучше использовать секцию connectionStrings файла конфигурации – web.config.

Теперь сделаем класс для записи журнала изменений в таблицу log. Класс будет довольно простым, назовем его, по подобию имени таблицы, Log. Фактически, класс должен заносить в таблицу log изменение каждого свойства. Т.е. предполагается, что будут производиться запросы к БД равные количеству измененных свойств объекта. Однако это может негативно отразиться на производительности. Поэтому лучше формировать в памяти запрос на добавление данных в журнал и затем выполнять его по требованию, используя всего одно обращение к базе данных (листинг 2).

 

Листинг 2. Класс Log.
Листинг 2. Класс Log.
(кликните по картинке, чтобы открыть её в полном размере)



В классе Log у нас описан конструктор (строки 12-15), который принимает имя логируемого объекта (objectType) и его идентификатор (objectId). Идентификатор объекта имеет строкой тип, это связано с тем, что у нас в таблице computers используется идентификатор типа Guid, который в большей степени является строкой, нежели числом, а в таблице users применяется идентификатор числового типа.

Метод Add (строки 17-27) служит для добавления задания на запись изменений объекта в журнал событий. Метод принимает имя измененного поля (field) и его старое значение (value). Мы будем записывать только старые значения, поскольку новые значения записываются непосредственно в отслеживаемую таблицу. При каждом вызове метода Add в SqlCommand добавляется новый запрос на запись данных в таблицу log (строки 18-20). Значения полей помещаются в коллекцию параметров (Parameters) SqlCommand (строки 21-24). Чтобы как-то различать параметры, у нас есть суммирующий счетчик – _Counter, значение которого после каждого добавленного задания увеличивается на единицу (строка 25).

Метод Flush (строки 29-37) выполняет сформированный запрос и заносит данные в таблицу log.

С классом Log мы разобрались, теперь создадим классы для таблиц users и computers (листинг 3). Каждое поле таблиц должно иметь аналогичное свойство в соответствующем таблице классе.

 

Листинг 3. Классы Users и Computers.
Листинг 3. Классы Users и Computers.
(кликните по картинке, чтобы открыть её в полном размере)


Далее необходимо сделать основной класс взаимодействия с базой данных. Назовем его DataObject. Этот класс должен содержать методы Load, Save и Delete для загрузки, сохранения и удаления данных (листинг 4). От него в последующем будут наследоваться классы таблиц – users и computers.

 

Листинг 4. Класс DataObject.
Листинг 4. Класс DataObject.


В идеале, в методах Load, Save и Delete нужно написать универсальный код, который будет выполнять некие действия в базе данных. Но мы ведь не знаем, какой класс будет наследоваться от DataObject и какие у него буду свойства? На самом деле знаем, и мы можем запросто получить эти свойства и их значения при помощи механизма .NET Reflection. Но здесь возникает еще одна проблема, ведь любой класс таблицы может содержать дополнительные свойства, никак не связанные с самой таблицей и базой данных в целом, как их определять? Вот тут к нам в помощь приходят атрибуты. Атрибуты – это обычные классы наследованные от класса System.Attribute, содержащие определенные параметры. Нам требуется сделать два новых атрибута, один – для классов таблиц, второй – для свойств.

Атрибут для классов таблиц должен содержать имя таблицы, это необходимо для формирования SQL-запросов, т.е. в принципе класс таблицы может иметь название отличное от имени таблицы, которую он представляет. Для этого атрибут, назовем его DataTableAttribute, должен иметь соответствующее свойство - TableName, которое будет устанавливаться в конструкторе (листинг 5).

 

Листинг 5. Класс DataTableAttribute.
Листинг 5. Класс DataTableAttribute.


Аналогично сделаем атрибут для свойств классов таблиц, назовем его DataColumnAttribute, который будет содержать имя поля в таблице - свойство ColumnName. Но помимо этого, нужно добавить три дополнительных параметра: SqlDataType – будет содержать информацию о SQL-типе данных поля; PrimaryKey – будет указывать на то, что свойство является ключевым, чтобы иметь возможность по этому полю работать с данными в базе данных; Log – будет указывать на то, что следует следить за изменением значения конкретного свойства (листинг 6).

 

Листинг 6. Класс DataColumnAttribute.
Листинг 6. Класс DataColumnAttribute.
(кликните по картинке, чтобы открыть её в полном размере)


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

Теперь можно приступит к написанию универсальных методов Load, Save и Delete в классе DataObject. Начнем с метода Load, который будет загружать информацию из базы данных в объект.

При помощи функции GetType() мы можем получить доступ к текущему объекту. Т.е. если текущий объект Users, который наследован от DataObject, мы из класса DataObject можем получить любые свойства, методы и функции класса Users. Первым делом, нам нужно определить имя таблицы и ключевое поле таблицы. Имя таблицы находится в атрибуте DataTableAttribute, именно этот атрибут нам и нужно найти в текущем объекте. Поскольку атрибуты у нас «самодельные», то искать их нужно через функцию GetCustomAttributes, которая, в случае успеха, возвращает массив найденных атрибутов. У нас предполагается наличие не более одного атрибута, т.е. первый найденный атрибут – это то, что нам нужно. То же самое необходимо проделать со свойствами. Получить коллекцию свойств текущего объекта можно через функцию GetProperties. Нас интересуют только свойства с атрибутом DataColumnAttribute, иные свойства, у которых данного атрибута нет, считаются никак несвязанными с базой данных. Для начала нужно найти ключевое поле, которое имеет параметр PrimaryKey = True. Лучше всего, и правильней, все это реализовать в конструкторе класса DataObject, и занести данные в память, чтобы не выполнять по несколько раз одни и те же операции, поскольку имя таблицы и ключевое поле нам будут необходимы в каждом случае работы с данными (листинг 7).

 

Листинг 7. Конструктор класса DataObject.
Листинг 7. Конструктор класса DataObject.


Также нам довольно часто придется производить поиск атрибута DataColumnAttribute в свойствах класса, поэтому целесообразно сделать для этого отдельную функцию (листинг 8).

 

Листинг 8. Функция GetColumnAttribute.
Листинг 8. Функция GetColumnAttribute.


Для хранения оригинальных данных, загруженных из базы, будет использоваться переменная _OriginalValues (листинг 7, строка 3).

Зная имя таблицы (_TableName), имя и значение ключевого поля (_PrimaryProperty), можно сформировать запрос на выборку строки данных из БД и заполнить ими текущий объект (листинг 9).

 

Листинг 9. Реализация метода Load класса DataObject.
Листинг 9. Реализация метода Load класса DataObject.


В строках 2-3 в блоке Using создается и открывается новое соединение с базой данных SQL Server. В строке 6, в переменную pk, при помощи функции GetColumnAttribute (листинг 8), переносится атрибут ключевого свойства класса, который содержит имя ключевого поля в текущей таблице (_TableName). Значение этого поля берется непосредственно из копии ключевого свойства _PrimaryProperty (которое было получено в листинге 7, строка 14), при помощи метода GetValue (строка 13). Затем, в строках 9-13, формируется SQL-запрос на выборку данных. В строке 16 открывается DataRead. Далее циклом (строки 19-27) данные переносятся в свойства текущего объекта (строка 23), при условии, что свойство имеет атрибут DataColumnAttribute (строки 20-21). Также копия данных заносится в коллекцию _OriginalValues (срока 25), чтобы в последующем иметь возможность отслеживать их изменения.

Теперь, аналогично можно реализовать метод Save (листинг 10), который будет производить запись объекта в базу данных, а также делать запись в журнал изменений данных.

 

Листинг 10. Реализация метода Save класса DataObject.
Листинг 10. Реализация метода Save класса DataObject.
(кликните по картинке, чтобы открыть её в полном размере)


Здесь следует учесть, что данных может не быть в БД, тогда нужно будет их добавить. Т.е. метод Save должен уметь, как сохранять данные, так и создавать новые при их отсутствии. При этом, если вставляются новые данные, то запись в журнал делать ненужно. Определить, что делать с данными можно по значению ключевого поля. Решение на этот счет нужно принимать в соответствии с типом данных ключевого поля (строки 11-22). Тип данных ключевого поля таблицы может быть абсолютно любым. Однако чаще всего используется числовой тип, а также uniqueidentifier (Guid). Если тип данных числовой, и свойство, соответствующее ключевому полю таблицы, имеет значение равное нулю (строка 19), значит нужно создать (INSERT) новые данные в базе данных, в противном случае - сохранить (UPDATE) данные. В случае с Guid, если значение Nothing или Guid.Empty (строка 15), то данные нужно создать, в противном случае – сохранить. Другие типы данных ключевых полей в рамках этой статьи не рассматриваются.

Поскольку запросы INSERT и UPDATE имеют разную структуру, для их формирования используются дополнительные переменные (строка 26).

Для полей, у которых атрибут DataColumnAttribute имеет свойство Log равное True требуется фиксировать изменения и заносить их в журнал (таблица log), для этого у нас используется класс Log, экземпляр которого создается в 30 строке. Проверка значений свойств производится при формировании SQL-запроса (строка 14). Для большей гибкость, проверять изменения лучше в отдельной функции - DataIsChange (листинг 11). Запись изменений в журнал осуществляется методом Flush после проверки значений всех свойств (строка 96).

Если в таблице создаются новые данные, в зависимости от типа данных ключевого поля, идентификатор данных передается в значение свойства, соответствующего ключевому полю (строки 78-86). Это позволит продолжить работу с созданной строкой данных из текущего экземпляра объекта. Следует отметить, что если ключевое поле имеет тип Guid, то значение для него формируется непосредственно в нашем приложении, а не на стороне SQL Server (строка 65), как это происходит с числовыми идентификаторами.

 

Листинг 11. Функция DataIsChanged класса DataObject.
Листинг 11. Функция DataIsChanged класса DataObject.


Далее можно сделать метод Delete, он у нас совсем простой (листинг 12).

 

Листинг 12. Реализация метода Delete класса DataObject.
Листинг 12. Реализация метода Delete класса DataObject.


Вот собственно и все. Осталось наследовать классы таблиц users и computers от класса DataObject и прописать соответствующие атрибуты (листинг 13).

 

Листинг 13. Конечная реализация классов Users и Computers.
Листинг 13. Конечная реализация классов Users и Computers.
(кликните по картинке, чтобы открыть её в полном размере)


 

Тестирование


Теперь можно проверить, как все это работает. Сначала проверим создание новых данных в БД. Для этого в программе нужно создать экземпляр класса соответствующего таблице, запись в которой требуется сделать (листинг 14).

 

Листинг 14. Тестирование создания новых данных в таблицах users и computers.
Листинг 14. Тестирование создания новых данных в таблицах users и computers.


После выполнения этого кода, в таблицах users и computers появятся новые данные (рисунок 1). Обратите внимание, что в записи computers для поля user_id установлен идентификатор пользователя из таблицы users, который был получен и передан в объект после сохранения данных (листинг 14, строка 9).

 

Рисунок 1. Добавленные записи в таблицы users и computers.
Рисунок 1. Добавленные записи в таблицы users и computers.


Далее проверим, как работает загрузка данных. Для этого, аналогично, нужно создать экземпляр класса соответствующего таблице, данные из которой требуется получить, а также установить идентификатор данных в ключевое поле и вызвать метод Load(). В моём случае, в таблице users есть запись с идентификатором (поле id) равным 1, а в таблице computers - 88DFC3CF-A43E-4120-AF1D-E0D2A5B8A39A, у вас эти значения могут отличаться. Именно эти идентификаторы и нужно указывать, чтобы загрузить данные (листинг 15).

 

Листинг 15. Загрузка и вывод данных из БД.
Листинг 15. Загрузка и вывод данных из БД.


В результате выполнения этого кода в консоль будут выведены загруженные из таблиц данные (рисунок 2).

 

Рисунок 2. Вывод в консоль данных из БД.
Рисунок 2. Вывод в консоль данных из БД.


Теперь вернемся к основной теме статьи – ведение журнала изменений. Для проверки этого, нужно загрузить данные, изменить их и сохранить (листинг 16).

 

Листинг 16. Загрузка данных, изменение и сохранение изменений.
Листинг 16. Загрузка данных, изменение и сохранение изменений.


После выполнения этого кода, в таблице log появятся записи со старыми данными (рисунок 3).

 

Рисунок 3. Записи журнала извещений в таблице log.
 Рисунок 3. Записи журнала изменений в таблице log.


Все работает, задача выполнена!
 

Что еще можно сделать


Для удобства загрузки данных, можно в классах таблиц users и computers прописать конструктор, в котором принимать идентификатор и загружать данные по нему в текущий объект (листинг 17).

 

Листинг 17. Конструкторы классов Users (слева) и Computers (справа).
Листинг 17. Конструкторы классов Users (слева) и Computers (справа).


Слева описан конструктор для класса Users. Здесь два предопределения. Первый (строки 1-3) – позволяет создавать экземпляр объекта без параметров, т.е. чистый, без загрузки данных. Его можно использовать для создания новых записей. Второй (строки 5-9) – принимает идентификатор данных, передает их в ключевое свойство (строка 7) и загружает данный в себя (строка 8).

Справа, аналогично, описан конструктор для класса Computers. Тут уже три предопределения. Поскольку идентификатор имеет тип Guid, который требует явного указания типа данных, а его передача чаще всего происходит в строковом виде, то для удобства третий конструктор принимает идентификатор в строковом (строка 11) виде и уже на его основе создает Guid (строка 13).

Обратите также внимание, что во всех конструкторах происходит инициализация базового класса – MyBase.New(). Это важно, поскольку в конструкторе класса DataObject выполняется первоначальное определение имени текущей таблицы и ключевого поля, без этого код будет работать некорректно.

Теперь можно указывать идентификатор записи при создании экземпляра класса и данные будут автоматически загружаться в объект (листинг 18).

 

Листинг 18. Загрузка и вывод данных из таблиц users и computers по указанному в конструкторе идентификатору.
Листинг 18. Загрузка и вывод данных из таблиц users и computers по указанному в конструкторе идентификатору.


 

Рисунок 4. Результат выполнения код из листинга 18.
Рисунок 4. Результат выполнения код из листинга 18.


Еще можно связать классы Users и Computers. Как вы помните, в таблице computers у нас есть поле user_id, которое содержит идентификатор пользователя в таблице users. Благодаря объектной модели, в классе Computers у нас есть возможность сделать свойство User, которое будет содержать экземпляр пользователя (листинг 19).

 

Листинг 19. Реализация свойства User в классе Computers.
Листинг 19. Реализация свойства User в классе Computers.


И, соответственно, мы будем иметь возможность работать с данными пользователя из экземпляра класса Computers (листинг 20).

 

Листинг 20. Получение данных о пользователе из экземпляра класса Computers.
Листинг 20. Получение данных о пользователе из экземпляра класса Computers.


 

Рисунок 5. Результат выполнения кода из листинга 20.
Рисунок 5. Результат выполнения кода из листинга 20.


А может так получиться, что один пользователь будет иметь более одной записи в таблице computers. Тогда целесообразно сделать в классе Users коллекцию Computers, которая будет содержать все связанные с указанным пользователем компьютеры. В общем, усложнять задачу можно бесконечно. Данные могут иметь довольно сложную структуру, но за счет объектной модели, в конечном счете, работать с ними будет достаточно просто.

 

Послесловие


Как видите, рутина была нейтрализована и теперь мы можем с минимальными временными затратами отслеживать изменения определенных данных. Не имеет значение, сколько таблиц будет в проекте, ибо писать отдельный функционал для этого не требуется. Классы таблиц являются своего рода скелетом. Если автоматизировать процесс создания классов для таблиц, то можно с большей пользой провести сэкономленное время. За счет наследования, мы в любое время можем улучшить и дополнить функционал базового класса – DataObject, не трогая классы наследники (скелеты).

Что касается производительности, то большое количество типового кода, помимо увеличения размера программы и скорости его обработки и отладки, ни к чему хорошему не приведет. Так что этот вопрос можно считать исчерпанным. Максимум функционала при минимуме кода – это, пожалуй, то к чему должен стремиться каждый программист. Хотя и приведенные в статье примеры можно было бы еще значительно укоротить, правда, тогда они могут оказаться понятными не для всех.

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

Если у вас возникнут какие-либо вопросы, с радостью отвечу в комментарии, либо на форуме для программистов.

--
Алексей Немиро
22.05.2010


Статья написана специально для проекта Kbyte.Ru. Полная, либо частичная перепечатка статьи запрещена.

Скачать пример к статье

+ Добавить в избранное
    ? Помощь
Об авторе

Алексей Немиро

Интернет-деятель. Автор многочисленных статей и переводов статей по программированию и информационным технологиям. Работы Алексея можно найти в популярных печатных изданиях компьютерной тематики. Автор проекта Kbyte.Ru.
Сейчас Алексей занимается профессиональным Web-программированием на базе технологий .NET Framework. Иногда пишет различные программки и компоненты для Windows и Android. В свободное время занимается Web-дизайном, увлекается фото- и видеосъемкой.

См. также:
Профиль автора
Алексей Немиро
Последние комментарии (всего: 4)

Добавлять комментарии могут только зарегистрированные пользователи сайта.
Если у Вас уже есть учетная запись на Kbyte.Ru, пройдите процедуру авторизации OpenID.
Если Вы еще не зарегистрированы на Kbyte.Ru - зарегистрируйтесь.

Алексей, а не могли бы Вы небольшой пример привести к тексту: "Тогда целесообразно сделать в классе Users коллекцию Computers...". С уважением.
Здравствуйте Алексей. Ссылка http://kbyte.ru/ru/Forums/Show.aspx?id=12492 почему-то перестала работать. Вы не могли бы посмотреть в чем дело?
Да, там лишний пробел нарисовался. Нужно скопировать ее как текст и вставить в адресную строку браузера.

PS: Свои сообщения (эта ссылка как раз на одно из них) форума можно найти в своем профиле в разделе "Сообщения в форумах" : http://kbyte.ru/ru/Private/ForumMessages.aspx?uid=2620
Авторизация
 
OpenID
Зарегистрируйся и получи 10% скидку на добавление своего сайта в каталоги! Подробнее »
Поиск по сайту
Люди на Kbyte.Ru
Реклама
Счетчики