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

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

Trong bài 50, chúng ta đã biết cách sử dụng kỹ thuật lập trình ORM để tương tác cơ sở dữ liệu MySQL Server bằng Python. Tuy nhiên chúng ta chưa đề cập tới vấn đề cả bảng dữ liệu có mối quan hệ dạng Master-Details.

Ví dụ như trong Quản lý Sản phẩm ta có Danh mục, ta có sản phẩm. 1 Danh mục có nhiều sản phẩm, và 1 sản phẩm thuộc về 1 danh mục. Vậy với trường hợp có relationship master-details như thế này thì chúng ta lập trình ORM ra sao? và trong thực tế chúng ta luôn luôn gặp các relationship master-details này.

Hay ngay trong quá trình cài đặt MySQL Server, chúng ta cũng được cung cấp một cơ sở dữ liệu mẫu “sakila“, là một cơ sở dữ liệu liên quan tới quản lý cho mượn Film, và cũng có nhiều relationship Master-Details, chúng ta sẽ lập trình ORM trên cơ sở dữ liệu mẫu này luôn (ta làm trên bảng Customer và bảng Rental):

Ta xem Relationship giữa bảng Customer và bảng Rental:

  • Một Customer có nhiều Rental (dựa vào khóa ngoại customer_id trong bảng rental, khi có 1 Customer ta sẽ biết được Customer này có bao nhiêu Rental)
  • Một Rental thuộc về một Customer (từ bảng rental ta có thuộc tính customer_id để suy ngược ra thông tin chi tiết Customer của Rental)

Đây chính là Relationship 1-many.

Ta tạo dự án “sakila_orm” trong Pycharm có cấu trúc như dưới đây:

Trong dự án này Tui chủ ý tách ra làm 3 file mã lệnh Python độc lập để có thể dễ dàng tái sử dụng:

  • File “DatabaseConnection.py“: Là file mã lệnh để kế nối Cơ sở dữ liệu, nó phải được thực hiện trước các câu lệnh khác trong dự án
  • File “ClassMapping.py“: Là file mã lệnh khai báo class mapping và relationship
  • File “TestRelationShipORM.py“: Là file mã lệnh để thử nghiệm kỹ thuật ORM với các bảng có Relationship

Bước 1:

Bây giờ ta xem mã lệnh của 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 kết nối cơ sở dữ liệu được tách độc lập ra 1 file khác, khi có sự thay đổi về thông tin kết nối thì bạn chỉ cần vào file này chỉnh sửa sẽ không bị rối.

Bước 2:

Tiếp theo là mã lệnh File “ClasssMapping.py“:

Dựa vào cấu trúc bảng Customer và Rental, Ta có 1 Customer có nhiều Rental, nên ở đây trong lớp Customer ta dùng hàm has_many. Còn đối với lớp Rental thì ứng với 1 đối tượng Rental ta biết được Rental này của Customer nào nên ta dùng hàm belongs_to:

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')
    ]
class Rental(Table):
    table_name = 'rental'

    relations = [
        belongs_to(name='customer', _class='Customer', foreign_key='customer_id',primary_key="customer_id")
    ]

hàm has_many:

Hàm has_many cho biết đối tượng này chứa nhiều đối tượng khác.

+ name=’rentals’: Đây là phương thức rentals() khi gọi phương thức này nó sẽ trả về danh sách đối tượng có kiểu Rental được khai báo trong _class

+_class: Đây là tham số định nghĩa kiểu dữ liệu đối tượng trả về cho hàm “rentals”

+foreign_key=’customer_id’: Cho biết relationship giữa Customer và Rental, khi truy vấn thì dựa vào thuộc tính này chương trình sẽ lọc ra danh sách các Rental theo foreign_key

hàm belongs_to:

hàm belongs_to cho biết đối tượng này thuộc về đối tượng khác

+name=’customer’: Là là phương thức customer() trả về đối tượng Customer của Rental đang xét. Kiểu dữ liệu trả về được khai báo trong _class là ‘Customer’

+_class=’Customer’: Đầy là tham số định nghĩa kiểu dữ liệu đối tượng trả về cho hàm customer()

+foreign_key=’customer_id’: Cho biết relationship giữa Customer và Rental, khi truy vấn thì dựa vào thuộc tính này thì chương trình sẽ cho biết mã tham chiếu tới customer hiện tại của Rental là customer_id

+primary_key=’customer_id’: Khi gọi hàm customer() sẽ truy vấn ngược lại bảng Customer dựa vào primary_key customer_id này.

Bước 3:

Cuối cùng ta viết mã lệnh cho file “TestRelationshipORM.py“:

Đâu tiên chúng ta cần import DatabaseConnection, sau đó tới MappingClass:

import DatabaseConnection
from ClassMapping import Customer, Rental

Lưu ý cần theo đúng trình tự Import ở trên, vì chương trình cần kết nối cơ sở dữ liệu trước sau đó mới tới Mapping các Class cho Table.

Tiếp theo, để truy vấn toàn bộ Customer, ta viết nối tiếp mã lệnh như sau:

#Get all Customer
customers = Customer.all()
align='{0:<6} {1:<10} {2:<10} {3:<10}'
print(align.format('Id', 'First Name','Last Name',"Email"))
for cust in customers:
    print(align.format(cust.customer_id,cust.first_name,cust.last_name,cust.email))

Chạy mã lệnh trên ta có kết quả:

Để truy vấn lấy thông tin chi tiết của Customer khi biết Primary Key:

#Query 1 Customer by primary key customer_id=1:
customer = Customer.find(1)
print(f"Id={customer.customer_id}")
print(f"First Name={customer.first_name}")
print(f"Last Name={customer.last_name}")
print(f"Email={customer.email}")

Chạy mã lệnh trên ta có kết quả:

Để truy vấn lấy danh sách Rental của Customer này ta viết lệnh:

#Get all rentals for this customer:
rentals=customer.rentals()
align='{0:<10} {1:<20} {2:<20} {3:<10} {4:<10}'
print(align.format('Rental Id', 'Rental Date','Return Date',"Staff Id","Customer Id"))
for rental in rentals:
    print(align.format(
        rental.rental_id,
        str(rental.rental_date),
        str(rental.return_date),
        rental.staff_id,
        rental.customer_id))

Thực thi mã lệnh ORM ở trên ta có kết quả danh sách Rentals của Customer như dưới đây:

Để truy vấn lấy thông tin chi tiết của Rental khi biết primary key rental_id ta làm như sau:

#Query Rental detail by primary key rental_id=2
rental=Rental.find(2)
print(align.format('Rental Id', 'Rental Date','Return Date',"Staff Id","Customer Id"))
print(align.format(
        rental.rental_id,
        str(rental.rental_date),
        str(rental.return_date),
        rental.staff_id,
        rental.customer_id))

Mã lệnh ở trên là truy vấn thông tin chi tiết của Rental khi biết rental_id=2

Thực thi lệnh trên ta có kết quả:

Bây giờ, để in thông tin chi tiết Customer của Rental này ta viết mã lệnh:

# Get Customer of this Rental:
customer=rental.customer()
print(f"Id={customer.customer_id}")
print(f"First Name={customer.first_name}")
print(f"Last Name={customer.last_name}")
print(f"Email={customer.email}")

Thực thi lệnh trên ta có kết quả:

Như vậy Tui đã minh họa xong kỹ thuật ORM để xử lý các bảng có Relationship. Đây là bài học rất quan trọng và cần thiết vì nó minh họa trường hợp trong thực tế mà ta thường gặp, hầu hết các bảng dữ liệu đều có các mối quan hệ này.

Dưới đây là mã lệnh đầy đủ của TestRelationshipORM.py:

import DatabaseConnection
from ClassMapping import Customer, Rental

#Get all Customer
customers = Customer.all()
align='{0:<6} {1:<10} {2:<10} {3:<10}'
print(align.format('Id', 'First Name','Last Name',"Email"))
for cust in customers:
    print(align.format(cust.customer_id,cust.first_name,cust.last_name,cust.email))

#Query 1 Customer by primary key customer_id=1:
customer = Customer.find(1)
print(f"Id={customer.customer_id}")
print(f"First Name={customer.first_name}")
print(f"Last Name={customer.last_name}")
print(f"Email={customer.email}")

#Get all rentals for this customer:
rentals=customer.rentals()
align='{0:<10} {1:<20} {2:<20} {3:<10} {4:<10}'
print(align.format('Rental Id', 'Rental Date','Return Date',"Staff Id","Customer Id"))
for rental in rentals:
    print(align.format(
        rental.rental_id,
        str(rental.rental_date),
        str(rental.return_date),
        rental.staff_id,
        rental.customer_id))

#Query Rental detail by primary key rental_id=2
rental=Rental.find(2)
print(align.format('Rental Id', 'Rental Date','Return Date',"Staff Id","Customer Id"))
print(align.format(
        rental.rental_id,
        str(rental.rental_date),
        str(rental.return_date),
        rental.staff_id,
        rental.customer_id))

# Get Customer of this Rental:
customer=rental.customer()
print(f"Id={customer.customer_id}")
print(f"First Name={customer.first_name}")
print(f"Last Name={customer.last_name}")
print(f"Email={customer.email}")

Ngoài ra bạn có thể tải mã nguồn của toàn bộ dự án ở đây:

https://www.mediafire.com/file/v67quhs1j3srmnz/sakila_orm.rar/file

Các chức năng ORM khác như: Thêm, sửa, Xóa thì các bạn tự xử lý theo bài 50 đã được học.

Bài học sau Tui sẽ minh họa ORM trên giao diện tương tác người dùng PyQt6 – Qt Designer.

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

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

Bài 48bài 49 các bạn đã lập trình thành thạo Python với MySQL Server, đã thực hiện đầy đủ các chức năng CRUD liên quan tới phần mềm quản lý.

Tuy nhiên, các cách lập trình ấy có một vài khuyết điểm như: Tốn thời gian viết các mã lệnh tương tác cơ sở dữ liệu, tốn thời gian mô hình hóa hướng đối tượng.

Các kỹ sư phần mềm họ đã phát triển một kỹ thuật ORM rất lợi hại để mô hình hóa đối tượng với các bảng dữ liệu một cách nhanh chóng.

ORM (Object Relational Mapping) là kỹ thuật trong lập trình phần mềm giúp ánh xạ dữ liệu từ cơ sở dữ liệu quan hệ vào các đối tượng trong mã nguồn. Điều này giúp giảm sự phụ thuộc vào cấu trúc của cơ sở dữ liệu, đồng thời tạo ra một cách tiếp cận linh hoạt hơn trong việc quản lý dữ liệu:

Tóm tắt một số Ưu điểm chính của ORM (hình nguồn medium):

  1. ORM giúp người lập trình tập trung hơn vào việc lập trình hướng đối tượng
  2. Cho phép truy cập vào code nghiệp vụ thay vì database
  3. Hạn chế những lỗi ngữ pháp trong SQL
  4. Quản lý Transaction và tạo key tự động
  5. Đơn giản và dễ sử dụng
  6. Ẩn chi tiết của những truy vấn SQL từ Object Oriented logic (OO-logic)
  7. Đem lại năng suất cao hơn cho lập trình viên
  8. Nâng cao tính độc lập
  9. Năng suất hơn nhờ việc viết code ít hơn
  10. Cho phép lập trình viên sử dụng lại code
  11. ORM Framework cho phép truy xuất nhanh hơn bằng cách cache dữ liệu
  12. Tự động thực hiện những thao tác với dữ liệu

Python cũng giống như các công ngữ lập trình C#, Java, nó cũng được cung cấp các thư viện khác nhau để thực hiện ORM:

Thông qua thư viện Python thì cơ sở dữ liệu sẽ được mapping hướng đối tượng theo nguyên tắc(hình nguồn medium) :

  • Tên bảng <-> Tên lớp
  • Các cột trong bảng<-> Thuộc tính của lớp
  • Từng dòng dữ liệu trong bảng <-> Các đối tượng của lớp

Ví dụ: Giả sử ta có bảng Person dưới đây, bảng này có 4 cột ID (Auto Increment), First_Name, Last_Name và Phone. Khi ORM ta có:

Tạo mới đối tượng ta không cần ID vì ID auto increment và tự động phát sinh giá trị khi chúng ta lưu thành công, khi truy vấn thì nó tự động có ID có giá trị được mapping lên.

Chú ý là bài học này ta vẫn sử dụng “Student Management” đã tạo trước đó. Nên để học tốt bài này thì các bạn cần học bài 47, bài 48bài 49 trước.

Chuỗi bài học này sẽ hướng dẫn các bạn sử dụng orm-mysql

Bước 0: Chuẩn bị cơ sở dữ liệu Student Management ở bài học trước:

Bước 1: cài đặt orm-mysql

pip install orm-mysql

Mở command line và chạy lệnh trên, ta có kết quả:

Bước 2: Tạo dự án “LearnMySQLORM” trong Pycharm, trong dự án này tạo file “TestORM.py” và tiến hành viết mã lệnh theo các bước sau:

Bước 3: Tạo file cấu hình kết nối cơ sở dữ liệu và tiến hành kết nối

from orm import Table, get_table

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

Ở trên ta dùng phương thức connect của Table để tiến hành kết nối cơ sở dữ liệu

Bước 4: Kết nối và mapping bảng dữ liệu, ví dụ ta muốn ORM cho bảng student:

#mapping student table
Student = get_table('student')

Bước 5: Truy vấn toàn bộ dữ liệu Sinh viên

#Query all students:
students = Student.all()
align='{0:<3} {1:<6} {2:<18} {3:<10}'
print(align.format('ID', 'Code','Name',"Age"))
for stu in students:
    print(align.format(stu.ID,stu.Code,stu.Name,stu.Age))

lệnh trên ta gọi hàm get_table(“student”), hàm này giúp ta mapping bảng student vào lớp đối tượng Student.

phương thức all() dùng để truy vấn toàn bộ dữ liệu Sinh viên

Kết quả thi khi thực hiện lệnh truy vấn toàn bộ Sinh viên:

Bước 6: Để tìm Student theo ID (tìm chi tiết, ta thường sử dụng)

#Find student detail by ID (eg: 2)
student = Student.find(2)
print("Id=",student.ID)
print("code=",student.Code)
print("name=",student.Name)
print("age=",student.Age)

Chạy lệnh trên ta có kết quả là chi tiết của Sinh viên có ID=2 được xuất ra màn hình:

Bước 7: Để thêm mới một Student:

#Insert new Student Object:
new_student = Student(Code="SV300",Name='Đông Tà',Age=25)

new_student.save()

Khi chạy mã lệnh trên, chương trình sẽ tự động thực hiện câu truy vấn:

SQL: INSERT INTO student (ID, Code, Name, Age, Avatar, Intro) VALUES (NULL, 'SV300', 'Đông Tà', 25, NULL, NULL); 

cột nào không có giá trị nó sẽ tự động có giá trị NULL

Thực hiện lệnh trên thành công thì MySQL Server sẽ có dữ liệu mới với thông tin được cung cấp ở trên.

Bước 8: Để chỉnh sử dữ liệu cho đối tượng Student:

#Update student detail by ID 3
student = Student.find(3)
student.update(Name="Nguyễn Thị Lung Linh",Age=23)

Coding trên minh họa việc chỉnh tên và tuổi của Student có mã Id=3

Chạy lệnh trên ta có kết quả:

Bước 9: Để xóa Student ta viết:

#Remove student  by ID 15
student = Student.find(15)
student.destroy()

Chạy mã lệnh, ta không còn thấy Sinh viên Đông Tà có mã 15 nữa.

Toàn bộ Coding ORM cho chức năng CRUD Tui tổng hợp ở đây:

from orm import Table, get_table

#configuration JSON for connection MySQL
CONFIG={
    'host': 'localhost',
    'port': 3306,
    'user': 'root',
    'password': '@Obama123',
    'database': 'studentmanagement'
}
#connect to database from JSON configuration
Table.connect(config_dict=CONFIG)
#mapping student table
Student = get_table('student')
#Query all students:
students = Student.all()
align='{0:<3} {1:<6} {2:<18} {3:<10}'
print(align.format('ID', 'Code','Name',"Age"))
for stu in students:
    print(align.format(stu.ID,stu.Code,stu.Name,stu.Age))
#Find student detail by ID (eg: 2)
student = Student.find(2)
print("Id=",student.ID)
print("code=",student.Code)
print("name=",student.Name)
print("age=",student.Age)

#Insert new Student Object:
new_student = Student(Code="SV300",Name='Đông Tà',Age=25)

new_student.save()

#Update student detail by ID 3
student = Student.find(3)
student.update(Name="Nguyễn Thị Lung Linh",Age=23)

#Remove student  by ID 15
student = Student.find(15)
student.destroy()

Như vậy tới đây Tui đã trình bày xong ORM liên quan tới 1 bảng dữ liệu, đủ các chức năng CRUD. Các bạn chú ý thực hành nhiều lần để hiểu về nó, thành thạo về ORM.

Bài học sau Tui sẽ tiếp tục hướng dẫn ORM , tuy nhiên nâng cao hơn ở chỗ thực hiện ORM với các bảng có mối quan hệ, ví dụ Sinh viên – Môn học. Các bạn chú ý theo dõi, đây là các bài học quan trọng và thực tế

Các bạn tải mã nguồn đầy đủ của bài này ở đây:

https://www.mediafire.com/file/s5qfcwke9bikdki/LearnMySQLORM.rar/file

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