Как правильно и удобно читать данные из БД?

Когда я читаю данные из файла, я делаю это синхронно (чаще всего это так).
Я выполняю код последовательно и знаю, что следующая строка кода будет выполнена только после предыдущей.
string fileContent1 = File.ReadAllText("example1.txt");
string fileContent2 = File.ReadAllText("example2.txt");
string resultString = fileContent1 + fileContent2; // Для строки resultString  "всегда" будут данные  (синхронное, последовательное чтение).


Когда я читаю данные из БД SQLite, я делаю это асинхронно. Код чтения выполняется "параллельно" (в другом потоке, контексте, пуле и т.д.)
Я не знаю когда будет получено значение из базы (нагрузка на БД, скорость чтения и т.д.), поэтому использую колбеки.
public static void ReadFromBase1(Action onComplete)
{
// Тут код чтения из базы. Мы получаем данные из базы - число.
int numberFromBase1 = db.Query<SQLiteTable1.MyTable>(sqlCommandText);
onComplete?.Invoke();
}


Пусть мне надо сложить 2 числа. И эти числа хранятся в разных БД (не будем использовать джойны и агрегатные функции).
Тогда мне надо получить первое число из базы - дождаться когда придет ответ и далее, аналогично, получить второе число. Когда оба числа поступят - сложить их.

В результате получаем такой код
public static void ReadFromBase1(Action onComplete)
{
int numberFromBase1 = db.Query<SQLiteTable.MyTable1>(sqlCommandText);
onComplete?.Invoke();
}

public static void ReadFromBase2(Action onComplete)
{
int numberFromBase2 = db.Query<SQLiteTable.MyTable2>(sqlCommandText);
onComplete?.Invoke();
}


public static void Summ(Action onComplete)
{

// Получаем число из БД 1
ReadFromBase1(
// Только после получения числа из БД 1 получаем число из БД 2 (например так)
ReadFromBase2(() => 
{
// Все числа получены - складываем.
int summ =  numberFromBase1 + numberFromBase2;
});
});


onComplete?.Invoke();
}


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

Можно сделать обертку.
// Обернули 2 метода в метод с колбеком.
public static void SummWrapper(Action onComplete)
{
ReadFromBase1(() => {});
ReadFromBase2(() => {});
onComplete?.Invoke();
}

public static void Summ()
{
SummWrapper(() => {
int summ =  numberFromBase1 + numberFromBase2;
});
}


В результате простое чтение двух чисел из БД приводит к довольно большому объему кода, в котором надо контролировать последовательность выполнения операций.
Можно ли это упростить?
Как правильно и удобно читать данные из БД?
  • Вопрос задан
  • 275 просмотров
Решения вопроса 2
1. async-await используй
2. Используй какую-нибудь orm-ку (EF core, Dapper, linq2db). Многие из них как правило явно запрещают параллельные запросы в рамках одной транзакции.

Но вроде чисто в теории sqlite разрешает конкурентное чтение (но не запись)
Ответ написан
Комментировать
@mvv-rus
Настоящий админ AD и ненастоящий программист
Думаю, что надо раскрыть подробнее тему предыдущих ответов и комментариев.
Проблемы со сложностью у вас, потому что вы не используете современые средства .NET и C#.
В .NET исторически сложилось несколько слоев API для обеспечения параллельной работы, и использование функций обратного выдова - самый старый и самый громздкий из них. А самый удобный и современный - это Task Parallel Library (TPL): он, плюс конструкции языка C# async/await, позволяет писать такой асинхронно выполняющийся код так, будто он выполняется синхронно.
Скорее всего (я просто не уточнял, потому пишу так) для SQLite есть асинхронный API и для использования с TPL, и API для синхронного использования, что ещё удобнее - и он должен быть не менее быстрым чем синхронная работа с файлами, потому что это встроенная БД.
Но если вы по каким-то причинам будете работать с асинхронным API на функциях обратного вызова, то тут тоже есть способ упростить код: транслировать вызовы такого API для использования его со средствами TPL. Для этого используйте объект типа TaskCompletionSource (в вашем случае - обобщенный, специалиированный вашим типом результата: TaskCompletionSource<int>
У этого объекта есть свойство Task, возвращающее задачу, состоянием котрой можно управлять с помощью методов этого объекта - SetResult и других, вызывая эти методы в функциях обратного вызова (AKA callback). Как это делать - см. пример по ссылке.
А с Task, каким бы способом вы его не получили, вам будет работать значительно удобнее: если пометить метод, в котором вам надо работать с БД как async (в современном C# так можно пометить даже Program.Main), то в этом методе можно будет использовать для получения значения операцию await
TaskCompletionSource<int> tcs1=new TaskCompletionSource();
//... операции по по настройке tcs1 и запуску первого чтения из БД
int numberFromBase1 = await tcs1.Task.ConfigureAwait(false);
TaskCompletionSource<int> tcs2=new TaskCompletionSource();
//... операции по по настройке tcs2 и запуску второго чтения из БД
int numberFromBase2 = await tcs2.Task.ConfigureAwait(false);
int summ =  numberFromBase1 + numberFromBase2;
return summ;

Вызов ConfigureAwait(false) здесь нужен для избежания блокировок в некоторых контекстах синхронизации (приложения Windows Forms, WPF и др.). Если же у вас программа выполняется вне специфического контекста синхронизации - консольное приложение, приложение ASP.NET Core и др., - то этот вызов не обязателен.
Компилятор, увидев операцию await, преобразует метод нужным образом, так, чтобы он выполнялся асинхронно, передавая управление другим задачам во время ожидания (т.е. до выполнения функции обратного вызова) и возобнавлял выполнение по его завершении -и вам думать об этом и писать нужный для этого код будет не нужно.
Такой метод вернет Task (возможно - ещё до завершения всех операций) и с его результатом можно что-то сделать: либо получить его опять чере await асинхронно, либо дождаться завершения асинхронных операций с получением результата через вызов метода Task<int&gt.GetAwaiter().GetResult() (есть другие варианты, но этот IMHO лучше всего в плане обработки исключений).
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы