Russian Qt Forum
Июнь 17, 2024, 09:50 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

Войти
 
  Начало   Форум  WIKI (Вики)FAQ Помощь Поиск Войти Регистрация  

Страниц: [1] 2   Вниз
  Печать  
Автор Тема: sqlite + QSqlTableModel в многопоточном приложении  (Прочитано 11088 раз)
vregess
Гость
« : Апрель 02, 2015, 21:18 »

Очередной вопрос про sqlite и потоки.

Т.к. с embedded firebird есть проблемы, решил откатиться на sqlite. Но тут свои грабли.

Основная проблема (imho) в том, что QSqlTableModel блокирует базу, потому что использует подгрузку строк (fetchMore) и все время держит активным QSqlQuery.
Поэтому я включил WAL, который по идее должен решать проблему блокировки читателей и писателей, и использую IMMEDIATE TRANSACTION, которую рекомендуют для многопоточных приложений, и в дополнение это решает проблему синхронизаций между несколькими подключениями.

Так вот, нифига это не работает, прошу помощь зала.

Накатал простой пример:
1. Есть QSqlTableModel и 2 кнопки.
2. Нажимаем кнопку 1 - worker в отдельном потоке заполняет таблицу, используя транзакцию (нажимаем 2 раза).
3. пинаем QSqlTableModel, чтобы она обновила данные.
4. Нажимаем кнопку 2 - в главном потоке пытаемся записать что-то в базу, тоже в транзакции.
5. Ничего не получается - база заблокирована.

Если на шаге 3 вытягиваем все строки, то блокировки не будет:
Код
C++ (Qt)
   m_model->select();
   while(m_model->canFetchMore())
         m_model->fetchMore();
 

но в некоторых случаях загрузка всех данных не желательна.

Как победить? Наследоваться от QSqlQueryModel/QSqlTableModel  и переопределять подгрузку данных? Или можно как-то настроить sqlite? И почему WAL здесь не решает проблему с блокировками?

PS qt 5.4.0, msvc 2010 x32, win7.
Записан
qate
Супер
******
Offline Offline

Сообщений: 1175


Просмотр профиля
« Ответ #1 : Апрель 03, 2015, 13:02 »

насколько я помню, полностью многопоточной sqlite и не является - она не упадет при обращении из другого потока, но вернет SQLITE_BUSY
Записан
Termit
Самовар
**
Offline Offline

Сообщений: 144



Просмотр профиля WWW
« Ответ #2 : Апрель 03, 2015, 16:09 »

но в некоторых случаях загрузка всех данных не желательна.

Приведите пример такого случая. Не могу придумать.
Записан

Человеческая глупость дает представление о бесконечности
(с) Иоанна Хмелевская
vregess
Гость
« Ответ #3 : Апрель 03, 2015, 21:47 »

насколько я помню, полностью многопоточной sqlite и не является - она не упадет при обращении из другого потока, но вернет SQLITE_BUSY

Не совсем так. Есть несколько режимов многопоточной работы:
Multi-thread - каждый поток должен использовать свое подключение.
Serialized - одно подключение можно использовать в любом потоке. Это режим с наименьшими ограничениями для хост-приложения, но неприемлемый для Qt, т.к. соединение можно использовать только в пределах родительского потока.

но в некоторых случаях загрузка всех данных не желательна.

Приведите пример такого случая. Не могу придумать.
1 млн строк в БД. Ну как пример (у меня не так).
Да и не зря же придумали QAbstractItemModel::fetchMore() и QAbstractItemModel::canFetchMore().
Записан
Termit
Самовар
**
Offline Offline

Сообщений: 144



Просмотр профиля WWW
« Ответ #4 : Апрель 04, 2015, 12:47 »

1 млн строк в БД. Ну как пример (у меня не так).
Да и не зря же придумали QAbstractItemModel::fetchMore() и QAbstractItemModel::canFetchMore().

Ну да и не зря же придумали пагинацию, фильтры и т.д. Не представляю реальное приложение где пользователю покажут таблицу в 1 млн строк сиди мол читай, разбирайся...
Воспользуйтесь QSqlQueryModel установите лимиты, фильтры и выгребайте все через fetchMore.

ИМХО при работе со sqlite и моделями это единственное правильное решение.
Записан

Человеческая глупость дает представление о бесконечности
(с) Иоанна Хмелевская
qate
Супер
******
Offline Offline

Сообщений: 1175


Просмотр профиля
« Ответ #5 : Апрель 04, 2015, 13:31 »

Воспользуйтесь QSqlQueryModel установите лимиты, фильтры и выгребайте все через fetchMore.

QSqlQueryModel и выгребает частями, это видно по примеру (также там видно, что sqlite не знает сколько данных всего придет - скролл бар прыгает)

Как я понимаю проблема тут в другом - пока QSqlQueryModel не выгреб еще все данные - база в статусе SHARED https://www.sqlite.org/lockingv3.html
И никакая запись не пройдет (не важно из какого потока), что конечно неудобно
Записан
qate
Супер
******
Offline Offline

Сообщений: 1175


Просмотр профиля
« Ответ #6 : Апрель 04, 2015, 15:08 »

получилось поменять пример чтобы все работало - надо при запросе insert в on_btt2_clicked() дать _новое_ имя для соединения (т.е. не подавать по соединению, которое использует модель для отображения)

Записан
vregess
Гость
« Ответ #7 : Апрель 06, 2015, 14:31 »

Как я понимаю проблема тут в другом - пока QSqlQueryModel не выгреб еще все данные - база в статусе SHARED https://www.sqlite.org/lockingv3.html

https://www.sqlite.org/lockingv3.html говорит:
Цитировать
The document only describes locking for the older rollback-mode transaction mechanism. Locking for the newer write-ahead log or WAL mode is described separately.

Я использую WAL.

И никакая запись не пройдет (не важно из какого потока), что конечно неудобно
Как раз с использованием WAL все будет работать, т.к. для этого он и предназначен - чтобы "читатели" не мешали "писателям".
Написал тест, где запись проходит при активном читающем запросе.

Но sqlite блокирует базу для соединения и в режиме WAL в определенном случае, и что-то в доках по этому поводу в https://www.sqlite.org/wal.html ничего не нашел.

Прикладываю тесты, кому интересно (все тесты для режима WAL):

1. testOneThread - все в одном потоке. Сначала делаем запрос и оставляем его активным, а следом в другом запросе пытаемся изменить БД.
2. testLockedMultiThread - делаем запрос в главном потоке и оставляем его активным, изменяем БД в другом потоке, а потом опять в главном потоке пытаемся изменить БД. Вот этот тест аналог программки в первом сообщении. Тут происходит блокировка.
3. testUnLockedMultiThread - аналог testLockedMultiThread, но первый запрос делаем неактивным. Все работает, ничего не блокируется.

testLockedMultiThread - не совсем понятно, почему так происходит. Пока мысль такая: внутри отдельного соединения все работает, как описано в https://www.sqlite.org/lockingv3.html, а взаимоотношения между несколькими соединениями описываются в https://www.sqlite.org/wal.html.

Видимо поэтому
не подавать по соединению, которое использует модель для отображения
и работает.

не зря же придумали пагинацию, фильтры и т.д.
Все же подгрузка данных и загрузка их по частям это разные вещи. Да, цель одна, но подходы разные.

ИМХО при работе со sqlite и моделями это единственное правильное решение.

Похоже на то. Я бы сказал, что не стоит держать активных соединений.

В общем пока решил написать свою модель, которая будет подгружать данные не активным запросом, как в Qt, а отдельными запросами с LIMIT.
Вопрос все еще открыт - почему падает testLockedMultiThread и где об этом написано?
Записан
qate
Супер
******
Offline Offline

Сообщений: 1175


Просмотр профиля
« Ответ #8 : Апрель 07, 2015, 12:44 »

вложения к последнему посту не смотрел, но почему бы не отдать моделям соединения на чтения - пусть себе читают сколько хотят, а если надо записать - делать это по другому новому соединению ?
Записан
qate
Супер
******
Offline Offline

Сообщений: 1175


Просмотр профиля
« Ответ #9 : Апрель 07, 2015, 12:49 »

********* Start testing of LockTest *********
Config: Using QtTest library 5.4.1, Qt 5.4.1 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 4.8.1 20130909 [gcc-4_8-branch revision 202388])
PASS   : LockTest::initTestCase()
PASS   : LockTest::testOneThread()
PASS   : LockTest::testLockedMultiThread()
PASS   : LockTest::testUnLockedMultiThread()
PASS   : LockTest::cleanupTestCase()
Totals: 5 passed, 0 failed, 0 skipped, 0 blacklisted
********* Finished testing of LockTest *********

что не так ?
Записан
vregess
Гость
« Ответ #10 : Апрель 07, 2015, 12:54 »

Все так.

строка 235:
Код
C++ (Qt)
QVERIFY( !query2.exec("BEGIN IMMEDIATE TRANSACTION") );
 

транзакция не начинается, но должна начинаться.

вложения к последнему посту не смотрел, но почему бы не отдать моделям соединения на чтения - пусть себе читают сколько хотят, а если надо записать - делать это по другому новому соединению ?

Интересная идея, но придется переписывать QSqlTableModel в тех местах, где происходит запись в бд. Хотя так и так что-то надо переписывать, возможно это самый короткий путь. Я попробую.

Спасибо за советы.
Записан
vregess
Гость
« Ответ #11 : Апрель 07, 2015, 14:25 »

вложения к последнему посту не смотрел, но почему бы не отдать моделям соединения на чтения - пусть себе читают сколько хотят, а если надо записать - делать это по другому новому соединению ?

Все же так просто не получается. Основные изменения базы идут через модели QSqlTableModel. Если даже сделать запись через отдельное соединение, переопределив некоторые методы, то получим проблему неполных данных в читающем соединении, и кучу сопутствующих проблем.

Пример.
Чтобы добавить строку переопределяем QSqlTableModel::deleteRowFromTable() и там, создав транзакцию в новом соединении, делаем инсерт. Чтобы модель узнала об этом, надо заново делать select(), но вряд ли так можно делать из deleteRowFromTable(). Не буду расписывать почему.
Записан
qate
Супер
******
Offline Offline

Сообщений: 1175


Просмотр профиля
« Ответ #12 : Апрель 08, 2015, 08:35 »

Пример.
Чтобы добавить строку переопределяем QSqlTableModel::deleteRowFromTable() и там, создав транзакцию в новом соединении, делаем инсерт. Чтобы модель узнала об этом, надо заново делать select(), но вряд ли так можно делать из deleteRowFromTable(). Не буду расписывать почему.

QTimer::singleShot(100, myModel, SLOT(select()));
Записан
vregess
Гость
« Ответ #13 : Апрель 08, 2015, 09:34 »

QTimer::singleShot(100, myModel, SLOT(select()));

Не, ну это костыль, и не факт, что все будет гладко.

В общем я сделал аналог QSqlQueryModel, чтобы она делала fetchMore() не активным запросом, а вытаскивала по кускам при помощи LIMIT/OFFSET, и его наследника (аналог QSqlTableModel), который все изменения делает в IMMEDIATE TRANSACTION.
Пока тестирую, но должно заработать.

Вопрос "Как победить?" вроде решился, но так и не ясно почему WAL не решает проблему с блокировками?
Записан
qate
Супер
******
Offline Offline

Сообщений: 1175


Просмотр профиля
« Ответ #14 : Апрель 08, 2015, 13:23 »

В общем я сделал аналог QSqlQueryModel, чтобы она делала fetchMore() не активным запросом, а вытаскивала по кускам при помощи LIMIT/OFFSET, и его наследника (аналог QSqlTableModel), который все изменения делает в IMMEDIATE TRANSACTION.
Пока тестирую, но должно заработать.

каждый LIMIT/OFFSET это же новый запрос, это медленнее чем курсором
каков ожидаемый объем таблиц ?

Вопрос "Как победить?" вроде решился, но так и не ясно почему WAL не решает проблему с блокировками?

копать глубже, писать тесты с использованием https://www.sqlite.org/capi3ref.html
Записан
Страниц: [1] 2   Вверх
  Печать  
 
Перейти в:  


Страница сгенерирована за 0.163 секунд. Запросов: 22.