본문 바로가기
PyQt5_

Querying SQL databases with Qt models

by 자동매매 2023. 3. 13.

databases/tableview.py

import os
import sys

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        # self.model = ?
        # self.table.setModel(self.model)

        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

Connecting to a database

데이터베이스의 데이터를 앱에 표시하려면 먼저 데이터베이스에 연결해야 합니다. 서버 (IP, : PostgreSQL 또는 MySQL) 파일 기반 (SQLite) 데이터베이스는 모두 Qt에서 지원되지만 설정 방법에만 차이가 있습니다.

https://github.com/lerocha/chinook-database

chinook.sqlite
1.02MB

 

 

import os
from PyQt6.QtSql import QSqlDatabase
basedir = os.path.dirname(__file__)
db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()

코드를 배치하는 위치는 응용 프로그램에 따라 다릅니다. 종종 단일 데이터베이스 연결을 만들어 전체에서 사용하려고 합니다 경우 별도의 모듈을 보유할 db.py) 만드는 것이 가장 좋습니다.

 

프로세스는 모든 데이터베이스에 대해 동일합니다 데이터베이스 개체를 만들고 이름을 설정한 다음 데이터베이스를 열어 연결을 초기화합니다.

 

Displaying a table with QSqlTableModel

 

앱을 데이터베이스 저장소에 연결한 수행할 있는 가장 간단한 작업은 응용 프로그램에 단일 테이블을 표시하는 것입니다. 이를 위해 QSqlTableModel 사용할 있습니다. 모델은 테이블에서 직접 데이터를 표시하므로 편집할 있습니다.

 

먼저 위에서 만든 데이터베이스 개체를 전달하는 테이블 모델의 인스턴스를 만들어야 합니다. 그런 다음 데이터를 쿼리할 원본 테이블을 설정해야 합니다이것은 데이터베이스의 테이블 이름이며 여기서는 <테이블 이름>입니다. 마지막으로 모델에서 .select() 호출해야 합니다.

model = QSqlTableModel(db=db)

model.setTable('<table    name>')

model.select()

 

.select() 호출하여 모델에 데이터베이스를 쿼리하고 결과를 표시 준비를하도록 지시합니다. 데이터를 QTableView 표시하려면 단순히 views .setModel() 메서드에 전달하면됩니다.

table = QTableView()

table.setModel(self.model)

 

데이터는 테이블 모델에 표시되며 스크롤 막대를 사용하여 탐색할 있습니다. 데이터베이스를 로드하고 뷰에 track 테이블을 표시하는 전체 코드는 아래를 참조하십시오.

 

tableview_tablemodel.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlTableModel(db=db)

        self.table.setModel(self.model)

        self.model.setTable("Track")
        self.model.select()

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

This will give you the following window when run.

Figure 148. The tracks table displayed in a QTableView

 

오른쪽 가장자리를 끌어 크기를 조정할 있습니다.

오른쪽 가장자리를 클릭하여 내용에 맞게 크기를 조정합니다.

 

Editing the data

QTableView 표시된 데이터베이스 데이터는 기본적으로 편집 가능합니다 - 셀을 클릭하면 내용을 수정할 있습니다. 변경 내용은 편집을 마친 직후 데이터베이스에 다시 유지됩니다.

 

editing strategy

Strategy Description
QSqlTableModel.EditStrategy.On ManualSubmit Changes are cached in the model, and written to the database only when .submitAll() is called, or discarded when revertAll() is called.
QSqlTableModel.EditStrategy.On FieldChange Changes are applied automatically, when the user deselects the edited cell.
QSqlTableModel.EditStrategy.On RowChange Changes are applied automatically, when the user selects a different row.

 

적용 예

self.model.setEditStrategy(QSqlTableModel.EditStrategy.OnRowChange)

 

Sorting columns

주어진 열을 기준으로 테이블을 정렬하려면 모델에서 .setSort() 호출하여 인덱스와 Qt.SortOrder.AscendingOrder 또는 Qt.SortOrder.DescendingOrder 전달할 있습니다.

 

databases/tableview_tablemodel_sort.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlTableModel(db=db)

        self.table.setModel(self.model)

        # tag::sortTable[]
        self.model.setTable("Track")
        self.model.setSort(2, Qt.SortOrder.DescendingOrder)
        self.model.select()
        # end::sortTable[]

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

작업은 .select() 호출하기 전에 수행해야 합니다. 데이터를 얻은 정렬하려면 다른 .select() 호출을 수행하여 새로 고칠 있습니다.

Figure 149. The tracks table sorted on column index 2, the album_id

 

인덱스 대신 이름을 사용하여 테이블을 정렬하는 것이 좋습니다. 이렇게 하려면 이름이 있는 인덱스를 조회할 있습니다.

 

databases/tableview_tablemodel_sortname.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlTableModel(db=db)

        self.table.setModel(self.model)

        # tag::sortTable[]
        self.model.setTable("Track")
        idx = self.model.fieldIndex("Milliseconds")
        self.model.setSort(idx, Qt.SortOrder.DescendingOrder)
        self.model.select()
        # end::sortTable[]

        self.model.select()

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

이제 테이블이 밀리초 열로 정렬됩니다.

 

Figure 150. The tracks table sorted on the milliseconds column.

 

 

Column titles

 

기본적으로 테이블의 머리글 제목은 데이터베이스의 이름에서 가져옵니다. 종종 이것은 매우 사용자 친화적이지 않으므로 .setHeaderData 사용하여 열 인덱스, 방향(horizontal (top) or vertical (left) header) 레이블 전달하여 적절한 제목으로 바꿀 있습니다.

 

database/tableview_tablemodel_titles.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlTableModel(db=db)

        self.table.setModel(self.model)

        # tag::titles[]
        self.model.setTable("Track")
        self.model.setHeaderData(1, Qt.Orientation.Horizontal, "Name")
        self.model.setHeaderData(
            2, Qt.Orientation.Horizontal, "Album (ID)"
        )
        self.model.setHeaderData(
            3, Qt.Orientation.Horizontal, "Media Type (ID)"
        )
        self.model.setHeaderData(
            4, Qt.Orientation.Horizontal, "Genre (ID)"
        )
        self.model.setHeaderData(
            5, Qt.Orientation.Horizontal, "Composer"
        )
        self.model.select()
        # end::titles[]

        self.model.select()

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

 

Figure 151. The tracks table with nicer column titles.

 

정렬할 때와 마찬가지로 인덱스를 사용하는 것이 항상 편리한 것은 아닙니다데이터베이스에서 순서가 변경되면 응용 프로그램에 설정된 이름이 동기화되지 않습니다.

 

이전과 마찬가지로 .fieldIndex() 사용하여 주어진 이름의 인덱스를 조회할 있습니다. 단계 나아가 모델을 설정할 번에 적용할 이름과 제목의 Python dict 정의할 있습니다.

 

database/tableview_tablemodel_titlesname.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlTableModel(db=db)

        self.table.setModel(self.model)

        # tag::titles[]
        self.model.setTable("Track")
        column_titles = {
            "Name": "Name",
            "AlbumId": "Album (ID)",
            "MediaTypeId": "Media Type (ID)",
            "GenreId": "Genre (ID)",
            "Composer": "Composer",
        }
        for n, t in column_titles.items():
            idx = self.model.fieldIndex(n)
            self.model.setHeaderData(idx, Qt.Orientation.Horizontal, t)

        self.model.select()
        # end::titles[]

        self.model.select()

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

Selecting columns

 

테이블의 모든 열을 표시하지 않으려는 경우가 많습니다. 모델에서 열을 제거하여 표시할 열을 선택할 있습니다. 호출을 수행하려면

.removeColumns() 제거할 번째 열의 인덱스와 후속 열의 수를 전달합니다.

 

self.model.removeColumns( 2 , 5 )

 

제거되면 열이  이상 테이블에 표시되지 않습니다.  레이블 지정에 사용되는 것과 동일한 이름 조회 방법을 사용하여 이름으로 열을 제거할  있습니다.

columns_to_remove = ['name', 'something']

for cn in columns_to_remove:

    idx = self.model.fieldIndex(cn)

    self.model.removeColumns(idx, 1 )

이러한 방식으로 열을 제거하면 뷰에서 열만 제거됩니다.

SQL 열을 필터링하려면 아래 쿼리 모델을 참조하십시오.

 

Filtering a table

모델에서 .setFilter() 호출하여 필터를 설명하는 매개 변수를 전달하여 테이블을 필터링 있습니다. 필터 매개 변수는 앞에 WHERE 추가되지 않은 유효한 SQL WHERE 절일 있습니다. 예를 들어 name="Martin" 사용하여 정확히 일치시키거나 name LIKE "Ma%"을 사용하여 "Ma" 시작하는 필드를 일치시킵니다.

Pattern Description
field="{}" Field matches the string exactly.
field LIKE "{}%" Field begins with the given string.
field LIKE "%{}" Field ends with the given string.
field LIKE "%{}%" Field contains the given string.

 

예제에서 {} 파이썬 "{}".format(search_str) 사용하여 보간해야 하는 검색 문자열입니다. 정렬과 달리 필터는 .select() 다시 호출할 필요 없이 데이터에 자동으로 적용됩니다.

 

select()가 아직 호출되지 않은 경우 필터가 처음 호출될 때 적용됩니다.

 

다음 예제에서는 QLineEdit 필드를 추가하고 이를 연결하여 트랙 이름 필드에서 테이블을 검색합니다. 라인 편집 변경 신호를 연결하여 필터를 구성하고 모델에 적용합니다.

 

databases/tableview_tablemodel_filter.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import (
    QApplication,
    QLineEdit,
    QMainWindow,
    QTableView,
    QVBoxLayout,
    QWidget,
)

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        container = QWidget()
        layout = QVBoxLayout()

        self.search = QLineEdit()
        self.search.textChanged.connect(self.update_filter)
        self.table = QTableView()

        layout.addWidget(self.search)
        layout.addWidget(self.table)
        container.setLayout(layout)

        self.model = QSqlTableModel(db=db)

        self.table.setModel(self.model)

        self.model.setTable("Track")
        self.model.select()

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(container)

    def update_filter(self, s):
        filter_str = 'Name LIKE "%{}%"'.format(s)
        self.model.setFilter(filter_str)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

Figure 152. Filtering the tracks table on the name.

 

이것은 SQL 주입에 취약합니다.

이것이 작동하는 동안, 이것은 사용자가 유효하지 않거나 악의적 SQL 문을 구성 있기 때문에 테이블에서 검색 수있게하는 나쁜 방법입니다. 예를 들어 검색 상자에 "라는 단일 문자를 입력하면 필터링이 작동하지 않고 앱을 다시 시작할 때까지 다시 작동하지 않습니다.

 

이는 잘못된 SQL 문을 만들었 때문입니다.

'name LIKE "%"%"'

문제를 해결하는 이상적인 방법은 매개 변수가 있는 쿼리를 사용하여 입력의 이스케이프를 데이터베이스로 남겨두고 위험하거나 잘못된 형식이 전달되지 않도록 하는 것입니다. 그러나 이것은 Qt 필터 인터페이스에서는 불가능하며 문자열 전달할 있습니다.

 

간단한 일반 텍스트 검색의 경우 문자열에서 영숫자가 아닌 문자 또는 공백 문자를 간단히 제거 있습니다. 이것이 적절한지 여부는 사용 사례에 따라 다릅니다.

import re

s = re.sub('[\W_]+', '', s)

query = 'field="%s"' % s

예제에서 필터 메서드에 넣으면 다음 코드를 얻을 있습니다.

 

databases/tableview_tablemodel_filter_clean.py

import os
import re
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import (
    QApplication,
    QLineEdit,
    QMainWindow,
    QTableView,
    QVBoxLayout,
    QWidget,
)

basedir = os.path.dirname(__file__)
db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        container = QWidget()
        layout = QVBoxLayout()

        self.search = QLineEdit()
        self.search.textChanged.connect(self.update_filter)
        self.table = QTableView()

        layout.addWidget(self.search)
        layout.addWidget(self.table)
        container.setLayout(layout)

        self.model = QSqlTableModel(db=db)

        self.table.setModel(self.model)

        self.model.setTable("Track")
        self.model.select()

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(container)

    # tag::filter[]
    def update_filter(self, s):
        s = re.sub("[\W_]+", "", s)
        filter_str = 'Name LIKE "%{}%"'.format(s)
        self.model.setFilter(filter_str)

    # end::filter[]


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

Try running the example again, and entering " — and any other garbage you can think of. You should find that the search continues to work.

 

Displaying related data with QSqlRelationalTableModel

이전 예제에서는 QSqlTableModel 사용하여 단일 테이블의 데이터를 표시했습니다. 그러나 관계형 데이터베이스에서 테이블은 다른 테이블과 관계를 가질 있으며 관련 데이터를 인라인으로 있으면 유용한 경우가 많습니다. 

관계형 데이터베이스의 관계는 외래 키를 통해 처리됩니다. 이들은 테이블의 열에 저장되는 (일반적으로) 숫자 값으로, 다른 테이블의 행에 대한 기본 키를 참조합니다.

 

예제 tracks 테이블에서 외래 키의 예는 album_id 또는 genre_id입니다. 앨범 장르 테이블의 레코드를 가리키는 숫자 값입니다. 이러한 값을 사용자에게 표시하는 것은 (1, 2, 3 ) 자체가 의미가 없기 때문에 도움이되지 않습니다. 

좋은 것은 앨범 이름이나 장르를 가져와 테이블보기에 표시하는 것입니다.   이를 위해 QSqlRelationalTableModel 사용할 있습니다. 

모델의 설정은 이전 모델과 동일합니다. 관계를 정의하기 위해 인덱스를 전달하는 .setRelation() QSqlRelation 객체를 호출합니다

from PyQt6.QtSql import QSqlRelation, QSqlRelationalTableModel

self.model = QSqlRelationalTableModel(db=db)

relation = QSqlRelation('<related_table>', '<related_table_foreign_key_column',    '<column_to_display>')

self.model.setRelation(<column>,  relation)

QSqlRelation 객체는 먼저 데이터를 가져올 관련 테이블, 해당 테이블의 외래 , 마지막으로 데이터를 가져올 열의 가지 인수를 허용합니다. 

테스트 데이터베이스 트랙 테이블의 경우 다음은 앨범 ID, media_type ID 장르 ID(각각 3, 4, 5) 대한 관련 테이블에서 데이터를 가져옵니다.

 

databases/tableview_relationalmodel.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import (
    QSqlDatabase,
    QSqlRelation,
    QSqlRelationalTableModel,
)
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlRelationalTableModel(db=db)

        self.table.setModel(self.model)

        # tag::setRelation[]
        self.model.setTable("Track")
        self.model.setRelation(
            2, QSqlRelation("Album", "AlbumId", "Title")
        )
        self.model.setRelation(
            3, QSqlRelation("MediaType", "MediaTypeId", "Name")
        )
        self.model.setRelation(
            4, QSqlRelation("Genre", "GenreId", "Name")
        )
        self.model.select()
        # end::setRelation[]

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

실행하면 개의 _id 열이 관련 테이블에서 가져온 데이터로 대체 것을 있습니다. 열은 충돌하지 않거나 이름이 구성된 경우 관련 필드의 이름을 사용합니다.

Figure 153. Displaying data from related fields.

 

Using QSqlRelationalDelegate to edit related fields.

QSqlRelationalTableModel에서 필드를 편집하려고 하면 문제가 발생합니다기본 테이블(여기서는 트랙) 필드를 편집할 있지만 관련 필드(: 앨범 제목) 대한 편집 내용은 저장되지 않습니다. 이러한 필드는 현재 데이터에 대한 보기일 뿐입니다.

 

관련 필드의 유효한 값은 관련 테이블의 값에 의해 제한되며, 많은 선택을 하려면 관련 테이블에 다른 행을 추가해야 합니다. 옵션이 제한되어 있기 때문에 QComboBox 선택 항목을 표시하는 것이 좋습니다. Qt에는이 조회 표시를 수행 수있는 모델 항목 대리자 QSqlRelationalDelegate 함께 제공됩니다.

 

databases/tableview_relationalmodel_delegate.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import (
    QSqlDatabase,
    QSqlRelation,
    QSqlRelationalDelegate,
    QSqlRelationalTableModel,
)
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlRelationalTableModel(db=db)

        self.table.setModel(self.model)

        # tag::setRelation[]
        self.model.setTable("Track")
        self.model.setRelation(
            2, QSqlRelation("Album", "AlbumId", "Title")
        )
        self.model.setRelation(
            3, QSqlRelation("MediaType", "MediaTypeId", "Name")
        )
        self.model.setRelation(
            4, QSqlRelation("Genre", "GenreId", "Name")
        )

        delegate = QSqlRelationalDelegate(self.table)
        self.table.setItemDelegate(delegate)

        self.model.select()
        # end::setRelation[]

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

대리자는 관계형 필드에 대한 매핑을 자동으로 처리합니다. QTableView 인스턴스를 전달하는 대리자를 만든 다음 모델에 결과 대리자를 설정하면 모든 것이 자동으로 처리됩니다.

 

이것을 실행하면 관련 필드를 편집 드롭 다운이 표시됩니다.

Figure 154. Making relatable fields editable through a drop-down with QSqlRelationalDelegate

 

 

Generic queries with QSqlQueryModel

지금까지 우리는 QTableView 가지 선택적 필터링 정렬을 통해 전체 데이터베이스 테이블을 표시했습니다. 그러나 Qt QSqlQueryModel 사용하여보다 복잡한 쿼리를 표시 수도 있습니다. 부분에서는 QSqlQueryModel 사용하여 먼저 간단한 단일 테이블 쿼리로 시작한 다음 관계형 매개 변수화 쿼리로 시작하여 SQL 쿼리를 표시하는 방법을 살펴 보겠습니다.

 

모델을 사용하여 쿼리하는 프로세스는 약간 다릅니다. 데이터베이스를 모델 생성자에 전달하는 대신 여기서는 데이터베이스 연결을 가져 오는 QSqlQuery 객체를 만든 다음이를 모델에 전달합니다.

query = QSqlQuery("SELECT name, composer FROM track ", db=db)

, 단일 QSqlQueryModel 사용하고 원하는 경우 다른 데이터베이스에서 쿼리를 수행 있습니다. 쿼리의 전체 작업 예제는 다음과 같습니다.

 

databases/tableview_querymodel.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlQueryModel()
        self.table.setModel(self.model)

        query = QSqlQuery("SELECT Name, Composer FROM track ", db=db)

        self.model.setQuery(query)

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

Figure 155. Performing a simple query.

번째 예제에서는 트랙 테이블에 대해 매우 간단한 쿼리를 수행했으며 해당 테이블에서 개의 필드만 반환했습니다. 그러나 QSqlQuery 객체는 쿼리를 수정하기 위해 값을 전달할 수있는 교차 테이블 조인 매개 변수화 쿼리를 포함하여보다 복잡한 쿼리에 사용할 있습니다.

 

매개 변수가 있는 쿼리는 SQL 삽입 공격으로부터 앱을 보호합니다.

 

다음 예제에서는 간단한 쿼리를 확장하여 앨범 테이블에 관련 조회를 추가합니다. 또한 앨범 테이블에 대한 포함 검색에 사용되는 album_title 매개 변수를 바인딩합니다.

 

databases/tableview_querymodel_parameter.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.table = QTableView()

        self.model = QSqlQueryModel()
        self.table.setModel(self.model)

        query = QSqlQuery(db=db)
        query.prepare(
            "SELECT Name, Composer, Album.Title FROM Track "
            "INNER JOIN Album ON Track.AlbumId = Album.AlbumId "
            "WHERE Album.Title LIKE '%' || :album_title || '%' "
        )
        query.bindValue(":album_title", "Sinatra")
        query.exec()

        self.model.setQuery(query)
        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

이제 쿼리에 매개 변수를 추가하려고하므로 생성 QSqlQuery 전달할 없습니다. 이렇게하면 매개 변수 교체없이 즉시 실행됩니다. 대신 이제 쿼리를 .prepare() 전달하여 드라이버에 쿼리에서 매개 변수를 식별하고 값을 기다리도록 지시해야 합니다.

 

 

다음으로 .bindValue() 사용하여 매개 변수를 바인딩하고 마지막으로 query.exec() 사용하여 실제로 데이터베이스에서 쿼리를 수행합니다.

 

매개 변수가 있는 쿼리는 다음 SQL 동일합니다.

SELECT Name, Composer, Album.Title FROM Track

INNER JOIN Album ON Track.AlbumId = Album.AlbumId

WHERE Album.Title LIKE '%Sinatra%'

This gives the following result —

Figure 156. The result of the parameterised query.

 

마지막 예에서는 트랙 제목, 아티스트 앨범 제목에 대한 검색 필드 개의 검색 필드를 추가합니다. 이러한 신호의 .textChanged 신호를 쿼리의 매개 변수를 업데이트하는 사용자 지정 메서드에 연결합니다.

 

databases/tableview_querymodel_search.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLineEdit,
    QMainWindow,
    QTableView,
    QVBoxLayout,
    QWidget,
)

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        container = QWidget()
        layout_search = QHBoxLayout()

        self.track = QLineEdit()
        self.track.setPlaceholderText("Track name...")
        self.track.textChanged.connect(self.update_query)

        self.composer = QLineEdit()
        self.composer.setPlaceholderText("Artist name...")
        self.composer.textChanged.connect(self.update_query)

        self.album = QLineEdit()
        self.album.setPlaceholderText("Album name...")
        self.album.textChanged.connect(self.update_query)

        layout_search.addWidget(self.track)
        layout_search.addWidget(self.composer)
        layout_search.addWidget(self.album)

        layout_view = QVBoxLayout()
        layout_view.addLayout(layout_search)

        self.table = QTableView()

        layout_view.addWidget(self.table)

        container.setLayout(layout_view)

        self.model = QSqlQueryModel()
        self.table.setModel(self.model)

        self.query = QSqlQuery(db=db)

        self.query.prepare(
            "SELECT Name, Composer, Album.Title FROM Track "
            "INNER JOIN Album ON Track.AlbumId=Album.AlbumId WHERE "
            "Track.Name LIKE '%' || :track_name || '%' AND "
            "Track.Composer LIKE '%' || :track_composer || '%' AND "
            "Album.Title LIKE '%' || :album_title || '%'"
        )

        self.update_query()

        self.setMinimumSize(QSize(1024, 600))
        self.setCentralWidget(container)

    def update_query(self, s=None):

        # Get the text values from the widgets.
        track_name = self.track.text()
        track_composer = self.composer.text()
        album_title = self.album.text()

        self.query.bindValue(":track_name", track_name)
        self.query.bindValue(":track_composer", track_composer)
        self.query.bindValue(":album_title", album_title)

        self.query.exec()
        self.model.setQuery(self.query)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

 

옵션을 실행하면 필드를 독립적으로 사용하여 데이터베이스를 검색할 있으며 검색 쿼리가 변경될 때마다 결과가 자동으로 업데이트됩니다.

Figure 157. The result of the multi-parameter search query.

 

QDataWidgetMapper

지금까지의 모든 예제에서 QTableView 를 사용하여 데이터베이스의 출력 데이터를 테이블에 표시했습니다. 이것은 종종 데이터를 보는 데 의미가 있지만 데이터 입력 또는 편집의 경우 일반적으로 입력을 입력하고 탭할 수 있는 형식으로 표시하는 것이 좋습니다.

 

이를 CRUD(만들기, 읽기, 업데이트 및 삭제) 작업 및 인터페이스라고 합니다. 

 

The full working example is shown below.

 

databases/widget_mapper.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel
from PyQt6.QtWidgets import (
    QApplication,
    QComboBox,
    QDataWidgetMapper,
    QDoubleSpinBox,
    QFormLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QSpinBox,
    QWidget,
)

basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join(basedir, "chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        form = QFormLayout()

        self.track_id = QSpinBox()
        self.track_id.setRange(0, 2147483647)
        self.track_id.setDisabled(True)
        self.name = QLineEdit()
        self.album = QComboBox()
        self.media_type = QComboBox()
        self.genre = QComboBox()
        self.composer = QLineEdit()

        self.milliseconds = QSpinBox()
        self.milliseconds.setRange(0, 2147483647)  # <1>
        self.milliseconds.setSingleStep(1)

        self.bytes = QSpinBox()
        self.bytes.setRange(0, 2147483647)
        self.bytes.setSingleStep(1)

        self.unit_price = QDoubleSpinBox()
        self.unit_price.setRange(0, 999)
        self.unit_price.setSingleStep(0.01)
        self.unit_price.setPrefix("$")

        form.addRow(QLabel("Track ID"), self.track_id)
        form.addRow(QLabel("Track name"), self.name)
        form.addRow(QLabel("Composer"), self.composer)
        form.addRow(QLabel("Milliseconds"), self.milliseconds)
        form.addRow(QLabel("Bytes"), self.bytes)
        form.addRow(QLabel("Unit Price"), self.unit_price)

        self.model = QSqlTableModel(db=db)

        self.mapper = QDataWidgetMapper()  # <2>
        self.mapper.setModel(self.model)

        self.mapper.addMapping(self.track_id, 0)  # <3>
        self.mapper.addMapping(self.name, 1)
        self.mapper.addMapping(self.composer, 5)
        self.mapper.addMapping(self.milliseconds, 6)
        self.mapper.addMapping(self.bytes, 7)
        self.mapper.addMapping(self.unit_price, 8)

        self.model.setTable("Track")
        self.model.select()  # <4>

        self.mapper.toFirst()  # <5>

        self.setMinimumSize(QSize(400, 400))

        widget = QWidget()
        widget.setLayout(form)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

①Widgets must be configured to accept all valid values from the table.

②One QDataWidgetMapper for all widgets.

③Widgets are mapped to _columns.

④Perform the select to populate the model.

Step the mapper forward to the first record.

 

예제를 실행하면 다음 창이 표시됩니다. self.mapper.toFirst() 호출은 테이블의 번째 레코드를 선택하고 맵핑된 위젯에 표시됩니다.

Figure 158. Viewing a record via mapped widgets.

현재 보고 있는 레코드를 변경하거나 레코드에 대한 변경 내용을 저장할 없습니다. 이를 가능하게하기 위해 3 개의 버튼을 추가 있습니다 - 레코드를 통해 이전 다음 찾아보기를 위해 각각 하나씩, 데이터베이스에 변경 사항을 커밋하기 위해 저장합니다. 이를 위해 일부 QPushButton 위젯을 매퍼 슬롯에 연결할 있습니다.

.toPrevious, .toNext .submit.

 

init 메서드의 끝을 업데이트하여 다음을 추가하고 위젯을 기존 레이아웃에 추가합니다.

 

databases/widget_mapper_controls.py

self.setMinimumSize(QSize(400,   400)) 
controls = QHBoxLayout()

prev_rec  =  QPushButton("Previous") 
prev_rec.clicked.connect(self.mapper.toPrevious) 

next_rec  =  QPushButton("Next") 
next_rec.clicked.connect(self.mapper.toNext) 

save_rec = QPushButton("Save Changes") 
save_rec.clicked.connect(self.mapper.submit) 

controls.addWidget(prev_rec) 
controls.addWidget(next_rec) 
controls.addWidget(save_rec) 

layout.addLayout(form) 
layout.addLayout(controls) 

widget = QWidget() 
widget.setLayout(layout) 
self.setCentralWidget(widget)

 

databases/widget_mapper_controls.py

import os
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtSql import QSqlDatabase, QSqlTableModel

from PyQt6.QtWidgets import (
    QApplication,
    QComboBox,
    QDataWidgetMapper,
    QDoubleSpinBox,
    QFormLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QPushButton,
    QSpinBox,
    QVBoxLayout,
    QWidget,
)


basedir = os.path.dirname(__file__)

db = QSqlDatabase("QSQLITE")
db.setDatabaseName(os.path.join("chinook.sqlite"))
db.open()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        layout = QVBoxLayout()

        form = QFormLayout()

        self.track_id = QSpinBox()
        self.track_id.setRange(0, 2147483647)
        self.track_id.setDisabled(True)
        self.name = QLineEdit()
        self.album = QComboBox()
        self.media_type = QComboBox()
        self.genre = QComboBox()
        self.composer = QLineEdit()

        self.milliseconds = QSpinBox()
        self.milliseconds.setRange(0, 2147483647)
        self.milliseconds.setSingleStep(1)

        self.bytes = QSpinBox()
        self.bytes.setRange(0, 2147483647)
        self.bytes.setSingleStep(1)

        self.unit_price = QDoubleSpinBox()
        self.unit_price.setRange(0, 999)
        self.unit_price.setSingleStep(0.01)
        self.unit_price.setPrefix("$")

        form.addRow(QLabel("Track ID"), self.track_id)
        form.addRow(QLabel("Track name"), self.name)
        form.addRow(QLabel("Composer"), self.composer)
        form.addRow(QLabel("Milliseconds"), self.milliseconds)
        form.addRow(QLabel("Bytes"), self.bytes)
        form.addRow(QLabel("Unit Price"), self.unit_price)

        self.model = QSqlTableModel(db=db)

        self.mapper = QDataWidgetMapper()
        self.mapper.setModel(self.model)

        self.mapper.addMapping(self.track_id, 0)
        self.mapper.addMapping(self.name, 1)
        self.mapper.addMapping(self.composer, 5)
        self.mapper.addMapping(self.milliseconds, 6)
        self.mapper.addMapping(self.bytes, 7)
        self.mapper.addMapping(self.unit_price, 8)

        self.model.setTable("Track")
        self.model.select()

        self.mapper.toFirst()

        # tag::controls[]
        self.setMinimumSize(QSize(400, 400))

        controls = QHBoxLayout()

        prev_rec = QPushButton("Previous")
        prev_rec.clicked.connect(self.mapper.toPrevious)

        next_rec = QPushButton("Next")
        next_rec.clicked.connect(self.mapper.toNext)

        save_rec = QPushButton("Save Changes")
        save_rec.clicked.connect(self.mapper.submit)

        controls.addWidget(prev_rec)
        controls.addWidget(next_rec)
        controls.addWidget(save_rec)

        layout.addLayout(form)
        layout.addLayout(controls)

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)
        # end::controls[]


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

 

이제 트랙 테이블에서 레코드를 찾아보고 트랙 데이터를 변경한 다음 이러한 변경 내용을 데이터베이스에 전송할 있습니다. 예제의 전체 소스 코드는 소스 코드의 데이터베이스/widget_mapper_controls.py에서 사용할 있습니다.

Figure 159. Viewing records, with previous/next controls and save to submit.

 

Authenticating with QSqlDatabase

지금까지의 예에서는 SQLite 데이터베이스 파일을 사용했습니다. 그러나 종종 원격 SQL 서버에 연결하려고 것입니다. 이를 위해서는 호스트 이름 (데이터베이스가있는 위치) 사용자 이름 암호 (적절한 경우) 포함한 가지 추가 매개 변수가 필요합니다.

# Create database connection.

db = QSqlDatabase('<driver>')

db.setHostName('<localhost>')

db.setDatabaseName('<databasename>')

db.setUserName('<username>')

db.setPassword('<password>') db.open()

참고: <driver> 값은 다음 ['QSQLITE', '<driver>QMYSQL', 'QMYSQL3', 'QODBC', 'QODBC3', 'QPSQL', 'QPSQL7'] 하나일 있습니다. 목록을 가져오려면 시스템이 QSqlDatabase.drivers() 실행합니다.

 

그거에요! 연결이 설정되면 모델은 이전과 동일하게 작동합니다.

'PyQt5_' 카테고리의 다른 글

Creating Custom Widgets  (0) 2023.03.13
Bitmap Graphics in Qt  (0) 2023.03.13
Tabular data in ModelViews, with numpy & pandas  (0) 2023.03.13
The Model View Architecture — Model View Controller  (0) 2023.03.13
Qt Style Sheets (QSS)  (0) 2023.03.13

댓글