Абстрактная задача
Существует таблица main, содержащая помимо прочего поле-ссылку на справочник classifier_id. Код на sqlite:
CREATE TABLE classifier ( id INTEGER NOT NULL, name TEXT ); CREATE TABLE main ( id INTEGER NOT NULL, name TEXT, classifier_id INTEGER REFERENCES classisfier (id) );
Необходимо написать на PyQt диалог редактирования записи таблицы main с возможностью добавления новых записей в таблицу classifier в процессе редактирования.
Модель для main - QSqlRelationalTableModel, добавлена связка c третьим полем (индекс 2) QRelation('classifier', 'id', 'name').
Для редактирования поля classifier_id необходимо использовать QComboBox, виджеты диалога связываются с редактируемой моделью с помощью QDataWidgetMapper (SubmitPolicy = ManualSubmit)
Проблема
Задача решалась в лоб известными средствами PyQt, при необходимости добавить запись в таблицу classifier выполнялась следующая последовательность действий:
- У главной модели запрашивалась связанная модель (relationModel) по индексу 2.
- В связанную модель добавлялась строка (inserRow) и заполнялась данными (setData).
- Изменения связанной модели фиксировались (submit).
- Комбо-бокс позиционировался на вновь созданной записи.
Добавленная запись сразу же появлялась в выпадающем списке комбо-бокса, отображалось в самом комбо-боксе и вроде бы при вызове submitAll маппера идентификатор новой записи должен был появиться в поле main.classifier_id. Однако этого не происходило, поле оставалось пустым. Потратил в общей сложности около десяти часов пытаясь добиться нужного поведения, однако не преуспел. Пришлось лезть в код Qt.
Причина проблемы
Все дело оказалось в особенности реализации класса QSqlRelationalTableModel. В нем существует внутренние словарики пар "индексное поле - отображаемое поле" для каждого relation и при заполнении основной модели данными проверяется наличие значения соответствующего индексного поля в соответствующем словарике. Если значения нет - поле тупо не заполняется. Данные словарики заполняются в трех точках - при получении/установки данных соответствующей колонки (data, setData) и при доступе к соответствующей связанной модели (relationModel). Очищаются эти словарики только при полной очистке модели, что в контексте решаемой задачи совсем неприемлемо. Предварительный вывод печален - без допиливания кода Qt решить задачу нельзя :(
Решение
После некоторых размышлений относительно малокровное решение было найдено. Может не самое оптимальное, но быстро реализуемое, рабочее и не затрагивающее Qt/PyQt. Основная идея - наследовать свою модель от QSqlRelationalTableModel, завести собственную структуру данных с подобными словариками, перекрыть весь код QSqlRelationalTableModel, который их использует (благо его немного) и добавить метод, обновляющий словарики принудительно. Основной недостаток такого решения - появление в памяти дублирующихся данных из связанных таблиц, что при большом размере классификаторов может вызвать падение программы по памяти. В моем случае классификаторы маленькие и ничего оптимизировать я не стал.
Код нового класса модели:
from PyQt4.QtSql import QSqlRelationalTableModel, QSqlTableModel from PyQt4.QtCore import Qt class UpdatableRelationalModel (QSqlRelationalTableModel): def __init__(self, *args, **kwargs): QSqlRelationalTableModel.__init__(self, *args, **kwargs) self.cache = {} #обновляемый контейнер для хранения связанных данных def setRelation(self, column, relation): u''' Перегрузка функции QSqlRelationalTableModel.setRelation Дополнительно инициализирует контейнер для соответствующей колонки ''' QSqlRelationalTableModel.setRelation(self, column, relation) self.cache[column] = {} def data(self, index, role=Qt.DisplayRole): u''' Перегрузка функции QSqlRelationalTableModel.data Заполняет контейнер данными для соответствующей колонки, если еще не заполнен ''' col = index.column() if col in self.cache and not self.cache[col]: self._populateDictionary(col) val = QSqlRelationalTableModel.data(self, index, role) if not val.isValid() and Qt.DisplayRole == role: tm_val = QSqlTableModel.data(self, index, role) if tm_val.isValid(): return self.cache[col][unicode(tm_val.toString())] return val def setData(self, index, value, role=Qt.EditRole): u''' Перегрузка функции QSqlRelationalTableModel.data Заполняет контейнер данными для соответствующей колонки, если еще не заполнен. Проверка на валидность внешнего ключа использует self.cache, а не внутренний контейнер QSqlRelationalTableModel ''' col = index.column() if Qt.EditRole == role and col > 0 and self.relation(col).isValid(): if not self.cache[col]: self._populateDictionary(col) if unicode(value.toString()) not in self.cache[col]: return False return QSqlTableModel.setData(self, index, value, role) def _populateDictionary(self, column): u''' Заполнение контейнера соответствующей колонки из связанной модели. Контейнер предварительно не очищается. Функция предназначена для использования только внутри класса. ''' rel_model = self.relationModel(column) rel = self.relation(column) for i in xrange(0, rel_model.rowCount()): rec = rel_model.record(i) self.cache[column][unicode(rec.field(rel.indexColumn()).value().toString())] = \ unicode(rec.field(rel.displayColumn()).value().toString()) def relationRefresh(self, column): u''' Обновление контейнера соответствующей колонки из связанной модели с предварительной очисткой и обновлением связанной модели. ''' rel_model = self.relationModel(column) if rel_model: rel_model.select() self.cache[column] = {} self._populateDictionary(column) def relationsRefresh(self): u''' Обновление контейнеров всех колонок из связанных моделей. ''' for i in xrange(0, self.columnCount()): self.relationRefresh(i)
Решение для Qt на С++ я не реализовывал, ибо не пользуюсь. Однако путь тот же самый - породить свою модель и добавить свой метод обновления этих словариков. Решение для Qt получится еще проще, так как не придется вводить свою структуру словариков (ибо доступна родная) и дописывать код ее обработки (переопределять методы data и setData).
То есть надо будет добавить примерно такую функцию класса:
void UpdatableRelationalModel::refreshRelation(int column) { d->relations[column].clear(); d->relations[column].populateDictionary(); }
Приведенный код C++ не проверялся, но смысл должен быть ясен :)
Перед использованием приведенного кода в своем проекте настоятельно рекомендую просмотреть код класса QSqlRelationalTableModel (QtDir\src\sql\models\qsqlrelationaltablemodel.cpp). Он легко читается, простой и понятный.
Теперь, после добавления и заполнения новой строки в модель справочника (таблица classifier), нужно просто дернуть функцию обновления связки для нужной колонки.
Комментариев нет:
Отправить комментарий