Небольшая библиотека для упрощения работы с SQLite базой данных в ZennoPoster. Легковесный C# wrapper для SQLite.
В первую очередь, данная библиотека поможет исключить множество повторяющегося кода (перехват ошибок, создание SQLiteCommand на каждый запрос, правильную передачу параметров через SQLiteCommand.Parameters). А также, облегчит разработку и упростит обновление и изменение ваших проектов по ходу их развития.
Идея и часть реализации подсмотрена у DmitryAk + PHP PDO database framework Medoo.
Текущий релиз не избавляет полностью от работы с System.Data.SQLite объектами + содержит несколько изъянов, которые могут допустить SQL-injection (или скорее порчу данных), но даже эта версия уже пригодна для использования (аккуратного).
- Скомпилировать самостоятельно или взять последнюю версию FastSqliteHelper.dll из релизов и положить в директорию:
C:\Program Files (x86)\ZennoLab\RU\ZennoPoster Pro\[версия ZP]\Progs\ExternalAssemblies - Скопировать System.Data.SQLite.dll и SQLite.Interop.dll из папки
sqlite-netFx46-binary-x64-2015-1.0.107.0в вышеприведенную директориюExternalAssemblies - В проекте выбрать
Добавить действие->Свой код->Ссылки из GAC - Зайти в появившийся внизу блок
References(в Расширенном редакторе), нажать кнопкуДобавить..., затем кнопкуОбзор... - В появившемся окне выбрать:
C:\Program Files (x86)\ZennoLab\RU\ZennoPoster Pro\[версия ZP]\Progs\ExternalAssemblies\FastSqliteHelper.dll - Повторить пункт 4 и в появившемся окне выбрать:
C:\Program Files (x86)\ZennoLab\RU\ZennoPoster Pro\[версия ZP]\Progs\ExternalAssemblies\System.Data.SQLite.dll - В проекте выбрать
Добавить действие->Свой код->Директивы using и общий код - Зайти в появившийся внизу блок
OwnCodeUsings(в Расширенном редакторе) и в окне "Директивы Using" вставить:
using FastSqliteHelperLib;
using System.Data.SQLite;После этого у вас в коде появится класс FastSqliteHelper с публичными методами, описанными ниже.
- Добавлен метод
GetLastQuery(последний отправленный запрос) + добавлено его описание в readme - В описании ошибок, которые генерируются из библиотеки, добавлен вывод последнего отправленного запроса
- Доступ к методам
GenerateErrorMessageиSendToLogизменен на приватный - Улучшена простановка connection string для подключения + в случае ошибки добавлено отображение connection string в описании ошибки
- Исправлено неправильное описание подключения с использованием типа журнала WAL + исправлен тестовый шаблон
Первая версия библиотеки.
- Примеры работы практически со всеми методами есть в шаблоне
test_project.xmlz. - При использовании многопоточных шаблонов в ZennoPoster всегда добавляйте к строке дополнительных параметров connection string в методе
FastSqliteHelper.Initвключение типа журнала WAL с помощью:
Version=3;Journal Mode=WAL;
С ним скорее всего станет возможно писать и читать из одной таблицы без получения состояния "database is locked" в многопотоке. Но появятся дополнительные файлы (*-shm и *-wal), при этом вам нужно будет решить: оставить фиксацию измененных данных в автоматическом режиме или вручную делать так называемый "checkpoint" (везде советуют делать его в отдельном процессе, когда работа с базой в этот момент сведена к минимуму).
- Всегда закрывайте соединение с базой данных через метод
FastSqliteHelper.DeInit(). - Если в результате выполнения какого-либо из методов в результате возвращается объект класса
System.Data.SQLite.SQLiteDataReader("читатель"), то необходимо всегда закрывать его через метод.Close, чтобы избежать ошибок в многопотоке. Пример корректного закрытия "читателя" при получения множества строк с помощью методаFastSqliteHelper.Select:
System.Data.SQLite.SQLiteDataReader reader = FastSqliteHelper.Select("table_name", new string[]{"id", "value"});
try {
foreach(dynamic row in reader) {
project.SendInfoToLog("Строка: " + row["id"] + " -> " + row["value"]);
}
} finally {
reader.Close();
}Пример корректного закрытия "читателя" при получении единственного поля из единственной строки с помощью метода FastSqliteHelper.Select:
System.Data.SQLite.SQLiteDataReader reader = FastSqliteHelper.Select("table_name", "count(id) as count_all");
int count = 0;
try {
if (reader.Read()) {
count = Convert.ToInt32(reader["count_all"]);
}
} finally {
reader.Close();
}- Если в каком-то условии (
condition) для выборки/обновления/удаления вам нужно сделатьLIMIT,ORDER BYили что-то, что идет заWHERE- не забывайте, что в библиотеке это условие конкатенуется ("склеивается") с:
WHERE 1=1 andПоэтому, если вам не нужно указывать критерий для выборки (WHERE) - лучше добавить в условие что-то вроде (пример для LIMIT):
2=2 LIMIT 0,10Для того, чтобы избежать ошибки syntax error.
- Для массового добавления строк в таблицу необходимо открывать транзакцию (пример реализации работы через транзакцию есть в методе
FastSqliteHelper.Insert, но он может быть неудобен для ваших задач, т.к. предварительно нужно будет сделать список словарей с данными для вставки). Если не открывать транзакцию, то SQLite сделает это неявно на каждый Insert, поэтому у вас с очень большой долей вероятности будут "тормоза". Также, для ускорения процесса вставки (только при использовании в однопоточном шаблоне!) лучше отключить тип журнала и тип синхронизации при подключении к БД:
Journal Mode=OFF;Synchronous=0;
- В SQLite при дефолтном "journal_mode" очень просто поймать состояние "database is locked", оно возникает в том случае, если первый поток читает данные из таблицы, а второй поток туда пишет. Чтобы предотвратить такого рода поведение - нужно заранее продумывать логику работы шаблона и обязательно использовать оператор блокировки:
lock(YourOwn.LockerObject) {
// работа с БД через методы FastSqliteHelper
}Плюс, как было написано в самом начале - лучше использовать тип журнала WAL, но не факт, что это даст 100% гарантии того, что шаблон не попадет в такое состояние.
- Всегда явно указывайте столбцы для выборки, избегайте "*" для возвращения всех столбцов из таблицы. Это ускорит работу и поможет избежать ошибок, допущенных по невнимательности.
- В методе подключения к БД есть один очень важный параметр:
throw_exc_on_errors- "выдавать ли исключение в случае ошибки или просто по-тихому писать в лог ZP". Я настоятельно рекомендую выставлять его всегда вtrue, чтобы в случае ошибки - шаблон останавливал свое выполнение и прекращал работу, минуя тем самым другие ошибки, которые могут произойти далее. - К сожалению, в текущей реализации невозможно работать с 2 и более более SQLite базами данных одновременно. Это ограничение связано с тем, что класс
FastSqliteHelperи его методы - статические. Можно сделать "хак" с monkey-patching объекта подключения и переопределять его "на лету", но это не тот подход, который хотелось бы предлагать для использования. Возможно, когда-то в дальнейшем эта оплошность будет устранена. - Для удобного просмотра/изменения базы данных я советую использовать бесплатную программу SQLiteStudio (в ней есть русификация).
- Помните, что SQLite в первую очередь - это встраиваемая база данных в одном файле, поэтому ожидать от неё существенного прироста производительности - не стоит. Для таких целей лучше использовать MySQL или PostgreSQL. Но в целом, для небольших проектов/шаблонов, в которых не будет миллионов строк в базе - это очень хорошее решение, которое поможет избавиться от "списков" и "таблиц" в ZennoPoster. Советую к прочтению небольшой цикл статей о SQLite на хабре.
bool Init(string database_path, IZennoPosterProjectModel zp_project, string add_to_connection_string="", bool throw_exc_on_errors=true, bool show_log_in_poster=false)| Параметр | Описание |
|---|---|
| database_path | полный путь к файлу базы данных |
| zp_project | объект project из Zenno |
| add_to_connection_string | строка дополнительных параметров, добавляемых к connection string |
| throw_exc_on_errors | выдавать ли исключение в случае ошибки или просто по-тихому писать в лог ZP |
| show_log_in_poster | показывать ли ошибку в логе ZennoPoster (в случае false - сообщение будет отображено только в ProjectMaker) |
| возвращаемое значение | удалось ли подключиться к БД |
Инициализация подключения к SQLite базе данных (дополнительные параметры connection string в виде словаря)
bool Init(string database_path, IZennoPosterProjectModel zp_project, Dictionary<string, string> add_to_connection_string, bool throw_exc_on_errors=true, bool show_log_in_poster=false)| Параметр | Описание |
|---|---|
| database_path | полный путь к файлу базы данных |
| zp_project | объект project из Zenno |
| add_to_connection_string | словарь "ключ" => "значение" дополнительных параметров, добавляемых к connection string |
| throw_exc_on_errors | выдавать ли исключение в случае ошибки или просто по-тихому писать в лог ZP |
| show_log_in_poster | показывать ли ошибку в логе ZennoPoster (в случае false - сообщение будет отображено только в ProjectMaker) |
| возвращаемое значение | удалось ли подключиться к БД |
void DeInit()void PragmaSet(string name_and_value)| Параметр | Описание |
|---|---|
| name_and_value | название и значение параметра PRAGMA |
object PragmaGet(string name)| Параметр | Описание |
|---|---|
| name | название параметра PRAGMA |
| возвращаемое значение | значение параметра |
SQLiteDataReader Select(string table, string columns, string condition="")| Параметр | Описание |
|---|---|
| table | таблица |
| columns | строка столбцов, разделенных запятой |
| condition | условие выборки |
| возвращаемое значение | объект SQLiteDataReader с содержимым (не забывайте его закрыть!) |
SQLiteDataReader Select(string table, string[] columns, string condition="")| Параметр | Описание |
|---|---|
| table | таблица |
| columns | массив строк столбцов |
| condition | условие выборки |
| возвращаемое значение | объект SQLiteDataReader с содержимым (не забывайте его закрыть!) |
SQLiteDataReader Select(string table, IEnumerable columns, string condition="")| Параметр | Описание |
|---|---|
| table | таблица |
| columns | список строк столбцов |
| condition | условие выборки |
| возвращаемое значение | объект SQLiteDataReader с содержимым (не забывайте его закрыть!) |
int Insert(string table, Dictionary<string, object> data)| Параметр | Описание |
|---|---|
| table | таблица |
| data | данные в виде словаря "столбец" => "значение" |
| возвращаемое значение | количество добавленных записей |
int Insert(string table, List<Dictionary<string, object>> data_list)| Параметр | Описание |
|---|---|
| table | таблица |
| data_list | список словарей с данными в виде "столбец" => "значение" |
| возвращаемое значение | количество добавленных записей |
long LastInsertID()| Параметр | Описание |
|---|---|
| возвращаемое значение | идентификатор |
bool Update(string table, Dictionary<string, object> data, string condition="")| Параметр | Описание |
|---|---|
| table | таблица |
| data | словарь "столбец" => "значение" для изменения |
| condition | условие изменения |
| возвращаемое значение | успешно ли произошло обновление |
bool Update(string table, string field_name, object field_value, string condition="")| Параметр | Описание |
|---|---|
| table | таблица |
| field_name | столбец |
| field_value | новое значение |
| condition | условие изменения |
| возвращаемое значение | успешно ли произошло обновление |
int Delete(string table, string condition)| Параметр | Описание |
|---|---|
| table | таблица |
| condition | условие для удаления |
| возвращаемое значение | количество удаленных записей |
int Delete(string table, Dictionary<string, object> conditions, string logical_operator="AND")| Параметр | Описание |
|---|---|
| table | таблица |
| conditions | словарь "столбец" => "значение" для формирования условия выборки |
| logical_operator | логический оператор (AND/OR) для формирования условия |
| возвращаемое значение | количество удаленных записей |
int Query(string sql)| Параметр | Описание |
|---|---|
| sql | текст запроса |
| возвращаемое значение | количество затронутых строк |
SQLiteDataReader QueryReader(string sql)| Параметр | Описание |
|---|---|
| sql | текст запроса |
| возвращаемое значение | объект SQLiteDataReader с содержимым (не забывайте его закрыть!) |
object QueryScalar(string sql)| Параметр | Описание |
|---|---|
| sql | текст запроса |
| возвращаемое значение | первый столбец первой строки (если вернулась хотя бы 1 строка) или null (в ином случае) |
string GetLastQuery()| Параметр | Описание |
|---|---|
| возвращаемое значение | последний отправленный запрос |
Ниже описан план развития, задачи из которого по моему мнению нужно сделать в первую очередь, минуя добавление другого функционала, который может быть реализован непосредственно через работу с System.Data.SQLite.
Если у вас есть непреодолимое желание помочь этому проекту в развитии - посмотрите ниже, что я хотел бы видеть в следующих версиях этой библиотеки, сделайте Fork репозитория и отправьте свой Pull Request с решением любой из задач :)
- Написать тесты для каждого метода, чтобы в случае выпуска нового релиза можно было проверить не сломалось ли что-то где-то.
- Реализовать тестовый проект для консольного приложения, чтобы не было нужды проверять все методы в ProjectMaker, а была возможность работы в VisualStudio/SharpDevelop.
- Проверить и разобраться почему при установке какого-либо значения PRAGMA через строку подключения - чтение этого параметра через метод
PragmaGetполучает другой результат. - Избавиться от возвращения
SQLiteDataReaderв нескольких методах, чтобы можно было не думать о том, что его нужно всегда "закрывать". - Облегчить работу с транзакциями (избавиться от явного вызова Commit/Rollback), используя анонимные функции (делегаты в C#?). Пример правильной организации выполнения запросов внутри транзакции на PHP: https://medoo.in/api/action
- Реализовать передачу условий в методы, чтобы они не "склеивались" с условием
WHERE 1=1 and, чтобы была возможность передатьLIMIT 0,10без добавления конструкции вида2=2. - Продумать и реализовать способ передачи условий в виде "столбец" => "значение" без ограничения на то, что все имена столбцов должны быть уникальны.
- Добавить метод для экранирования значений, которые могут быть в дальнейшем переданы в виде plain-запроса в методы
Query*. Пример похожего функционала в PHP PDO драйвере: http://php.net/manual/ru/pdo.quote.php - Добавить возможность работы с несколькими SQLite базами одновременно (убрать статический модификатор для класса, но это повлечет проблемы при передаче объекта через
project.Contextв ProjectMaker или сделать полеconnectionв виде словаря подключений, но нужно будет предварительно протестировать этот вариант в многопоточном режиме, чтоб не вышло, что данные будут пересекаться). - Переименовать методы
Init->Open,DeInit->Close. Сомнительно и добавит обратную несовместимость, но читаемость кода повысится. - Реализовать получение/вставку/обновление/удаление данных через объекты, а не через словари. Плюс - не нужно будет делать некрасивые словари для работы с этими методами; минус - для каждой таблицы, с которой нужно будет работать - нужно будет создавать классы и вести работу через них.
- Проверить и исправить все методы, которые подвержены sql-injection, чтобы в ходе работы не испортились данные.
- В методах, которые не требуют работы с параметрами (
SQLiteCommand.Parameters) - переписать отправку запроса/получение результата через внутренние методыQuery*. Но это может привести к тому, что будет сложно разобрать в сообщении об ошибке - откуда именно упало исключение (поэтому данная задача под сомнением). - Добавить метод, который будет возвращать последний выполненный запрос (или даже лучше - отправленный, чтобы в случае перехвата ошибки внутри метода его можно было отобразить). Пример такого функционала в PHP фреймворке Medoo: https://medoo.in/api/last
- Подумать над тем, что в последний отправленный запрос может быть списком (логом) всех запросов. Плюс решить стоит ли там заменять параметры на их значения.
- Написать Contributing Guidelines, которые будут включать в себя: правильную настройку редактора, чтобы избежать табуляций в коде; использование
String.Format/String.Concat; удаление trailing whitespace и другие полезные вещи.
- тут может быть твой ник и ссылка на тебя ;-)
CC BY-NC-SA 3.0 (Creative Commons — «Attribution-NonCommercial-ShareAlike» 3.0)
Лицензия «С указанием авторства — Некоммерческая — С сохранением условий»
Данная лицензия позволяет другим людям редактировать, поправлять и брать произведение за основу для производных в некоммерческих целях при условии, что они указывают авторство и лицензируют свои новые произведения на тех же условиях.