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

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

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

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

Кроме того, хотя мои QTableViews являются редактируемыми, результаты, похоже, не сохраняются в базе данных SQLite. (Когда я комментирую fill_tables, я все еще получаю исходные результаты после перезапуска GUI. Изменение 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'")

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

Возможностьредактирования:

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

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

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

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

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

2 ответа

Решение

Основываясь на отличном конкретном решении @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_())

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

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

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

  • Я включил флаг 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_())
Другие вопросы по тегам