- Реализовать работу генераторов в транзакции
- Добавить checkbox, с помощью которого можно контролировать выполнять добавление в транзакции или нет
Транзакции / подсказка к 3 задачке
Транзакция — это способ управления набором операций над объектами в базе данных как единой сущностей.
То есть, если некоторое действие требует манипуляции сразу несколькими объектами. То обычно если мы добавляем или удаляем или изменяем сразу несколько объектов и в процессе выполнения операции и что-то идет не так, то база у нас оказывается в не консистентном состоянии.
Ну то есть условно я хотел добавить дом с пятью этажами. Но во время добавления 3-го этажа у меня разорвалось соединение с базой данных и программа упала либо выкинула ошибку. В общем дом у меня теперь остается недостроенным. И в реальной жизни исправление такое ситуации может потребовать большого количества ручной работы.
Специально для таких целей были придуманы транзакции. Это такой способ выполнить набор команд как одну мета команду. С точки зрения SQL Server выглядит это примерно так.
BEGIN TRANSACTION; -- открываем транзакцию
INSERT Table(title, info) VALUES("...", "")
INSERT Table(title, info) VALUES("...", "")
-- ... куча инсертов либо каких то других манипуляций с данными
INSERT Table(title, info) VALUES("...", "")
COMMIT; -- выполняем команды которые попали в транзакцию
то есть транзакция как бы накапливает внутри себя набор изменений и выполняет их только по итогу выполнения команды COMMIT
Точнее даже не совсем так. Она их выполняет, но не выкатывает на общее обозрение пока не будет вызван COMMIT.
Например, если вы добавили новую строчку в таблицу Table
внутри транзакции, то внутри транзакции при запросе данных из таблицы Table
вы увидите эту строчку. А вот если кто-то другой попробуем запросить данные из этой таблицы, то для него новой строчки не будет существовать пока в транзакции не будет выполнена команда COMMIT;
Если пока не понятно, то сейчас разберем на реальном примере и все станет понятнее.
В общем, глянем как с этим работать в C#
Допустим я хочу, чтобы коты у меня добавлялись все кучей сразу, а не последовательно. Для этого я иду в метод генерации котов
private void GenerateCats(
SqlConnection connection,
Dictionary<string, Range> breedsProbablity,
int count,
int maleFemale,
IProgress<int> progress,
CancellationTokenSource cancellationToken
)
{
using (connection)
{
connection.Open();
var transaction = connection.BeginTransaction(); // создаю транзакцию (то есть по сути делаю BEGIN TRANASCTION)
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
SqlCommand command = new($@"
INSERT INTO Cats(name, breed)
VALUES('{name}', '{breed}')"
, connection);
// передаю транзакцию команду
// эту нужно чтобы команда попала в блок BEGIN TRANASCTION ... COMMIT
command.Transaction = transaction;
command.ExecuteNonQuery();
// ...
}
}
}
попробуем запустить
и видим, что хотя процесс создания записей идет, несмотря на это Статистика не обновляется. И более того даже по завершению процесса данные не добавляются.
Почему так происходит?
Потому что транзакцию обязательно надо закоммитить. Для этого после того как все операции который вы хотели выполнить в транзакции закончены надо добавить еще одну команду
using (connection)
{
connection.Open();
var transaction = connection.BeginTransaction();
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
SqlCommand command = new($@"
INSERT INTO Cats(name, breed)
VALUES('{name}', '{breed}')"
, connection);
command.Transaction = transaction;
command.ExecuteNonQuery();
// ...
}
transaction.Commit(); // <<< ВОТ ЭТУ, закоммитил транзакцию
}
пробуем запустить еще раз:
как видите добавление данных произошло только один раз. В момент, когда процесс генерации запустился.
Попробуем для полного понимания процесса еще выдавать информацию о количестве объектов изнутри транзакции. Как раз чтобы увидить что внутри транзакции все данные уже сохранены.
Для этого нам правда придется немного скорректировать IProgress, который мы передаем в функцию.
Короче, добавим структуру под информацию о прогрессе.
Теперь добавим на форму еще один лейбл, куда будем писать реальное количество кошек, которые висят в транзакции
теперь идем в реакцию на кнопку запустить
private async void btnGenerateCats_Click(object sender, EventArgs e)
{
// ...
// ЭТО ЗАМЕНЯЕМ
// var progress = new Progress<int>(value =>
// {
// pbCatGenerator.Value = value;
// });
// на вот это
var progress = new Progress<ProgressInfo>(progress =>
{
pbCatGenerator.Value = progress.value; // прогресс пихаем в прогресс бар
lblRealCatsCounts.Text = progress.info; // а текст пихаем в лейбл
});
// остальное не трогаем
btnGenerateCats.Text = "Отмена";
await Task.Run(() =>
{
GenerateCats(
connection,
breedsProbablity,
count,
maleFemale,
progress,
catGeneratorCanceletionToken
);
});
// ...
}
правлю метод GenerateCats, чтобы он на вход принимал IProgress<ProgressInfo>
private void GenerateCats(
SqlConnection connection,
Dictionary<string, Range> breedsProbablity,
int count,
int maleFemale,
IProgress<ProgressInfo> progress, // ТУТ ЗАМЕНИЛ на IProgress<ProgressInfo>
CancellationTokenSource cancellationToken
)
{
using (connection) // тут теперь просто передаю переменную в using
{
connection.Open();
var transaction = connection.BeginTransaction();
Random rnd = new();
for (var i = 0; i < count; ++i)
{
// ...
//progress?.Report(i + 1); старый прогресс убираю
// добавляю запрос чтобы узнать реальное количество кошек
command = new("SELECT count(*) FROM Cats", connection);
// опять подключаем транзакцию, так как уже новый запрос
command.Transaction = transaction;
// формирую информацию о прогрессе
var info = $"Кошек в транзакции {command.ExecuteScalar()}";
// ну и новый Report теперь так выглядит
progress?.Report(new ProgressInfo
{
value = i + 1,
info = info,
});
// ...
}
transaction.Commit();
}
}
проверяем:
то есть видно, что делая запрос изнутри транзакции у нас есть более актуальная информация о количестве кошек, но так как потенциально мы можем процесс отменить, то эти кошки никогда не будут добавлены в базу.
Проще говоря, если я буду тыкать Отмена
и запускать процесс по новой, то отсчет будет начинаться с последнего закомиченного изменения: