Bài 52: Kỹ thuật ORM trong Python với Cơ sở dữ liệu MySQL Server (p3)

Như vậy các bạn đã sử dụng thành thạo kỹ thuật ORM trong Python với Cơ sở dữ liệu MySQL Server thông qua bài 50bài 51. Toàn bộ các chức năng CRUD trên các bảng dữ liệu các bạn đã có thể xem, thêm, sửa xóa với ORM technique, cũng như xử lý dữ liệu dạng Master-Details với các mối quan hệ has_manybelongs_to.

Bài này Tui tiếp tục hướng dẫn các bạn cách sử dụng ORM để tương tác dữ liệu với giao diện người dùng trong cơ sở dữ liệu Sakila đã đề cập ở bài học trước, chúng ta sẽ sử dụng bảng Customer, bảng Address và bảng Rental. Sakila là cơ sở dữ liệu mẫu có nhiều relationship khi chúng ta cài đặt My SQL Server, nên các bạn có thể sử dụng trực tiếp từ máy của bạn. Giao diện tương tác được thiết kế như đưới đây:

Phần mềm trên bao gồm các chức năng sử dụng kỹ thuật ORM được liệt kê như dưới đây:

(1) Nạp danh sách Customer vào QTableWidget

(2) Xem chi tiết Customer, khi người dùng nhấn chọn Customer nào trong QTableWidget thì thông tin chi tiết của Customer sẽ hiển thị và các ô QLineEdit ở bên phải màn hình

(3) Xem danh sách Rentals của Customer, khi người dùng nhấn chọn Customer nào trong QTableWidget thì danh sách Rentals của Customer ngày sẽ được nạp vào QTableWidget ở bên dưới màn hình.

(4) Chức năng “Clear”: Khi người dùng nhấn vào nút lệnh này thì toàn bộ dữ liệu trong phần Customer Details sẽ được xóa rỗng để người sử dụng nhập mới dữ liệu Customer

(5) Chức năng “Insert“: Khi người dùng nhấn vào nút lệnh này thì Customer sẽ được thêm mới vào trong cơ sở dữ liệu Sakila.

(6) Chức năng “Update“: Khi người dùng nhấn vào nút lệnh này thì Customer sẽ được cập nhật dữ liệu vào cơ sở dữ liệu Sakila.

(7) Chức năng “Delete“: Khi người dùng nhấn vào nút lệnh này thì Customer sẽ được xóa khỏi cơ sở dữ liệu Sakila. Có xác nhận xóa hay không

(8) Chức năng “Exit”: Xác nhận thoát phần mềm hay không.

Dưới đây là 3 bảng Customer, Address và Rental trong cơ sở dữ liệu mẫu Sakila khi bạn cài MySQL Server:

Lưu ý các mã lệnh cấu hình chuỗi kết nối cơ sở dữ liệu nó lệ thuộc vào máy tính của bạn, và cũng tương tự như các bài học trước.

Ta tiến hành thực hiện các bước sau để hoàn tất dự án:

Bước 0: Tạo dự án “sakila_orm_gui” trong Pycharm có cấu trúc như dưới đây:

Mô tả cho các thư mục, tập tin của dự án như sau:

  • Thư mục “Classes“: Chứa các tập tin các mã lệnh để kết nối cơ sở dữ liệu (DatabaseConnection.py), tạo các classes mapping cho các bảng Customer, Address và Rental (ClassMapping.py).
  • Thư mục “Images“: Thư mục chứa hình ảnh, icon cho phần mềm
  • Thư mục “UI“: Thư mục chứa các giao diện thiết kế phần mềm (MainWindow.ui), code generate Python cho giao diện(MainWindow.py), code Python kế thừa để xử lý tương tác người dùng(MainWindowEx.py)
  • Tập tin “MyApp.py“: Tập tin chứa mã lệnh để thực thi chương trình

Bước 1: Viết mã lệnh “DatabaseConnection.py

from orm import Table

#configuration JSON for connection MySQL
CONFIG={
    'host': 'localhost',
    'port': 3306,
    'user': 'root',
    'password': '@Obama123',
    'database': 'sakila'
}
#connect to database from JSON configuration
Table.connect(config_dict=CONFIG)

Mã lệnh trên cấu hình chuỗi kết nối tới MySQL Server, tùy thuộc vào sự cài đặt và cấu hình phần mềm của bạn mà chuỗi kết nối này sẽ khác nhau.

Sau khi có chuỗi kết nối, ta gọi phương thức connect () của Table.

Bước 2: Tạo các classes mapping, chúng được khai báo và tạo các relationship trong “ClassMapping.py“:

from orm import Table, has_many, belongs_to

class Customer(Table):
    table_name = 'customer'

    relations = [
        has_many(name='rentals', _class='Rental', foreign_key='customer_id'),
        has_many(name='addresses', _class='Address', foreign_key='address_id')
    ]
class Rental(Table):
    table_name = 'rental'

    relations = [
        belongs_to(name='customer', _class='Customer', foreign_key='customer_id',primary_key="customer_id")
    ]
class Address(Table):
    table_name = 'address'

Bảng customer được khai báo Mapping thành lớp Customer, bảng address được khai báo Mapping thành lớp Address. Customer và Address có mối quan hệ: 1 Customer có nhiều Address theo thiết kế. Trong lớp Customer các bạn thấy Tui khai báo relations has_many:

has_many(name='addresses', _class='Address', foreign_key='address_id')
  • name “addresses”: Đây chính là phương thức addresses() trả về danh sách Address
  • _class=”Address”: Tức là khi gọi hàm addresses() chương trình sẽ trả về danh sách đối tượng có kiểu Address
  • foreign_key=”address_id”: Dựa vào khóa ngoại này để ứng với 1 Customer sẽ truy suất ra được danh sách Address, và theo quan sát dữ liệu Sakila thì tuy thiết kế 1 Customer có nhiều Address nhưng dữ liệu chỉ có 1 Address, tức là mảng addresses() khi gọi hàm này thì ta lấy phần tử đầu tiên chính là 1 Address của Customer mà ta quan tâm.

Ngoài ra Customer cũng có mối quan hệ với Rental. 1 Customer có nhiều Rentals, và 1 Rental thuộc về một Customer.

Ta thấy relations khai báo trong Customer liên quan tới Rental:

has_many(name='rentals', _class='Rental', foreign_key='customer_id')
  • name “rentals”: Đây chính là phương thức rentals() trả về danh sách Rentals
  • _class=”Rental”: Tức là khi gọi hàm rentals() chương trình sẽ trả về danh sách đối tượng có kiểu Rental
  • foreign_key=”customer_id”: Dựa vào khóa ngoại này để ứng với 1 Customer sẽ truy suất ra được danh sách Rentals.

Lớp Rental cũng có relations thể hiện: 1 Rental thuộc về một Customer, ta có khai báo mối quan hệ belongs_to:

belongs_to(name='customer', _class='Customer', foreign_key='customer_id',primary_key="customer_id")
  • name “customer”: Đây chính là phương thức customer() tả 1 một đối tượng Customer
  • _class=”Customer”: Tức là khi gọi hàm customer() chương trình sẽ trả về đối tượng có kiểu Customer
  • foreign_key=”customer_id”: Dựa vào khóa ngoại này để ứng với 1 Customer sẽ truy suất ra được danh sách Rentals.
  • primary_key=”customer_id”: Dựa vào khóa này, thì từ Rental sẽ suy ra Customer đang sở hữu Rental này.

Bước 3: Thiết kế giao diện tương tác người dùng MainWindow.ui, sử dụng chức năng tích hợp PyQt6, Qt Designer trong Pycharm để tạo giao diện MainWindow.ui trong thư mục UI này, cấu trúc như sau:

Các bạn thiết kế giao diện và đặt tên cho các Widget như hình minh họa ở trên.

Bước 4: Generate Python code MainWindow.py cho giao diện MainWindow.ui, Chức năng Generate đã được học ở những bài đầu tiên, bạn chưa biết thì nhớ xem lại từ đầu:

# Form implementation generated from reading ui file 'E:\Elearning\sakila_orm_gui\UI\MainWindow.ui'
#
# Created by: PyQt6 UI code generator 6.6.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(727, 682)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("E:\\Elearning\\sakila_orm_gui\\UI\\../Images/ic_logo.jpg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        MainWindow.setWindowIcon(icon)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(10, 50, 391, 281))
        self.groupBox.setStyleSheet("background-color: rgb(255, 239, 240);")
        self.groupBox.setObjectName("groupBox")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox)
        self.verticalLayout.setObjectName("verticalLayout")
        self.tableWidgetCustomer = QtWidgets.QTableWidget(parent=self.groupBox)
        self.tableWidgetCustomer.setObjectName("tableWidgetCustomer")
        self.tableWidgetCustomer.setColumnCount(3)
        self.tableWidgetCustomer.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetCustomer.setHorizontalHeaderItem(2, item)
        self.verticalLayout.addWidget(self.tableWidgetCustomer)
        self.groupBox_2 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_2.setGeometry(QtCore.QRect(10, 400, 701, 241))
        self.groupBox_2.setStyleSheet("background-color: rgb(244, 246, 255);")
        self.groupBox_2.setObjectName("groupBox_2")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox_2)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.tableWidgetRental = QtWidgets.QTableWidget(parent=self.groupBox_2)
        self.tableWidgetRental.setObjectName("tableWidgetRental")
        self.tableWidgetRental.setColumnCount(7)
        self.tableWidgetRental.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetRental.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetRental.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetRental.setHorizontalHeaderItem(2, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetRental.setHorizontalHeaderItem(3, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetRental.setHorizontalHeaderItem(4, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetRental.setHorizontalHeaderItem(5, item)
        item = QtWidgets.QTableWidgetItem()
        self.tableWidgetRental.setHorizontalHeaderItem(6, item)
        self.verticalLayout_3.addWidget(self.tableWidgetRental)
        self.groupBox_3 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_3.setGeometry(QtCore.QRect(410, 50, 301, 281))
        self.groupBox_3.setStyleSheet("background-color: rgb(226, 255, 254);")
        self.groupBox_3.setObjectName("groupBox_3")
        self.label = QtWidgets.QLabel(parent=self.groupBox_3)
        self.label.setGeometry(QtCore.QRect(20, 30, 81, 16))
        self.label.setObjectName("label")
        self.lineEditCustomerId = QtWidgets.QLineEdit(parent=self.groupBox_3)
        self.lineEditCustomerId.setGeometry(QtCore.QRect(100, 30, 191, 22))
        self.lineEditCustomerId.setStyleSheet("background-color: rgb(255, 255, 0);")
        self.lineEditCustomerId.setObjectName("lineEditCustomerId")
        self.lineEditFirstName = QtWidgets.QLineEdit(parent=self.groupBox_3)
        self.lineEditFirstName.setGeometry(QtCore.QRect(100, 60, 191, 22))
        self.lineEditFirstName.setStyleSheet("background-color: rgb(255, 255, 0);")
        self.lineEditFirstName.setObjectName("lineEditFirstName")
        self.label_2 = QtWidgets.QLabel(parent=self.groupBox_3)
        self.label_2.setGeometry(QtCore.QRect(20, 60, 71, 16))
        self.label_2.setObjectName("label_2")
        self.lineEditLastName = QtWidgets.QLineEdit(parent=self.groupBox_3)
        self.lineEditLastName.setGeometry(QtCore.QRect(100, 90, 191, 22))
        self.lineEditLastName.setStyleSheet("background-color: rgb(255, 255, 0);")
        self.lineEditLastName.setObjectName("lineEditLastName")
        self.label_3 = QtWidgets.QLabel(parent=self.groupBox_3)
        self.label_3.setGeometry(QtCore.QRect(20, 90, 71, 16))
        self.label_3.setObjectName("label_3")
        self.lineEditEmail = QtWidgets.QLineEdit(parent=self.groupBox_3)
        self.lineEditEmail.setGeometry(QtCore.QRect(100, 120, 191, 22))
        self.lineEditEmail.setStyleSheet("background-color: rgb(255, 255, 0);")
        self.lineEditEmail.setObjectName("lineEditEmail")
        self.label_4 = QtWidgets.QLabel(parent=self.groupBox_3)
        self.label_4.setGeometry(QtCore.QRect(20, 120, 61, 16))
        self.label_4.setObjectName("label_4")
        self.lineEditAddress = QtWidgets.QLineEdit(parent=self.groupBox_3)
        self.lineEditAddress.setGeometry(QtCore.QRect(100, 150, 191, 22))
        self.lineEditAddress.setStyleSheet("background-color: rgb(255, 255, 0);")
        self.lineEditAddress.setObjectName("lineEditAddress")
        self.label_5 = QtWidgets.QLabel(parent=self.groupBox_3)
        self.label_5.setGeometry(QtCore.QRect(20, 150, 71, 16))
        self.label_5.setObjectName("label_5")
        self.checkBoxActive = QtWidgets.QCheckBox(parent=self.groupBox_3)
        self.checkBoxActive.setGeometry(QtCore.QRect(100, 190, 81, 20))
        self.checkBoxActive.setObjectName("checkBoxActive")
        self.lineEditCreateDate = QtWidgets.QLineEdit(parent=self.groupBox_3)
        self.lineEditCreateDate.setGeometry(QtCore.QRect(100, 220, 191, 22))
        self.lineEditCreateDate.setStyleSheet("background-color: rgb(255, 255, 0);")
        self.lineEditCreateDate.setObjectName("lineEditCreateDate")
        self.label_6 = QtWidgets.QLabel(parent=self.groupBox_3)
        self.label_6.setGeometry(QtCore.QRect(20, 220, 81, 16))
        self.label_6.setObjectName("label_6")
        self.lineEditLastUpdate = QtWidgets.QLineEdit(parent=self.groupBox_3)
        self.lineEditLastUpdate.setGeometry(QtCore.QRect(100, 250, 191, 22))
        self.lineEditLastUpdate.setStyleSheet("background-color: rgb(255, 255, 0);")
        self.lineEditLastUpdate.setObjectName("lineEditLastUpdate")
        self.label_7 = QtWidgets.QLabel(parent=self.groupBox_3)
        self.label_7.setGeometry(QtCore.QRect(20, 250, 81, 16))
        self.label_7.setObjectName("label_7")
        self.groupBox_4 = QtWidgets.QGroupBox(parent=self.centralwidget)
        self.groupBox_4.setGeometry(QtCore.QRect(10, 330, 701, 71))
        self.groupBox_4.setStyleSheet("background-color: rgb(255, 248, 239);")
        self.groupBox_4.setObjectName("groupBox_4")
        self.pushButtonClear = QtWidgets.QPushButton(parent=self.groupBox_4)
        self.pushButtonClear.setGeometry(QtCore.QRect(20, 20, 93, 41))
        self.pushButtonClear.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("E:\\Elearning\\sakila_orm_gui\\UI\\../Images/ic_clear.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonClear.setIcon(icon1)
        self.pushButtonClear.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonClear.setObjectName("pushButtonClear")
        self.pushButtonInsert = QtWidgets.QPushButton(parent=self.groupBox_4)
        self.pushButtonInsert.setGeometry(QtCore.QRect(140, 20, 93, 41))
        self.pushButtonInsert.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("E:\\Elearning\\sakila_orm_gui\\UI\\../Images/ic_save.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonInsert.setIcon(icon2)
        self.pushButtonInsert.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonInsert.setObjectName("pushButtonInsert")
        self.pushButtonUpdate = QtWidgets.QPushButton(parent=self.groupBox_4)
        self.pushButtonUpdate.setGeometry(QtCore.QRect(270, 20, 93, 41))
        self.pushButtonUpdate.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon3 = QtGui.QIcon()
        icon3.addPixmap(QtGui.QPixmap("E:\\Elearning\\sakila_orm_gui\\UI\\../Images/ic_update.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonUpdate.setIcon(icon3)
        self.pushButtonUpdate.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonUpdate.setObjectName("pushButtonUpdate")
        self.pushButtonDelete = QtWidgets.QPushButton(parent=self.groupBox_4)
        self.pushButtonDelete.setGeometry(QtCore.QRect(390, 20, 93, 41))
        self.pushButtonDelete.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon4 = QtGui.QIcon()
        icon4.addPixmap(QtGui.QPixmap("E:\\Elearning\\sakila_orm_gui\\UI\\../Images/ic_delete.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonDelete.setIcon(icon4)
        self.pushButtonDelete.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonDelete.setObjectName("pushButtonDelete")
        self.pushButtonExit = QtWidgets.QPushButton(parent=self.groupBox_4)
        self.pushButtonExit.setGeometry(QtCore.QRect(600, 20, 93, 41))
        self.pushButtonExit.setStyleSheet("background-color: rgb(255, 170, 255);")
        icon5 = QtGui.QIcon()
        icon5.addPixmap(QtGui.QPixmap("E:\\Elearning\\sakila_orm_gui\\UI\\../Images/ic_shutdown.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
        self.pushButtonExit.setIcon(icon5)
        self.pushButtonExit.setIconSize(QtCore.QSize(32, 32))
        self.pushButtonExit.setObjectName("pushButtonExit")
        self.label_8 = QtWidgets.QLabel(parent=self.centralwidget)
        self.label_8.setGeometry(QtCore.QRect(210, 10, 381, 31))
        font = QtGui.QFont()
        font.setFamily("MS Shell Dlg 2")
        font.setPointSize(15)
        font.setBold(False)
        font.setItalic(False)
        font.setWeight(50)
        self.label_8.setFont(font)
        self.label_8.setStyleSheet("color: rgb(0, 0, 255);\n"
"font: 15pt \"MS Shell Dlg 2\";")
        self.label_8.setObjectName("label_8")
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 727, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)
        MainWindow.setTabOrder(self.tableWidgetCustomer, self.lineEditCustomerId)
        MainWindow.setTabOrder(self.lineEditCustomerId, self.lineEditFirstName)
        MainWindow.setTabOrder(self.lineEditFirstName, self.lineEditLastName)
        MainWindow.setTabOrder(self.lineEditLastName, self.lineEditEmail)
        MainWindow.setTabOrder(self.lineEditEmail, self.lineEditAddress)
        MainWindow.setTabOrder(self.lineEditAddress, self.checkBoxActive)
        MainWindow.setTabOrder(self.checkBoxActive, self.lineEditCreateDate)
        MainWindow.setTabOrder(self.lineEditCreateDate, self.lineEditLastUpdate)
        MainWindow.setTabOrder(self.lineEditLastUpdate, self.pushButtonClear)
        MainWindow.setTabOrder(self.pushButtonClear, self.pushButtonInsert)
        MainWindow.setTabOrder(self.pushButtonInsert, self.pushButtonUpdate)
        MainWindow.setTabOrder(self.pushButtonUpdate, self.pushButtonDelete)
        MainWindow.setTabOrder(self.pushButtonDelete, self.pushButtonExit)
        MainWindow.setTabOrder(self.pushButtonExit, self.tableWidgetRental)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Trần Duy Thanh - Sakila - ORM"))
        self.groupBox.setTitle(_translate("MainWindow", "List of Customers"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "Customer ID"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "First Name"))
        item = self.tableWidgetCustomer.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Last Name"))
        self.groupBox_2.setTitle(_translate("MainWindow", "List of Rental"))
        item = self.tableWidgetRental.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "Rental ID"))
        item = self.tableWidgetRental.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "Rental Date"))
        item = self.tableWidgetRental.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "Inventory ID"))
        item = self.tableWidgetRental.horizontalHeaderItem(3)
        item.setText(_translate("MainWindow", "Customer ID"))
        item = self.tableWidgetRental.horizontalHeaderItem(4)
        item.setText(_translate("MainWindow", "Return Date"))
        item = self.tableWidgetRental.horizontalHeaderItem(5)
        item.setText(_translate("MainWindow", "Staff ID"))
        item = self.tableWidgetRental.horizontalHeaderItem(6)
        item.setText(_translate("MainWindow", "Last Update"))
        self.groupBox_3.setTitle(_translate("MainWindow", "Customer Details:"))
        self.label.setText(_translate("MainWindow", "Customer Id:"))
        self.label_2.setText(_translate("MainWindow", "First Name:"))
        self.label_3.setText(_translate("MainWindow", "Last Name:"))
        self.label_4.setText(_translate("MainWindow", "Email:"))
        self.label_5.setText(_translate("MainWindow", "Address:"))
        self.checkBoxActive.setText(_translate("MainWindow", "Is Active"))
        self.label_6.setText(_translate("MainWindow", "Create Date:"))
        self.label_7.setText(_translate("MainWindow", "Last Update:"))
        self.groupBox_4.setTitle(_translate("MainWindow", "Actions for Customer:"))
        self.pushButtonClear.setText(_translate("MainWindow", "Clear"))
        self.pushButtonInsert.setText(_translate("MainWindow", "Insert"))
        self.pushButtonUpdate.setText(_translate("MainWindow", "Update"))
        self.pushButtonDelete.setText(_translate("MainWindow", "Delete"))
        self.pushButtonExit.setText(_translate("MainWindow", "Exit"))
        self.label_8.setText(_translate("MainWindow", "Sakila  - ORM  Demonstration"))

Bước 5: Viết mã lệnh kế thừa để xử lý sự kiện tương tác người dùng cho giao diện ở trên, đặt tên “MainWindowEx.py“:

Bước 5.1: Tạo MainWindowEx kế thừa từ Ui_MainWindow được generate ra ở bước trước, và Khai báo các thư viện:

from datetime import datetime
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QTableWidgetItem, QMessageBox
from UI.MainWindow import Ui_MainWindow
import Classes.DatabaseConnection
from Classes.ClassMapping import Customer, Rental, Address

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        pass

Các bạn quan sát các thư viện sử dụng ở trên, đặc biệt cần khai báo DatabaseConnection trước ClasssMapping. Vì cần kết nối Cơ sở dữ liệu trước khi tạo các mapping và mối quan hệ.

Bước 5.2: Khai báo hàm setupUi() để thiết lập giao diện cho phần mềm, đồng thời viết các signals và slots cho các Widget:

def setupUi(self, MainWindow):
    super().setupUi(MainWindow)
    self.MainWindow=MainWindow
    self.showAllCustomerOnQTableWidget()
    self.tableWidgetCustomer.itemSelectionChanged.connect(self.processItemSelection)
    self.pushButtonClear.clicked.connect(self.processClear)
    self.pushButtonInsert.clicked.connect(self.processInsert)
    self.pushButtonUpdate.clicked.connect(self.processUpdate)
    self.pushButtonDelete.clicked.connect(self.processDelete)
    self.pushButtonExit.clicked.connect(self.processExit)
def showWindow(self):
    self.MainWindow.show()

Bước 5.3: Viết hàm nạp toàn bộ Customer lên QTableWidget bằng hàm showAllCustomerOnQTableWidget(), khi khởi động phần mềm, chương trình sẽ dùng ORM để truy vấn và mapping hướng đối tượng Customer, sau đó chúng ta nạp lên giao diện:

def showAllCustomerOnQTableWidget(self):
    customers = Customer.all()
    self.tableWidgetCustomer.setRowCount(0)
    row = 0
    for cust in customers:
        row = self.tableWidgetCustomer.rowCount()
        self.tableWidgetCustomer.insertRow(row)
        self.tableWidgetCustomer.setItem(row, 0, QTableWidgetItem(str(cust.customer_id)))
        self.tableWidgetCustomer.setItem(row, 1, QTableWidgetItem(cust.first_name))
        self.tableWidgetCustomer.setItem(row, 2, QTableWidgetItem(cust.last_name))
        if cust.active==0:
            self.tableWidgetCustomer.item(row,0).setBackground(Qt.GlobalColor.red)
            self.tableWidgetCustomer.item(row, 1).setBackground(Qt.GlobalColor.red)
            self.tableWidgetCustomer.item(row, 2).setBackground(Qt.GlobalColor.red)

Hàm trên đọc danh sách customer bằng cách gọi hàm Customer.all()

sau đó dùng vòng lặp để nạp lên giao diện QTableWidget, và những Customer nào có active=0 thì tô nền đỏ.

Nếu sau khi hoàn thành phần mềm, ta chạy mã lệnh này lên ta có kết quả như giao diện dưới đây:

Quan sát giao diện trên ta thấy, Customer ID=16, First Name là SANDRA, LastName là MARTIN có tô nền đỏ vì Active=0.

Bước 5.4: Viết hàm xử lý sự kiện khi người dùng nhấn chọn Customer trong QTableWidget, chương trình sẽ thực hiện 2 nhiệm vụ:

  • Nhiệm vụ 1: Hiển thị chi tiết Customer và mục Customer Details ở bên phải màn hình
  • Nhiệm vụ 2: Hiển thị danh sách Rentals của Customer vào QTableWidget ở bên dưới màn hình
def processItemSelection(self):
    row = self.tableWidgetCustomer.currentRow()
    if row == -1:
        return
    customer_id=int(self.tableWidgetCustomer.item(row,0).text())
    #process for selected Customer details
    customer = Customer.find(customer_id)
    self.lineEditCustomerId.setText(str(customer.customer_id))
    self.lineEditFirstName.setText(customer.first_name)
    self.lineEditLastName.setText(customer.last_name)
    self.lineEditEmail.setText(customer.email)
    if customer.active==1:
        self.checkBoxActive.setChecked(True)
    else:
        self.checkBoxActive.setChecked(False)
    self.lineEditCreateDate.setText(str(customer.create_date))
    self.lineEditLastUpdate.setText(str(customer.last_update))
    #process for address table
    addresses=customer.addresses()
    address=addresses[0]
    self.lineEditAddress.setText(address.address)
    #process for rentals
    self.showAllRentalOnQTableWidget(customer)

Dưới đây là hàm showAllRentalOnQTableWidget(customer):

def showAllRentalOnQTableWidget(self,customer):
    rentals = customer.rentals()
    self.tableWidgetRental.setRowCount(0)
    row = 0
    for rental in rentals:
        row = self.tableWidgetRental.rowCount()
        self.tableWidgetRental.insertRow(row)
        self.tableWidgetRental.setItem(row, 0, QTableWidgetItem(str(rental.rental_id)))
        self.tableWidgetRental.setItem(row, 1, QTableWidgetItem(str(rental.rental_date)))
        self.tableWidgetRental.setItem(row, 2, QTableWidgetItem(str(rental.inventory_id)))
        self.tableWidgetRental.setItem(row, 3, QTableWidgetItem(str(rental.customer_id)))
        self.tableWidgetRental.setItem(row, 4, QTableWidgetItem(str(rental.return_date)))
        self.tableWidgetRental.setItem(row, 5, QTableWidgetItem(str(rental.staff_id)))
        self.tableWidgetRental.setItem(row, 6, QTableWidgetItem(str(rental.last_update)))

Chạy các mã lệnh này lên ta sẽ có giao diện như dưới đây:

Hình trên minh họa, người sử dụng chọn Customer có ID là 18, thì dữ liệu chi tiết của Customer sẽ được hiển thị ở mục Customer Details, đồng thời danh sách Rentals của Customer này cũng được hiển thị vào mục List of Rental QTableWidget ở bên dưới màn hình.

Bước 5.5: Viết hàm xử lý sự kiện khi nhấn vào nút “Clear”

def processClear(self):
    self.lineEditCustomerId.setText("")
    self.lineEditFirstName.setText("")
    self.lineEditLastName.setText("")
    self.lineEditEmail.setText("")
    self.checkBoxActive.setChecked(False)
    self.lineEditCreateDate.setText("")
    self.lineEditLastUpdate.setText("")
    self.lineEditAddress.setText("")
    self.lineEditCustomerId.setFocus()

Dữ liệu trong phần Customer details sẽ bị xóa trống, và người dùng có thể nhập dữ liệu mới.

Bước 5.6: Viết hàm xử lý sự kiện khi nhấn vào nút “Insert

def processInsert(self):
    # Insert new Student Object:
    new_customer = Customer()
    new_customer.first_name=self.lineEditFirstName.text()
    new_customer.last_name = self.lineEditLastName.text()
    new_customer.email = self.lineEditEmail.text()
    if self.checkBoxActive.isChecked():
        new_customer.active=1
    else:
        new_customer.active = 0
    new_customer.store_id=1
    new_customer.address_id=1
    new_customer.save()
    self.lineEditCustomerId.setText(str(new_customer.customer_id))
    self.showAllCustomerOnQTableWidget()

Sau khi người dùng nhập liệu và nhấn nút “Insert” thì dữ liệu Customer được lưu thành công và hiển thị lại lên giao diện.

Coding ở trên vì Sakila design store_id và address_id là not null, nên Tui tạm thời để là 1. Dưới đây là minh họa chức năng INSERT khi chạy phần mềm:

Vì Customer mới nên Rentals không có do đó QTableWidget List of Rental rỗng.

Và bạn cũng thấy Last Update đang None vì mới Insert chưa có Update.

Bước 5.7: Viết hàm xử lý sự kiện khi nhấn vào nút “Update

def processUpdate(self):
    customer_id = int(self.lineEditCustomerId.text())
    # process for selected Customer details
    customer = Customer.find(customer_id)
    fname = self.lineEditFirstName.text()
    lname = self.lineEditLastName.text()
    email = self.lineEditEmail.text()
    if self.checkBoxActive.isChecked():
        active = 1
    else:
        active = 0
    customer.update(first_name=fname,last_name=lname,email=email,active=active,last_update=datetime.now())
    self.showAllCustomerOnQTableWidget()

Mã lệnh ở trên sẽ cập nhật dữ liệu Customer theo ORM. Khi thực hiện thành công chương trình sẽ cập nhật lại giao diện. Kết quả minh họa:

Ta thấy nếu dùng chức năng Update, thì Last Update sẽ có giá trị.

Bước 5.8: Viết hàm xử lý sự kiện khi nhấn vào nút “Delete“, chương trình sẽ xóa Customer theo primary customer_id:

def processDelete(self):
    customer_id = int(self.lineEditCustomerId.text())
    # process for selected Customer details
    customer = Customer.find(customer_id)
    dlg = QMessageBox(self.MainWindow)
    dlg.setWindowTitle("Confirmation Deleting")
    dlg.setIcon(QMessageBox.Icon.Critical)
    dlg.setText("Are you sure you want to delete?")
    buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    dlg.setStandardButtons(buttons)
    button = dlg.exec()
    if button == QMessageBox.StandardButton.Yes:
        customer.destroy()
        self.processClear()
        self.showAllCustomerOnQTableWidget()

Chương trình yêu cầu xác nhận có muốn xóa hay không, nếu đồng ý thì sẽ xóa.

mã lệnh ở trên ta thực hiện:

  • Xóa Customer hiện tại đang chọn khỏi cơ sở dữ liệu
  • Xóa trống các dữ liệu của Customer vừa xóa ra khỏi giao diện Details
  • Nạp lại dánh sách Customer cho QTableWidget phần List Customers

Chạy thử nghiệm ta có:

  • Trường hợp 1: Nếu xóa Customer đã có Rental, chương trình sẽ báo lỗi, vì chúng ta cần xóa hết Rentals của Customer này đã
  • Trường hợp 2: Xóa Customer vừa thêm vào, sẽ thành công

Các bạn tự xử lý thêm mã lệnh

Cuối cùng ta vào chức năng thoát phần mềm:

Bước 5.9: Viết hàm xử lý sự kiện thoát phần mềm:

def processExit(self):
    dlg = QMessageBox(self.MainWindow)
    dlg.setWindowTitle("Confirmation Exit")
    dlg.setText("Are you sure you want to Exit?")
    dlg.setIcon(QMessageBox.Icon.Question)
    buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
    dlg.setStandardButtons(buttons)
    button = dlg.exec()
    if button == QMessageBox.StandardButton.Yes:
       exit()

Chương trình sẽ xác nhận người dùng muốn thoát hay không, nếu đồng ý sẽ thoát:

Dưới đây là mã lệnh tổng hợp của MainWindowEx.py:

from datetime import datetime
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QTableWidgetItem, QMessageBox
from UI.MainWindow import Ui_MainWindow
import Classes.DatabaseConnection
from Classes.ClassMapping import Customer, Rental, Address

class MainWindowEx(Ui_MainWindow):
    def __init__(self):
        pass
    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)
        self.MainWindow=MainWindow
        self.showAllCustomerOnQTableWidget()
        self.tableWidgetCustomer.itemSelectionChanged.connect(self.processItemSelection)
        self.pushButtonClear.clicked.connect(self.processClear)
        self.pushButtonInsert.clicked.connect(self.processInsert)
        self.pushButtonUpdate.clicked.connect(self.processUpdate)
        self.pushButtonDelete.clicked.connect(self.processDelete)
        self.pushButtonExit.clicked.connect(self.processExit)
    def showWindow(self):
        self.MainWindow.show()
    def showAllCustomerOnQTableWidget(self):
        customers = Customer.all()
        self.tableWidgetCustomer.setRowCount(0)
        row = 0
        for cust in customers:
            row = self.tableWidgetCustomer.rowCount()
            self.tableWidgetCustomer.insertRow(row)
            self.tableWidgetCustomer.setItem(row, 0, QTableWidgetItem(str(cust.customer_id)))
            self.tableWidgetCustomer.setItem(row, 1, QTableWidgetItem(cust.first_name))
            self.tableWidgetCustomer.setItem(row, 2, QTableWidgetItem(cust.last_name))
            if cust.active==0:
                self.tableWidgetCustomer.item(row,0).setBackground(Qt.GlobalColor.red)
                self.tableWidgetCustomer.item(row, 1).setBackground(Qt.GlobalColor.red)
                self.tableWidgetCustomer.item(row, 2).setBackground(Qt.GlobalColor.red)
    def processItemSelection(self):
        row = self.tableWidgetCustomer.currentRow()
        if row == -1:
            return
        customer_id=int(self.tableWidgetCustomer.item(row,0).text())
        #process for selected Customer details
        customer = Customer.find(customer_id)
        self.lineEditCustomerId.setText(str(customer.customer_id))
        self.lineEditFirstName.setText(customer.first_name)
        self.lineEditLastName.setText(customer.last_name)
        self.lineEditEmail.setText(customer.email)
        if customer.active==1:
            self.checkBoxActive.setChecked(True)
        else:
            self.checkBoxActive.setChecked(False)
        self.lineEditCreateDate.setText(str(customer.create_date))
        self.lineEditLastUpdate.setText(str(customer.last_update))
        #process for address table
        addresses=customer.addresses()
        address=addresses[0]
        self.lineEditAddress.setText(address.address)
        #process for rentals
        self.showAllRentalOnQTableWidget(customer)
    def showAllRentalOnQTableWidget(self,customer):
        rentals = customer.rentals()
        self.tableWidgetRental.setRowCount(0)
        row = 0
        for rental in rentals:
            row = self.tableWidgetRental.rowCount()
            self.tableWidgetRental.insertRow(row)
            self.tableWidgetRental.setItem(row, 0, QTableWidgetItem(str(rental.rental_id)))
            self.tableWidgetRental.setItem(row, 1, QTableWidgetItem(str(rental.rental_date)))
            self.tableWidgetRental.setItem(row, 2, QTableWidgetItem(str(rental.inventory_id)))
            self.tableWidgetRental.setItem(row, 3, QTableWidgetItem(str(rental.customer_id)))
            self.tableWidgetRental.setItem(row, 4, QTableWidgetItem(str(rental.return_date)))
            self.tableWidgetRental.setItem(row, 5, QTableWidgetItem(str(rental.staff_id)))
            self.tableWidgetRental.setItem(row, 6, QTableWidgetItem(str(rental.last_update)))
    def processClear(self):
        self.lineEditCustomerId.setText("")
        self.lineEditFirstName.setText("")
        self.lineEditLastName.setText("")
        self.lineEditEmail.setText("")
        self.checkBoxActive.setChecked(False)
        self.lineEditCreateDate.setText("")
        self.lineEditLastUpdate.setText("")
        self.lineEditAddress.setText("")
        self.lineEditCustomerId.setFocus()

    def processInsert(self):
        # Insert new Student Object:
        new_customer = Customer()
        new_customer.first_name=self.lineEditFirstName.text()
        new_customer.last_name = self.lineEditLastName.text()
        new_customer.email = self.lineEditEmail.text()
        if self.checkBoxActive.isChecked():
            new_customer.active=1
        else:
            new_customer.active = 0
        new_customer.store_id=1
        new_customer.address_id=1
        new_customer.save()
        self.lineEditCustomerId.setText(str(new_customer.customer_id))
        self.showAllCustomerOnQTableWidget()
    def processUpdate(self):
        customer_id = int(self.lineEditCustomerId.text())
        # process for selected Customer details
        customer = Customer.find(customer_id)
        fname = self.lineEditFirstName.text()
        lname = self.lineEditLastName.text()
        email = self.lineEditEmail.text()
        if self.checkBoxActive.isChecked():
            active = 1
        else:
            active = 0
        customer.update(first_name=fname,last_name=lname,email=email,active=active,last_update=datetime.now())
        self.showAllCustomerOnQTableWidget()
    def processDelete(self):
        customer_id = int(self.lineEditCustomerId.text())
        # process for selected Customer details
        customer = Customer.find(customer_id)
        dlg = QMessageBox(self.MainWindow)
        dlg.setWindowTitle("Confirmation Deleting")
        dlg.setIcon(QMessageBox.Icon.Critical)
        dlg.setText("Are you sure you want to delete?")
        buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        dlg.setStandardButtons(buttons)
        button = dlg.exec()
        if button == QMessageBox.StandardButton.Yes:
            customer.destroy()
            self.processClear()
            self.showAllCustomerOnQTableWidget()
    def processExit(self):
        dlg = QMessageBox(self.MainWindow)
        dlg.setWindowTitle("Confirmation Exit")
        dlg.setText("Are you sure you want to Exit?")
        dlg.setIcon(QMessageBox.Icon.Question)
        buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
        dlg.setStandardButtons(buttons)
        button = dlg.exec()
        if button == QMessageBox.StandardButton.Yes:
           exit()

Bước 6: Cuối cùng ta khai báo MyApp.py để thực thi chương trình

from PyQt6.QtWidgets import QApplication, QMainWindow

from UI.MainWindowEx import MainWindowEx

qApplication=QApplication([])
qMainWindow=QMainWindow()
myWindow=MainWindowEx()
myWindow.setupUi(qMainWindow)
myWindow.showWindow()
qApplication.exec()

Thực thi mã lệnh trên, ta có phần mềm như mong muốn:

Như vậy, Tui đã hướng dẫn xong chi tiết cách sử dụng kỹ thuật ORM để lập trình xử lý giao diện tương tác người dùng cho cơ sở dữ liệu Sakila, làm việc trên 3 bảng có mối quan hệ: Customer, Address, Rental.

Đã minh họa đầy đủ các chức năng CRUD tương ứng với kỹ thuật ORM, các bạn chú ý làm lại nhiều lần để hiểu hơn về dự án.

Source code full của dự án các bạn tải ở đây:

https://www.mediafire.com/file/bntg4ywrmyjw02d/sakila_orm_gui.rar/file

Chúc các bạn thành công

Leave a Reply