четверг, 27 мая 2010 г.

PyQt: Добавление записей в связанную модель (relationModel) во время редактирования записи основной

Абстрактная задача


Существует таблица 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 выполнялась следующая последовательность действий:
  1. У главной модели запрашивалась связанная модель (relationModel) по индексу 2.
  2. В связанную модель добавлялась строка (inserRow) и заполнялась данными (setData).
  3. Изменения связанной модели фиксировались (submit).
  4. Комбо-бокс позиционировался на вновь созданной записи.

Добавленная запись сразу же появлялась в выпадающем списке комбо-бокса, отображалось в самом комбо-боксе и вроде бы при вызове 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), нужно просто дернуть функцию обновления связки для нужной колонки.

Комментариев нет:

Отправить комментарий