Редактируемый QTableView сложного SQL-запроса

Как создать редактируемый QTableView, отображающий данные из сложного запроса SQLite?

Мне нужно заполнить QTableView данными из нескольких таблиц SQLite. Это должно быть доступно для редактирования пользователем.

Поскольку запросы немного сложны (включая JOIN и CASE WHEN и т. д.), я делаю это с помощью QSqlTableModel и QSqlQuery. Однако мне сказали, что это не то, как следует использовать QSqlTableModels. Итак, может ли кто-нибудь показать мне, как правильно получить результат, подобный показанному здесь?

Кроме того, хотя мои QTableView доступны для редактирования, результаты, похоже, не сохраняются в базе данных SQLite. (Когда я комментирую fill_tables, я все еще получаю исходные результаты после перезапуска графического интерфейса. Изменение EditStrategy на OnFieldChange не помогает.) Это потому, что я неправильно обрабатываю QSqlTableModel?

#!/usr/bin/python3

from PyQt5.QtSql import (QSqlDatabase, QSqlQuery, QSqlTableModel, 
                         QSqlRelationalTableModel, QSqlRelation)
from PyQt5.QtWidgets import QTableView, QApplication
from PyQt5.Qt import QSortFilterProxyModel
import sys

db_file = "test.db"

def create_connection(db_file):
    db = QSqlDatabase.addDatabase("QSQLITE")
    db.setDatabaseName(db_file)
    if not db.open():
        print("Cannot establish a database connection to {}!".format(db_file))
        return False
    return db


def fill_tables():
    q = QSqlQuery()
    q.exec_("DROP TABLE IF EXISTS Manufacturers;")
    q.exec_("CREATE TABLE Manufacturers (Company TEXT, Country TEXT);")
    q.exec_("INSERT INTO Manufacturers VALUES ('VW', 'Germany');")
    q.exec_("INSERT INTO Manufacturers VALUES ('Honda' , 'Japan');")

    q.exec_("DROP TABLE IF EXISTS Cars;")
    q.exec_("CREATE TABLE Cars (Company TEXT, Model TEXT, Year INT);")
    q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 2009);")
    q.exec_("INSERT INTO Cars VALUES ('VW', 'Golf', 2013);")
    q.exec_("INSERT INTO Cars VALUES ('VW', 'Polo', 1999);")


class CarTable(QTableView):
    def __init__(self):
        super().__init__()
        self.init_UI()
        self.create_model()

    def create_model(self):
        query = """
        SELECT (comp.company || " " || cars.model) as Car,
                comp.Country,
                (CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
        from manufacturers comp left join cars
            on comp.company = cars.company
        """
        raw_model = QSqlTableModel()
        q = QSqlQuery()
        q.exec_(query)
        self.check_error(q)
        raw_model.setQuery(q)

        self.model = QSortFilterProxyModel()
        self.model.setSourceModel(raw_model)
        self.setModel(self.model)

        # filtering:
        self.model.setFilterKeyColumn(0)
        self.model.setFilterFixedString('VW')

    def init_UI(self):
        self.resize(500,300)

    def check_error(self, q):
        lasterr = q.lastError()
        if lasterr.isValid():
            print(lasterr.text())
            exit(1)


def main():
    mydb = create_connection(db_file)
    if not mydb:
        sys.exit(-1)
    fill_tables()
    app = QApplication(sys.argv)
    ex = CarTable()
    ex.show()
    result = app.exec_()

    if (mydb.open()):
        mydb.close()

    sys.exit(result)


if __name__ == '__main__':
    main()

Вместо этого я попытался использовать QSqlRelationalTableModel, но я не могу выполнить ту же сложность запросов, и он также не сохраняет изменения, как в приведенном выше коде. Это то, что я получил в этой попытке:

     def create_model_alternative(self):
        self.model = QSqlRelationalTableModel()
        self.model.setTable("Cars")
        self.model.setRelation(0, QSqlRelation("Manufacturers", "Company",
                                               "Company, Country"))  
        self.setModel(self.model)
        self.model.select()

        # filtering:
        self.model.setFilter("cars.Company = 'VW'")

Чтобы ответить на входящие вопросы:

Редактируемость:

В этом примере единственным столбцом, который определенно обязательно должен быть редактируемым (таким образом, чтобы изменения поступали в базу данных), является столбец Country (и изменения в нем должны влиять на все другие строки, которые имеют одинаковые значения). содержание; например, если вы замените «Германия» на «Франция» для любого автомобиля VW, оба должны указать «Франция» в качестве страны).

Если вы знаете способ сделать первый из них доступным для редактирования, чтобы соответствующие столбцы в базе данных обновлялись, это было бы неплохо увидеть, но это не обязательное требование. (В моих реальных таблицах я использую такие «объединения столбцов» для нередактируемых полей.) В этом конкретном случае я ожидаю, что изменение «VW Polo» на «Marco Polo» также обновит «VW Golf» на «Marco Golf». ', так как столбец, используемый в объединении столбцов, — это Manufacturers.company, а не cars.company. (В действительности для объединения, вероятно, следует использовать cars.company, и в этом случае «VW Golf» останется без изменений. Но давайте предположим, что запрос приведен выше.)

Третий столбец предназначен для примера вычисляемого результата статистики, и они, как правило, только для чтения (редактировать их не имеет смысла).

Порядок столбцов:

Я был бы очень признателен за возможность выбирать порядок отображения столбцов даже между объединенными таблицами (как я мог бы сделать с запросом).


person CodingCat    schedule 10.04.2018    source источник
comment
Я собираюсь задать вам много вопросов, чтобы правильно понять ваши требования, для продвижения решение будет очень конкретным, я вижу, что его практически невозможно обобщить и сделать переносимым. Прежде всего, я вижу, что первый столбец представляет собой объединение первого и второго столбцов таблицы Cars, разделенных пробелом, поэтому вы хотите, чтобы при редактировании одного из них изменялись данные в соответствующих столбцах?   -  person eyllanesc    schedule 12.04.2018
comment
Второй столбец легче понять, а третий, чтобы быть результатом операции, которая включает в себя год, будет только читаться, я прав?   -  person eyllanesc    schedule 12.04.2018
comment
@eyllanesc Я обновил свой пост, чтобы ответить на ваши вопросы. Спасибо за попытку решить это.   -  person CodingCat    schedule 12.04.2018
comment
оптимальное решение - использовать QSqlQueryModel, я буду редактировать только его, это нормально?   -  person eyllanesc    schedule 12.04.2018
comment
И последний вопрос: если вы измените страну в строке VW Gold, а значит, также измените страну в VW Polo, этого вы ожидаете?   -  person eyllanesc    schedule 12.04.2018
comment
@eyllanesc, вы думаете, что решить это с помощью QSqlQueryModel лучше всего, меня это устраивает, если это редактируется. (На самом деле, я думаю, что наличие редактируемого подкласса QSqlQueryModel может быть очень полезным и для других!) В этом случае было бы здорово, если бы вы также могли объяснить, почему и о различиях в использовании между обеими моделями (или указать на хорошие, простой для понимания справочник (я не считаю документацию Qt как таковую, так как трудно понять детали без опыта работы с C++).   -  person CodingCat    schedule 12.04.2018
comment
Ваше последнее обновление смущает меня, и я думаю, что оно неверно, поскольку противоречит вашим предыдущим комментариям: единственный столбец, который действительно должен быть редактируемым (...), — это столбец «Страна», поэтому изменение, которое он говорит, неверно, мой вопрос заключался в том, что если Германия изменена в сводной таблице, она должна быть изменена во всех строках. и я думаю, что это то, что вы хотите.   -  person eyllanesc    schedule 12.04.2018
comment
Например, если следующее изображение imgur.com/a/mIj8Z меняет страну из первой строки с Германии во Францию, каков будет результат?   -  person eyllanesc    schedule 12.04.2018
comment
@eyllanesc: тогда Страна во второй строке также должна измениться, так как столбец, на который это ссылается, — это manufactueres.country. Непонятно, откуда путаница? Мое предложение имело в виду, что, в отличие от столбца Country, редактирование двух других столбцов необязательно (точнее: редактирование первого столбца необязательно, но круто, а редактирование третьего столбца не требуется, так как это не имеет смысла). Я отредактировал, надеюсь, теперь стало понятнее.   -  person CodingCat    schedule 12.04.2018


Ответы (2)


QSqlTableModel — это класс, наследуемый от QSqlQueryModel, поэтому можно сказать, что QSqlTableModel — это специализированный QSqlQueryModel для редактирования таблицы, поэтому его размер может быть ограничен или увеличен.

Для этого особого случая я предлагаю QSqlQueryModel Editable, для этого я сделал следующие изменения:

  • Я включил флаг Qt.ItemIsEditable для второго столбца.

  • Я перезаписал метод setData() для обновления таблицы производителей.

  • Я добавил столбец, который представляет компанию, он будет скрыт, но полезно получить строки, которые должны быть изменены в предыдущем изменении.

  • Я реализовал метод setFilter() для создания фильтров.


import sys

from PyQt5.QtCore import Qt
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt5.QtWidgets import QApplication, QTableView

db_file = "test.db"


def create_connection(file_path):
    db = QSqlDatabase.addDatabase("QSQLITE")
    db.setDatabaseName(file_path)
    if not db.open():
        print("Cannot establish a database connection to {}!".format(file_path))
        return False
    return True


def fill_tables():
    q = QSqlQuery()
    q.exec_("DROP TABLE IF EXISTS Manufacturers;")
    q.exec_("CREATE TABLE Manufacturers (Company TEXT, Country TEXT);")
    q.exec_("INSERT INTO Manufacturers VALUES ('VW', 'Germany');")
    q.exec_("INSERT INTO Manufacturers VALUES ('Honda' , 'Japan');")

    q.exec_("DROP TABLE IF EXISTS Cars;")
    q.exec_("CREATE TABLE Cars (Company TEXT, Model TEXT, Year INT);")
    q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 2009);")
    q.exec_("INSERT INTO Cars VALUES ('VW', 'Golf', 2013);")
    q.exec_("INSERT INTO Cars VALUES ('VW', 'Polo', 1999);")


class SqlQueryModel(QSqlQueryModel):
    def flags(self, index):
        fl = QSqlQueryModel.flags(self, index)
        if index.column() == 1:
            fl |= Qt.ItemIsEditable
        return fl

    def setData(self, index, value, role=Qt.EditRole):
        if index.column() == 1:
            company = self.index(index.row(), 2).data()
            q = QSqlQuery("UPDATE Manufacturers SET Country = '{}' WHERE Company =  '{}'".format(value, company))
            result = q.exec_()
            if result:
                self.query().exec_()
            else:
                print(self.query().lastError().text())
            return result
        return QSqlQueryModel.setData(self, index, value, role)

    def setFilter(self, filter):
        text = (self.query().lastQuery() + " WHERE " + filter)
        self.setQuery(text)


query = '''
        SELECT (comp.company || " " || cars.model) as Car,
                comp.Country,
                cars.company,
                (CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
        from manufacturers comp left join cars
            on comp.company = cars.company
        '''

if __name__ == '__main__':
    app = QApplication(sys.argv)
    if not create_connection(db_file):
        sys.exit(-1)

    fill_tables()

    view = QTableView()

    model = SqlQueryModel()
    q = QSqlQuery(query)
    model.setQuery(q)
    model.setFilter("cars.Company = 'VW'")
    view.setModel(model)
    view.hideColumn(2)
    view.show()
    sys.exit(app.exec_())
person eyllanesc    schedule 12.04.2018
comment
Большое спасибо, это работает хорошо! Мне нравится идея сделать редактируемыми только необходимые столбцы. Я сделал обобщенную версию этого на основе вашего кода, который я опубликую как отдельный ответ, потому что он может быть полезен другим с аналогичной проблемой. Но я не смог бы сделать это без вашей помощи (по крайней мере, не в ближайшее время), так что вы получите награду. :) - person CodingCat; 13.04.2018
comment
@CodingCat Обобщение очень сложно, если я не знаю всех конкретных требований, поэтому я просто делаю это как можно конкретнее, тогда я ожидал, что ваши отзывы помогут его настроить, но если вы это сделали, это кажется отличным. жду награду :D - person eyllanesc; 13.04.2018
comment
Мне нужно подождать еще 4 часа, прежде чем я смогу присудить награду. :D И да, как можно конкретнее, это хорошо, так как часто легче увидеть, что происходит. - person CodingCat; 13.04.2018
comment
Как реализовать в этой модели model.setEditStrategy(QSqlTableModel.OnManualSubmit)? QSqlQueryModel, похоже, не имеет этого (неудивительно, поскольку он не предназначен для редактирования). Вы можете помочь, пожалуйста? (Или мне сделать это новым вопросом?) - person CodingCat; 02.09.2018
comment
Я разместил дополнительный вопрос здесь: stackoverflow.com /вопросы/52166429/ - person CodingCat; 04.09.2018

Основываясь на отличном конкретном решении @eyllanesc, я сделал обобщенную версию QSqlQueryModel, в которой можно указать, какие столбцы должны быть редактируемыми. Это может потребовать корректировок для запросов других людей, но я надеюсь, что это будет полезно для тех, кто борется с похожими проблемами:

import sys

from PyQt5.QtCore import Qt
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt5.QtWidgets import QApplication, QTableView

db_file = "test.db"


def create_connection(file_path):
    db = QSqlDatabase.addDatabase("QSQLITE")
    db.setDatabaseName(file_path)
    if not db.open():
        print("Cannot establish a database connection to {}!".format(file_path))
        return False
    return True


def fill_tables():
    q = QSqlQuery()
    q.exec_("DROP TABLE IF EXISTS Manufacturers;")
    q.exec_("CREATE TABLE Manufacturers (Company TEXT, Country TEXT);")
    q.exec_("INSERT INTO Manufacturers VALUES ('VW', 'Germany');")
    q.exec_("INSERT INTO Manufacturers VALUES ('Honda' , 'Japan');")

    q.exec_("DROP TABLE IF EXISTS Cars;")
    q.exec_("CREATE TABLE Cars (Company TEXT, Model TEXT, Year INT);")
    q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 2009);")
    q.exec_("INSERT INTO Cars VALUES ('VW', 'Golf', 2013);")
    q.exec_("INSERT INTO Cars VALUES ('VW', 'Polo', 1999);")


class SqlQueryModel_editable(QSqlQueryModel):
    """a subclass of QSqlQueryModel where individual columns can be defined as editable
    """
    def __init__(self, editables):
        """editables should be a dict of format: 
        {INT editable_column_nr : (STR update query to be performed when changes are made on this column
                                   INT model's column number for the filter-column (used in the where-clause),
                                   )} 
        """
        super().__init__()
        self.editables = editables

    def flags(self, index):
        fl = QSqlQueryModel.flags(self, index)
        if index.column() in self.editables:
            fl |= Qt.ItemIsEditable
        return fl

    def setData(self, index, value, role=Qt.EditRole):
        if role == Qt.EditRole:
            mycolumn = index.column()
            if mycolumn in self.editables:
                (query, filter_col) = self.editables[mycolumn]
                filter_value = self.index(index.row(), filter_col).data()
                q = QSqlQuery(query.format(value, filter_value))
                result = q.exec_()
                if result:
                    self.query().exec_()
                else:
                    print(self.query().lastError().text())
                return result
        return QSqlQueryModel.setData(self, index, value, role)

    def setFilter(self, myfilter):
        text = (self.query().lastQuery() + " WHERE " + myfilter)
        self.setQuery(text)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    if not create_connection(db_file):
        sys.exit(-1)

    fill_tables()

    view = QTableView()

    editables = {1 : ("UPDATE Manufacturers SET Country = '{}' WHERE Company = '{}'", 2)}
    model = SqlQueryModel_editable(editables)
    query = '''
        SELECT (comp.company || " " || cars.model) as Car,
                comp.Country,
                cars.company,
                (CASE WHEN cars.Year > 2000 THEN 'yes' ELSE 'no' END) as this_century
        from manufacturers comp left join cars
            on comp.company = cars.company
        '''
    q = QSqlQuery(query)
    model.setQuery(q)
    model.setFilter("cars.Company = 'VW'")
    view.setModel(model)
    view.hideColumn(2)
    view.show()
    sys.exit(app.exec_())

Чтобы сделать объединенные столбцы редактируемыми, потребуется дополнительная работа и другой формат для редактируемых элементов, но это должно работать для любых столбцов, которые не содержат объединенных/вычисленных/агрегированных данных (например, «Страна» в этом примере).

person CodingCat    schedule 13.04.2018
comment
Хорошо, это то, что вы упомянули с обобщением, я думаю, это правильно :D - person eyllanesc; 13.04.2018