Lập trình web với Cherrypy và Jinja2

Lập trình web với Cherrypy và Jinja2
Photo by James Harrison from Unsplash

Trong bài viết này, tôi sẽ giới thiệu một framework để phát triển Web – CherryPy – một framework được viết cho Python. Tôi sẽ không đi sâu vào phân tích và so sánh với các framework khác và các ngôn ngữ khác và tại sao bạn nên sử dụng framework này. Bởi vì mỗi framework đều có những điểm mạnh điểm yếu riêng, không có framework nào là hoàn hảo cả. Mục đích lớn nhất của bài viết là học hỏi và giới thiệu một framework mới, từ đó chúng ta có thêm nhiều lựa chọn hơn khi muốn phát triển Web. Và tất nhiên, tùy vào mục đích của trang Web mà bọn có thể chọn được framework thích hợp nhất cho mình.

Giới thiệu CherryPy và Jinja2

CherryPy

CherryPy là một framework phát triển Web hướng đối tượng theo phong cách Pythonic. CherryPy cho phép những người phát triển xây dựng các ứng dụng Web với cách thức hoàn toàn giống như xây dựng các ứng dụng Python hướng đối tượng khác. Điều này sẽ giúp giảm kích thước mã nguồn và thời gian phát triển, nếu như bạn đã có kinh nghiệm với Python.

Những thông tin về CherryPy bạn có thể tìm thấy trên trang chủ http://cherrypy.org/, những tài liệu và nhiều thông tin khác được đưa ra ở trong http://docs.cherrypy.org/en/latest/index.html. Ở những trang đó, bạn có thể thấy những ưu điểm nổi bật của CherryPy và lý do tại sao bạn nên dùng nó. Ở nội dung bài viết này, tôi sẽ không đi sâu vào phân tích những điều này.

Có một đặc điểm của CherryPy khác với rất nhiều các framework khác. CherryPy được xây dựng đơn giản gọn nhẹ, và công việc của nó cũng hết sức đơn giản, đó là nhận request từ người dùng (cụ thể là các trình duyệt Web) và gửi phản hồi. CherryPy không cung cấp bất kỳ một template engine nào để xây dựng các trang HTML. Và CherryPy cũng không cung cấp bất kỳ một ORM (Object Relation Mapper) nào để tương tác với cơ sở dữ liệu. Điều đó có nghĩa là bạn có thể tự do lựa chọn bất cứ một template engine nào, và bất cứ ORM nào được viết cho Python.

Có rất nhiều template engine khác nhau có thể sử dụng với CherryPy, bạn có thể tham khảo ở đây. Với nội dung bài viết này, tôi sẽ sử dụng Jinja2.

Jinja2

Jinja2 là một template engine cho Python với đầy đủ tính năng mà một template engine cần phải có với khả năng hỗ trợ Unicode, I18n, và cả khă năng chạy môi trường sandbox.

Dưới đây là một vài tính năng nổi bật của Jinja2

  • chạy sandbox
  • Có hệ thống escape HTML mạnh mẽ để phòng chống XSS
  • Có thể kế thừa, module hóa template
  • Được xây dựng để tối ưu hóa code Python
  • Tùy chọn cho phép dịch template head-in-time
  • Dễ dàng debug.
  • Cú phát cho phép dễ dàng config

Cài đặt

Để phát triển Web, trước hết bạn phải cài đặt các package cần thiết, cụ thể ở đây là CherryPy và Jinja2. Tôi nghĩ, tốt nhất chúng ta nên thiết lập môi trường ảo cho python ở trên máy của chúng ta, điều đó sẽ giúp những cài đặt sau đây không ảnh hưởng gì đến hệ thống.

Lưu ý, trong bài viết này, tôi sẽ sử dụng Python 3. Với Python 2, việc cấu hình và mã nguồn sẽ khác đi một chút.

  • Khởi tạo môi trường ảo cho Python
$ pyvenv venv

Nếu bạn sử dụng Ubuntu 14.04 thì Ubuntu cài đặt sẵn một bản pyvenv không đầy đủ, nên bạn không thể dùng lệnh trên được. Tham khảo cách làm ở đây để fix lỗi này.

  • Activate Python trong môi trường ảo
$ source venv/bin/activate
  • Cài đặt các package cần thiết
$ pip install CheryPy
$ pip install Jinja2

Vậy là đủ cho một ứng dụng Web đơn giản. Sau đây chúng ta sẽ đi vào cụ thể việc phát triển Web với CherryPy và Jinja2.

Phát triển Web

Cấu trúc file và thư mục

Bởi vì CherryPy được xây dựng để việc phát triển Web giống như phát triển một app Python bình thường, nên không có một quy định cụ thể nào về việc sắp xếp và tổ chức các thư mục. Tuy nhiên, tôi đã tham khảo một project trên bitbucket và thấy đây là một cấu trúc tốt, rất dễ hiểu. Nhiều người phát triển Web bằng CherryPy cũng tham khảo cách tổ chức này. Về cơ bản một project sẽ có các file và thư mục sau:

  • conf: thư mục chứa các file config của app.
  • lib: thư mục chứa các thư viện, tools và cả các models dùng cho app, một phần của nó giống như M trong MVC
  • public: thư mục chứa các tệp tĩnh như css, js, …
  • template: thư mục chứa các file template, nó chính là phần V trong MVC.
  • webapp: thư mục chưa file chính của app, nó đóng vai trò là phần C trong MVC.
  • server.py: file được gọi chính để khởi tạo server Web.

Nội dung của file server.py như sau:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import cherrypy
import tempfile

import webapp.app


class Server(object):

    def __init__(self):
        self._set_basic_config()
        self._setup()
        self._add_app()

    def _set_basic_config(self):
        self.base_dir = os.path.dirname(os.path.abspath(__file__))
        self.conf_path = os.path.join(self.base_dir, "conf")
        log_dir = os.path.join(self.base_dir, "logs")
        if not os.path.exists(log_dir):
            os.mkdir(log_dir)
        session_dir = os.path.join(tempfile.gettempdir(), "sessions")
        if not os.path.exists(session_dir):
            os.mkdir(session_dir)

    def _setup(self):
        # Update the global settings for the HTTP server and engine
        cherrypy.config.update(os.path.join(self.conf_path, "server.conf"))

    def _add_app(self):
        cherrypy.tree.mount(
            webapp.app.Bank(),
            "/",
            os.path.join(self.conf_path, "app.conf")
        )

    def run(self):
        engine = cherrypy.engine
        if hasattr(engine, "signal_handler"):
            engine.signal_handler.subscribe()

        if hasattr(engine, "console_control_handler"):
            engine.console_control_handler.subscribe()

        engine.start()
        engine.block()


if __name__ == "__main__":
    Server().run()

server.py sẽ là file chính được gọi khi khởi tạo server Web. Nhiệm vụ của nó là khởi tạo các cấu hình cần thiết của hệ thống để app có thể chạy được. Cụ thể trong trường hợp trên là khởi tạo các thư mục lưu sessions, logs, gọi các app. Các giá trị khởi tạo này hoàn toàn có thể cấu hình được. Và các file cấu hình được lưu trong thư mục conf. Trong ví dụ này, tôi lưu 2 file server.conf để lưu cấu hình hệ thống và app.conf để lưu các cấu hình riêng cho app.

Nội dung file server.conf như sau:

[global]
server.socket_host: "0.0.0.0"
server.socket_port: 8080
server.thread_pool: 10
engine.autoreload.on: False
log.error_file: "./logs/error.log"
log.access_file: "./logs/access.log"

File config này được viết theo cú pháp của CherryPy, bạn có thể tham khảo cú pháp đó trong trong Web của CherryPy. Ở đây, tôi đã cấu hình một vài thông số cơ bản như Web server sẽ chạy ở cổng 8080 và các giá trị về thread pool, các file log, …

Dưới đây là nội dung file app.conf

[/]
tools.staticdir.root: os.path.normpath(os.path.abspath(os.path.curdir))
tools.staticfile.root: os.path.normpath(os.path.abspath(os.path.curdir))
tools.encode.on: True
tools.gzip.on: True
tools.gzip.mime_types: ['text/html', 'text/plain', 'application/json', 'text/javascript', 'application/javascript']
tools.sessions.on: True
tools.sessions.timeout: 60
tools.sessions.storage_type: "file"
tools.sessions.storage_path: os.path.join(tempfile.gettempdir(), "sessions")

[/static]
tools.etags.on: True
tools.staticdir.on: True
tools.staticdir.dir: "public"

Những nội dung cụ thể của file này, tôi sẽ lần lượt giới thiệu sau đây.

Các file tĩnh (CSS, JavaScript,…)

Các file CSS, JavaScript, và file ảnh, … là các file tĩnh với một ứng dụng Web. Nó sẽ được load cùng với trang Web và không thay đổi nội dung trong suốt quá trình thao tác với trang Web đó. CherryPy cung cấp công cụ riêng để load các file này. Với ví dụ của chúng ta, các file tĩnh đặt trong thư mục public, và tôi muốn CherryPy sẽ load các file này với path tương tự như /static/app.js. Để làm được điều này, chỉ cần cấu hình trong file app.conf là đủ:

[/]
tools.staticdir.root: os.path.normpath(os.path.abspath(os.path.curdir))
tools.staticfile.root: os.path.normpath(os.path.abspath(os.path.curdir))

[/static]
tools.etags.on: True
tools.staticdir.on: True
tools.staticdir.dir: "public"

Với cấu hình trên, mỗi khi Web server muốn load một file tĩnh, ví dụ /static/app.js thì CherryPy sẽ tìm file tương ứng là public/app.js để load ra. Tương tự với các file CSS và file ảnh.

Khởi tạo app

Trong ví dụ của tôi, app controller sẽ được lập trình trong file webapp/app.py. Trước hết, cần khởi tạo app với việc import các package cần thiết và khởi tạo template engine

# -*- coding: utf-8 -*-

import cherrypy
import jinja2

__all__ = ["Bank"]


class Bank(object):

    def __init__(self):
        _tmp_loader = jinja2.FileSystemLoader(searchpath="template")
        self.tmp_env = jinja2.Environment(loader=_tmp_loader)

Như vậy template engine đã được khởi tạo, nó sẽ tìm các file trong thư mục template khi render.

Viết app

@cherrypy.expose
def index(self):
    template = self.tmp_env.get_template("index.jinja")
    return template.render()

Đây là hàm index, nó sẽ nhận request khi truy cập vào địa URL http://locahost:8080/ và trả về là nội dung đã được render của file index.jinja

Jinja2 không yêu cầu tên file có chứa phần mở rộng như thế nào, nên ở đây tôi sử dụng tên file là *.jinja. Jinja2 cho phép kế thừa và module hóa các template, nên bạn có thể chia các file template thành các file nhỏ và thư mục giống như các template engine khác. Ở ứng dụng của tôi, tôi sẽ sử dụng 1 file layout.jinja để xây dựng nên layout chính của trang Web, và các file khác ứng với các route khác, sẽ kế thừa file layout này và điền nội dung vào phần còn thiếu. Nội dung file layout.jinja như sau:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <!-- phần này sẽ load các file icon, js, css, và cá thẻ meta khác -->

    <title>{% block title %}{% endblock %}
      - yunachan yeu
    </title>
  </head>

  <body>
    <nav>
      <!-- phần này sẽ là navbar -->
    </nav>

    {% block content %}{% endblock %}

    <footer class="page-footer">
      <!-- phần này là footer -->
    </footer>
  </body>
</html>

Trên đây là nội dung của file layout. File này đã định nghĩa trước các block, ví dụ {% block content %}{% endblock %} sẽ là phần nội dung chính của trang Web. Sau này khi kế thừa, các file khác sẽ điền nội dung vào block này. Tương tụ với các block khác. Nếu bạn sử dụng layout khác, bạn hoàn toàn có thể viết lại layout này, và define nhiều block hơn nếu bạn cần.

Nội dung file index.jinja như sau. File này sẽ kế thừa layout và điền nội dung vào content.

{% extends "layout.jinja" %}

{% block title %}Top page{% endblock %}

{% block content %}
<div class="container">
  <!-- phần này là nội dung cần điền -->
</div>
{% endblock %}

Cú pháp của Jinja2 được giới thiệu rất cụ thể ở đây. Bạn có thể đọc và tham khảo để sau này sử dụng cho project của bạn.

Và với trang Web này, bạn thể sử dụng các CSS framework và các thư viện JavaScript để viết ứng dụng Web của mình trở nên sinh động và dễ sử dụng.

Thao tác với JSON

Khi lập trình Web, thường xuyên bạn sẽ gặp trường hợp server cần trả về kết quả kiểu JSON thay vì HTML truyền thống. Ví dụ như bạn lập trình API cho các ứng dụng di động hay đơn giản là trả kết quả cho request từ AJAX.

CherryPy cho phép bạn thực hiện điều này dễ dàng nhờ có decorator @cherypy.tools.json_out(). Ví dụ như sau:

@cherrypy.expose
@cherrypy.tools.json_out()
def some_func(self, *args):
    # Some process
    return {"status": "success", "result": result}

Ví dụ với hàm trên, decorator @cherrypy.expose sẽ cho phép hàm some_func được truy cập thông qua các request HTTP, decorator @cherypy.tools.json_out() sẽ làm hàm trả kết quả là dữ liệu kiểu JSON thay vì HTML mặc định. Kết quả trả về của hàm phải là kiểu dict của Python, ví dụ {"status": "success", "result": result}, khi đó, server Web sẽ trả kết quả cho request là dữ liệu kiểu JSON, giống như {status: success, result: result} cùng với header của gói tin HTTP.

Với decorator này, bạn có thể dễ dàng lập trình các API trả kết quả JSON dùng cho các ứng dụng đi động hay AJAX.

CherryPy còn cung cấp 1 decorator nữa, đó là @cherrypy.tools.json_in(). Decorator này sẽ khiến hàm sẽ nhận tham số dầu vào là dữ liệu kiểu JSON thay vì request HTTP mặc định. Tuy nhiên cần lưu ý rằng, trong phát triển các Web thông thường, các submit từ form hay AJAX đều sử dụng các request HTTP thông thường (GET, POST, …) chứ ít khi gửi dữ liệu JSON. Trường hợp dữ liệu gửi đi là JSON ít gặp nên tôi sẽ không đi sâu ở đây. Nếu quan tâm, bạn có thể tham khảo ở docs của CherryPy.

I18n cho Jinja2

Có 1 công cụ i18n được viết cho CherryPy ở đây. Tuy nhiên, đây là công cụ được viết cho CherryPy, tức là sẽ được dùng với các Web chỉ dùng CherryPy đơn thuần. Ở trong bài viết này, tôi sử dụng Jinja2 là template engine nên công cụ trên không sử dụng được.

Jinja2 có hỗ trợ phần mở rộng i18n, bạn có thể tham khảo phần mở rộng này ở đây. Tài liệu viết rất đầy đủ những gì Jinja2 hỗ trợ. Tuy nhiên, họ không đưa ra ví dụ cụ thể cách sử dụng nên khá khó khăn với những người mới bắt đầu. Dưới đây, tôi sẽ giới thiệu chi tiết về i18n đối với Jinja2.

Để sử dụng i18n, yêu cầu cần là phải có 1 translator, nó sẽ giúp dịch các đoạn text và Jinja2 sẽ tích hợp chúng vào HTML. Có thể sử dụng GNU gettext để làm translator. Tuy nhiên, tôi sẽ sử dụng babel, vì đơn giản, babel được viết cho Python nên dùng trong Python sẽ dễ dàng hơn. Thông tin về babel bạn có thể tham khảo ở đây.

Trước hết để sử dụng, bạn cần cài đặt babel

$ pip install babel

Ở trong controller chính của app, bạn hãy khởi tạo i18n cho Jinja2 tương tự như sau (có thể khởi tạo trong hàm __init__)

import babel.support

def __init__(self):
    _tmp_loader = jinja2.FileSystemLoader(searchpath="template")
    translations = babel.support.Translations.load(
        "locale",
        ["vi_VN"]
    )
    self.language = "vi_VN"
    self.tmp_env = jinja2.Environment(
        loader=_tmp_loader,
        extensions=[
            "jinja2.ext.i18n",
            "jinja2.ext.autoescape",
            "jinja2.ext.with_"
        ]
    )
    self.tmp_env.install_gettext_translations(translations)

Với khởi tạo như trên, Jinja2 sẽ tìm kiếm các file template trong thư mục tempalte và tìm cách file ngôn ngữ đã được dịch trong thư mục locale, ngôn ngữ được khởi tạo là vi_VN. Tất cả các khởi tạo này đều có thể thay đổi tùy theo nhu cầu và mục đích trang Web của bạn. Thậm chí, bạn có thể khởi tạo ngôn ngữ tùy theo setting hay IP của người dùng, nhằm giúp họ có được sự thoải mái khi truy cập trang Web của bạn.

Đến đây, sau khi đã khởi tạo translator đầy đủ, bạn có thể sử dụng các thẻ {% trans %} trong file template. Với các thể này, Jinja2 sẽ biết thay thế bằng cách đoạn văn bản tương ứng với ngôn ngữ. Ví dụ.

<h1>{% trans %}hello{% endtrans %}</h1>

Việc cần làm bây giờ là bạn cần định nghĩa các đoạn văn bản tương ứng với các thẻ trans trên. Với babel, công việc này khá là đơn giản. Babel sẽ giúp bạn đưa ra danh sách các cụm từ cần dịch một cách tự động. Trước hết bạn cần cầu hình cho babel

[jinja2: **/template/**.jinja]
encoding = utf-8
[python: source/*.py]
[extractors]
jinja2 = jinja2.ext:babel_extract

Với config trên, babel sẽ tìm thẻ trans ở tất cả các file .jinja và file .py và đưa ra danh sách các cụm từ cần dịch. Sau đó, bạn chạy lệnh dưới đây, babel sẽ tập hợp danh sách thành 1 file (lệnh này sẽ nhận config từ file ./babel.cfg và ghi kết quả ra file ./locale/messages.pot). Bạn có thể thay đổi đường dẫn của chúng nếu muốn.

$ pybabel extract -F ./babel.cfg -o ./locale/messages.pot ./

Bây giờ là đến công việc, gọi là địa phương hóa ngôn ngữ. Câu lệnh dưới đây sẽ khởi tạo ngôn ngữ vi_VN.

$ pybabel init -l vi_VN -d ./locale -i ./locale/messages.pot

Lệnh trên chỉ sử dụng khi khởi tạo, nếu đã từng khởi tạo rồi, thì chỉ cần update nội dung các file dịch với lệnh dưới đây (dùng lệnh trên cũng được nhưng nó sẽ xóa những gì bạn làm trước đó đi và bạn phải làm lại từ đầu):

$ pybabel update -l vi_VN -d ./locale -i ./locale/messages.pot

Các lệnh này sẽ cập nhật kết quả vào file locale\vi_VN\LC_MESSAGES\messages.po. Ở file này, bạn cần quan tâm đến những thông tin tương tự như sau:

msgid: "hello"
msgstr: ""

msgid là text bên trong thẻ transmsgstr là đoạn text tương ứng trong ngôn ngữ cần dịch, ở đây là vi_VN. Việc cần làm lúc này là sửa file trên, thêm lời dịch cho đoạn văn bản. Ví dụ:

msgid: "hello"
msgstr: "xin chào"

Vậy là đã xong, bây giờ bạn cần biên dịch file trên thành 1 file mà Jinja2 có thể hiểu và tích hợp vào HTML:

$ pybabel compile -f -d ./locale

Đến đây là đã hoàn thành công việc. Bây giờ, mỗi thẻ trans trên file template khi hiển thị sẽ được thay thế bằng đoạn văn tương ứng. Ví dụ

<h1>{% trans %}hello{% endtrans %}</h1>

sẽ được thay bằng

<h1>xin chào</h1>

như với ví dụ của tôi.

Trên đây tôi chỉ nêu ra 1 ví dụ với ngôn ngữ vi_VN. Bạn có thể mở rộng với các ngôn ngữ khác. Jinja2 không giới hạn bạn có thể dùng bao nhiêu ngôn ngữ. Điều đó là tùy thuộc vào bạn.

Authentication

Nếu trên trang Web của bạn, có những nơi bạn muốn giới hạn với mọi người, chỉ một số người được quyền truy cập. Bạn có thể sử dụng Authentication. CherryPy cung cấp cả 2 hình thức authentication là Basic và Digest. Dưới đây tôi sẽ trình bày về cách sử dụng Digest authentication, Basic cũng tương tự như vậy. Để bảo vệ trong trang Web của bạn, rất đơn giản, hay thêm vào file config như sau (với trang web của tôi, config là conf/app.conf)

[/protected]
tools.auth_digest.on: True
tools.auth_digest.realm: "realm"
tools.auth_digest.get_ha1: cherrypy.lib.auth_digest.get_ha1_dict_plain({"user": "passwd"})
tools.auth_digest.key: "a565c27146791cfb"

Với config như vậy thì khi truy cập /protected trang Web sẽ yêu cầu người dùng điền username và password để xác thực.

CherryPy cho phép bạn có thể config authentication khác nhau với những đường dẫn khác nhau. Tuy nhiên, có 1 hạn chế là nếu nhiều đường dẫn có cùng authentication thì CherryPy không cung cấp config theo regex hay wildcard nên bạn sẽ phải copy paste những đoạn config giống nhau cho các đường dẫn khác nhau.

Sử dụng SQLAlchemy làm ORM

Như đã được giới thiệu, CherryPy là một Web server đơn thuần. Nó không cung cấp bất cứ một template engine nào cũng như ORM nào. Và chúng ta đã sử dụng Jinja2 làm template engine để phát triển Web. Ở phần này, tôi sẽ giới thiệu việc sử dụng SQLAlchemy làm ORM trong ứng dụng Web viết bằng CherryPy.

Giới thiệu SQLAlchemy

SQLAlchemy là một bộ công cụ SQL của Python và nó cũng là một ORM (Object Relational Mapper) cung cấp cho những người lập trình ứng dụng đầy đủ sức mạnh và sự mềm dẻo của SQL.

Nó cung cấp một bộ đầy đủ các pattern nổi tiếng. Nó được thiết kế cho việc truy cập cơ sở dữ liệu hiệu quả và hiệu suất cao, đồng thời cho phép lập trình với phong cách Pythonic.

Tại sao lại dùng SQLALchemy

Thực ra, lần đầu tiên dữ dụng ORM cho Python, tôi đã sử dụng SQLAlchemy, lúc đầu nó cũng không dễ dùng cho lắm. Nhưng càng đi sâu tìm hiểu, tôi càng thấy nó thú vị. Nó có rất nhiều tính năng hay. Bạn có thể tham khảo ở đây. Ngoài ra SQLAlchemy được sử dụng với rất nhiều hãng công nghệ nổi tiếng như Reddit, Mozilla. Thực sự nó quá đầy đủ và mạnh mẽ. Nó đủ tốt tới mức tôi không tìm thấy lý do gì để phải tìm kiếm một ORM tốt hơn nữa. Vì vậy tôi đã quyết định dùng SQLAlchemy.

Cài đặt vào sử dụng

Cài đặt SQLAlchemy rất đơn giản thông qua pip. Với virtualenv đã được kích hoạt, bạn có thể cài SQLAlchemy với lệnh sau:

$ pip install sqlalchemy

Sau khi cài đặt, bạn có thể import nó giống như các package Python khác.

Sử dụng SQLAlchemy làm plugin cho CherryPy

Sau khi tham khảo một vài nơi, có nhiều người đã viết các tool dùng để tích hợp SQLAlchemy vào CherryPy. Một ví dụ là ở đây. Thường thì các tool này khá giống nhau. Và bạn có thể sử dụng bất cứ tool nào bạn thích. Tất nhiên, tôi chọn các tool có sẵn vì chúng rất đầy đủ và giúp chúng ta có thêm thời gian để tập trung vào phát triển app. Nếu muốn tìm hiểu sâu hơn cũng như muốn sử dụng tool của riêng mình, bạn hoàn toàn có thể tự phát triển nó, không sao cả. Dưới đây là tool tôi sử dụng. Tôi lưu nó trong file lib/tool/db.py

import cherrypy

__all__ = ['SATool']

class SATool(cherrypy.Tool):
    def __init__(self):
        """
        The SA tool is responsible for associating a SA session
        to the SA engine and attaching it to the current request.
        Since we are running in a multithreaded application,
        we use the scoped_session that will create a session
        on a per thread basis so that you don't worry about
        concurrency on the session object itself.
        This tools binds a session to the engine each time
        a requests starts and commits/rollbacks whenever
        the request terminates.
        """
        cherrypy.Tool.__init__(self, 'on_start_resource',
                               self.bind_session,
                               priority=20)

    def _setup(self):
        cherrypy.Tool._setup(self)
        cherrypy.request.hooks.attach('on_end_resource',
                                      self.commit_transaction,
                                      priority=80)

    def bind_session(self):
        """
        Attaches a session to the request's scope by requesting
        the SA plugin to bind a session to the SA engine.
        """
        session = cherrypy.engine.publish('bind-session').pop()
        cherrypy.request.db = session

    def commit_transaction(self):
        """
        Commits the current transaction or rolls back
        if an error occurs.  Removes the session handle
        from the request's scope.
        """
        if not hasattr(cherrypy.request, 'db'):
            return
        cherrypy.request.db = None
        cherrypy.engine.publish('commit-session')

Tool này cần 1 plugin cho CherryPy. Nội dung plugin như sau (lib/plugin/db_plugin.py):

import cherrypy
from cherrypy.process import wspbus, plugins
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

from lib.model import ORBase
from conf import db

__all__ = ['SAEnginePlugin']

class SAEnginePlugin(plugins.SimplePlugin):
    def __init__(self, bus):
        """
        The plugin is registered to the CherryPy engine and therefore
        is part of the bus (the engine *is* a bus) registery.
        We use this plugin to create the SA engine.  At the same time,
        when the plugin starts we create the tables into the database
        using the mapped class of the global metadata.
        """
        plugins.SimplePlugin.__init__(self, bus)
        self.sa_engine = None
        self.session = scoped_session(sessionmaker(autoflush=True,
                                                   autocommit=False))

    def start(self):
        self.bus.log('Starting up DB access')
        self.sa_engine = create_engine(db.url, echo=db.echo)
        #TODO: Make this configurable
        self.create_all()
        self.bus.subscribe("bind-session", self.bind)
        self.bus.subscribe("commit-session", self.commit)

    def stop(self):
        self.bus.log('Stopping down DB access')
        self.bus.unsubscribe("bind-session", self.bind)
        self.bus.unsubscribe("commit-session", self.commit)
        if self.sa_engine:
            #self.destroy_all()
            self.sa_engine.dispose()
            self.sa_engine = None

    def bind(self):
        """
        Whenever this plugin receives the 'bind-session' command, it applies
        this method and to bind the current session to the engine.
        It then returns the session to the caller.
        """
        self.session.configure(bind=self.sa_engine)
        return self.session

    def commit(self):
        """
        Commits the current transaction or rollbacks if an error occurs.
        In all cases, the current session is unbound and therefore
        not usable any longer.
        """
        try:
            self.session.commit()
        except:
            self.session.rollback()
            raise
        finally:
            self.session.remove()

    def create_all(self):
        self.bus.log('Creating database')
        from lib.model.user import User
        ORBase.metadata.create_all(self.sa_engine)

    def destroy_all(self):
        self.bus.log('Destroying database')
        ORBase.metadata.drop_all(self.sa_engine)

Sau khi có tool trên rồi, bạn cần kích hoạt database cho CherryPy bằng config như sau (conf/app.conf):

[/]
tools.db.on: True

Và ở file server.py, bạn cần import và khởi tạo tool này.

from lib.tool import db
from lib.plugin import db_plugin

def __init__(self):
    engine = cherrypy.engine
    cherrypy.tools.db = db.SATool()
    engine.db = db_plugin.SAEngine(engine)
    engine.db.subscribe()

Vậy là bạn đã có thể sử dụng SQLALchemy làm ORM như một plug in cho CherryPy. Bây giờ bạn có thể tạo các model và controller cho chúng.

Tạo model

Tôi sẽ tạo các model trong thư mục lib/model. Trước hết, tôi khởi tạo những ORBase để sử dụng sau này

from sqlalchemy.ext.declarative import declarative_base

__all__ = ["ORBase"]

ORBase = declarative_base()

Bây giờ, thử tạo 1 model user như sau:

import cherrypy
import sqlalchemy
from sqlalchemy import Column, UniqueConstraint, Index
from sqlalchemy.types import Integer, String, Boolean

from lib.model import ORBase

__all__ = ["User"]


class User(ORBase):
    __tablename__ = "users"

    user_id = Column(Integer, nullable=False, primary_key=True)
    name = Column(String(20), nullable=False)
    fullname = Column(String(50))
    email = Column(String(50))
    password = Column(String(128))
    Index(user_id)

Với khởi tạo model user như trên, SQLAlchemy như tạo một table trên database là users với các trường được định nghĩa và đánh index cho trường user_id. Ngoài ra, trong file model này, bạn có thể định nghĩa các thuộc tính cũng như phương thức của model sau này chúng sẽ được sử dụng trên controller.

Tương tác với database

Bây giờ, SQLAlchemy đã được plug vào CherryPy, nên bạn có thể thao tác các query với database thông qua cherrypy.request.db. Dưới đây, tôi sẽ giới thiệu một số thao tác cơ bản với database.

Lấy các bản ghi từ database
cherrypy.request.db.query(User)

Nếu muốn thêm điều kiện nào đó, bạn có thể sử dụng thêm filter. Ví dụ

cherrypy.request.db.query(User).filter(User.user_id < 100)

Kết quả trả về của các câu query trên sẽ là object chứ danh sách các đối tượng tương ứng với các bản ghi trên database. Nếu muốn lấy bản ghi đầu tiên, bạn có thể thêm .first().

cherrypy.request.db.query(User).filter(User.user_id < 100).first()

Nếu muốn sắp xếp, bạn có thể dùng order_by.

cherrypy.request.db.query(User).filter(User.user_id < 100).order_by(User.name.desc())

Nếu muốn sử dụng các điều kiện phức tạp hơn, bạn có thể kết hợp chúng bằng các phép toán logic như and_, or_, not_

Thêm bản ghi vào database
# khởi tạo object ở đây
new_user = User()
cherrypy.request.db.add(new_user)
cherrypy.request.db.commit()
Xóa bản ghi khỏi database
user = # lấy user từ database
cherrypy.request.db.delete(user)
cherrypy.request.db.commit()
Update bản ghi đang có
user = # lấy user từ trong database.
user.update()
cherrypy.request.db.add(user)
cherrypy.request.db.commit()

Như các bạn đã thấy, mọi thao tác với database đều kết thực với cherrypy.request.db.commit(). Thao tác này sẽ yêu cầu SQLAlchemy thao tác lệnh với database, trong khi các thao trước đó mới chỉ được lưu tạm thời. Nếu không có bước commit này, tất cả các thao tác trước đó sẽ không làm thay đổi database. SQLAlchemy không yêu cầu bạn phải commit từng thao tác của mình. Bạn có thể để một vài thay đổi được commit cùng 1 lúc cũng không sao cả. Điều đó phụ thuộc vào bạn và app của bạn.

Nếu bạn không muốn phải thực hiện commit bằng tay như vậy, bạn có thể config cho SQLAlchemy tự động thực hiện điều đó, bằng cách sử dụng option autocommit=True khi tạo session cho SQLAlchemy.

scoped_session(sessionmaker(autoflush=True, autocommit=True))

Các thông tin về SQLAlchemy bạn có thể xem thêm tại http://www.sqlalchemy.org.

Demo

Bạn có thể xem một demo ở đây.

Đây là demo một trang Web đơn giản sử dụng các kiến thức trong bài viết này. Sử dụng Cherrypy, Jinja2 và SQLALchemy. Trang Web có thể chuyển đổi 2 ngôn ngữ Việt và Anh.

Kết luận

CherryPy cùng với Jinja2 và SQLALchemy tạo thành 1 bộ công cụ rất tốt để phát triển Web. Có thể nó hơi khó khăn khi mới bắt đầu. Nhưng sau khi tìm hiểu kỹ, tôi tìn rằng, bạn sẽ thích nó ngay. Và biết thêm framework mới, bạn có nhiều lựa chọn hơn khi muốn viết một trang Web cho riêng mình, và bạn sẽ tìm được framework thích hợ nhất. Chúc thành công.

Tài liệu tham khảo

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.