Dùng Flask hay Django?

DjangoFlask là hai cái tên được nhắc đến nhiếu nhất khi nói về web framework của Python, đặc biệt là giữa những người mới bước chân vào thế giới Python. "Dùng Flask hay Django?" cũng là câu hỏi mà tôi hay nghe đi nghe lại trên các diễn đàn trao đổi, hỏi bài. Với kinh nghiệm về 2 thứ này, tôi viết một bài ngắn gọn, mong trả lời trước cho những ai mới chập chững bị "rắn cắn" này.

Flask_vs_Django Image credit: coderseye.com

Về Flask, tôi đã 3 lần sử dụng Flask cho phần mềm của mình. Lần sử dụng đầu tiên, khoảng năm 2013, tôi làm cho một tay nghiên cứu sinh về Big Data, khi anh chàng này viết một phần mềm xử lý dữ liệu lớn (Java + Hadoop) và cần một trang web để làm giao diện điều khiển cho việc chọn dữ liệu nguồn và lấy kết quả về sau khi xử lý. Lần tiếp theo, tôi viết server phân quyền cho cổng login của mạng wifi (có các tên gọi khác là Splash page, Captive Portal, Wifi Marketing), và sản phẩm cuối cùng tôi dùng Flask là để tạo trang blog này (viết lai rai từ 2013 đến nay).

Năm đầu tiên tôi đụng đến Django cũng là 2013 và từ đó đến nay liên tục sử dụng nó trong 6 dự án khác nhau (ngoại trừ dự án đầu tiên thì tất cả những dự án về sau tôi nắm quyền quyết định công nghệ và là người làm chính).

Sau khi đã thử khá đủ với cả hai thì bây giờ, nếu bắt đầu ứng dụng web mới và được hỏi "dùng Flask hay Django" thì 90% tôi sẽ chọn Django. Tại sao?

Flask, đúng như cách nó tự gọi, "the Python micro framework", chỉ phù hợp với những ứng dụng rất nhỏ. Mặc dù người ta nói có thể dùng cho dự án lớn, nhưng đó chỉ là "có thể", chứ không phải là "thuận tiện" hay "tốt". Ranh giới để tôi phân định khi nào dùng Flask, khi nào dùng Django là: Tất cả các view có thể nhét vừa 1 file Python hay không. Khi lượng code bắt đầu lớn lên, không thể nhét vừa một file nữa, ta sẽ phải suy nghĩ cách tổ chức code sao cho khi cần tìm chỗ nào để sửa thì biết được nhanh nó nằm ở thư mục nào, file nào. Đã có nhiều người nỗ lực tìm cách tổ chức bộ code Flask cho dự án lớn và bất ngờ thay, dần dần người ta đi đến một cách tổ chức y hệt Django!

Lấy ví dụ đây là cách Flask định nghĩa route:

@app.route('/user/<username>')
def profile(username):
    ...

@app.route('/<int:year>/<int:month>/<title>')
def article(year, month, title):
    ...

Mỗi route sẽ được viết kế bên hàm view. Khi code nhiều đến một mức nào đó, ta sẽ có 10 file views, và các dòng định nghĩa route sẽ nằm rải rác trong 10 file, và có thể 10 file đó sẽ nằm trong 5 thư mục khác nhau! Điều này gây ra khó khăn khi ta cần tìm hàm view theo URL. Ví dụ một ngày đẹp trời ta được hệ thống thu thập log thông báo có một URL bị lỗi 500:

sentry_log

Thế nhưng URL đó tương ứng với hàm view nào? Nếu ta có một website nhiều chức năng với nhiều nhánh URL khác nhau, mà các dòng khai báo route lại nằm rải rác thì thật mất thời gian để tìm kiếm.

Trong khi đó, Django gom tất cả dòng khai báo URL vào chung file và đặt tên file theo quy ước, giúp việc tìm kiếm trở nên thuận tiện:

django_urls

Khi website viết bằng Flask dần trở lên lớn, rốt cuộc tôi cũng phải tổ chức nó lại theo cách của Django. Từ đó tôi đặt câu hỏi, tại sao phải dùng Flask để rồi sau này phải làm lại những việc mà Django đã làm, thậm chí làm tốt hơn?

Còn một khía cạnh khác, là phong cách code của "hệ sinh thái" xung quanh Django khá đồng nhất, khiến việc học sử dụng những thư viện dành cho Django rất trơn tru. Do được thiết kế cho website lớn từ đầu, Django đã "nhúng tay" vào gần như mọi góc cạnh thường gặp trong việc xây dựng web, từ mô hình dữ liệu đến xử lý form, sinh mã HTML cho form, và dần dần hội tụ chúng về được một phong cách chung. Một nét đặc trưng của Django là việc sử dụng class Meta, ví dụ trong model:

class Membership(models.Model):
    user = ForeignKey(settings.AUTH_USER_MODEL, CASCADE)
    business = ForeignKey(Business, CASCADE)
    designation = models.CharField(max_length=200, null=True)

    class Meta:
        unique_together = ('user', 'business')
        verbose_name_plural = 'membership'
        ordering = ['id']

Khi định nghĩa form từ model, ta gặp lại class Meta:

class RecentFundingForm(forms.ModelForm):
    class Meta:
        model = RecentFunding
        fields = ('amount', 'funder', 'business')
        labels = {
            'amount': _('Amount (RM)')
        }
        widgets = {
            'business': forms.HiddenInput()
        }

Đó là hai loại đối tượng có sẵn của Django, từ đó các thư viện ngoài cũng học theo, ví dụ đây là serializer trong Django-Rest-Framework:

class BusinessSerializer(ModelSerializer):
    stage_title = CharField(read_only=True, source='stage.title')

    class Meta:
        model = Business
        fields = ('id', 'title', 'stage', 'stage_title')

Và đây là Django-Tables2:

class ApplicationBaseTable(tables.Table):
    no = tables.Column(empty_values=(), orderable=False)
    stage = tables.Column(accessor=tables.A('business.stage'))
    judges_progress = tables.Column(_("Judges' progress"))
    # Override this
    action = tables.Column()
    judges = tables.Column(orderable=False)
    judges_progress = tables.Column(orderable=False)

    class Meta:
        model = Application
        fields = ('no', 'business', 'stage', 'status',
                  'judges', 'judges_progress', 'final_score', 'action')
        attrs = {'class': 'table table-bordered'}

Tiếp đến, Django Filter:

class DeviceFirmwareFilter(filters.FilterSet):
    hardware_id = filters.CharFilter(label='Hardware ID', method='filter_by_hardware_id')
    # Current version of device
    version = filters.CharFilter(label='Version', method='search_by_version',
                                 validators=[validate_version_string])
    hardware_model = filters.ModelChoiceFilter(label='Hardware Model',
                                               queryset=HardwareModel.objects.all(),
                                               to_field_name='name')

    class Meta:
        model = DeviceFirmware
        fields = ('device_type', 'version', 'hardware_id', 'hardware_model')

Flask, theo hướng tiếp cận khác, không muốn ôm đồm nhiều quá, một mặt có lợi là cho phép người ứng dụng tự do hơn trong việc lựa chọn những thành phần cần thiết, nhưng mặt khác, không xây dựng được một "hệ sinh thái" đủ mạnh, nhất quán.

Lấy ví dụ, về khoản truy cập cơ sở dữ liệu, Flask không có bộ ORM riêng như Django. Thay vào đó, người dùng Flask thường dùng chung Flask với SQLAlchemy, một bộ ORM độc lập, có phong cách thiết kế khác với Django. Vấn đề là, do không được định hướng một phong cách riêng, người dùng SQLAlchemy với Flask lại "nhớ nhung" Django và kết quả là sự ra đời của Flask-SQLAlchemy, một thư viện giúp kết hợp SQLAlchemy với Flask, nhưng lại nhái phong cách của Django. Đây là một giải pháp nửa chừng, không triệt để nên không phát huy được sức mạnh của SQLAlchemy, dẫn đến tình trạng đôi khi, người ta có khi phải dùng song song hai phong cách, "nhái Django" và "thuần SQLAlchemy" trong một dự án web. Chưa kể, model định nghĩa trong Flask-SQLchemy không tạo được sự kế thừa để ứng dụng lại trong các thành phần khác, giống kiểu model Django giúp tiết kiệm code cho form, serializer v.v...

Trên đây tôi đã kể một số điểm lợi của Django. Tất nhiên điều đó không có nghĩa là Flask không có đất diễn. Với xu hướng làm web gần đây, sự tách bạch backend - frontend, sự chia nhỏ thành các microservice khiến cho mỗi bộ code web không phức tạp như trước nữa, nên những gì Flask có là đủ dùng.

-- Cập nhật --

Ngày nay, khi cần một microframework, tôi khuyến nghị dùng Litestar thay vì Flask. Cũng có thể dùng FastAPI nhưng FastAPI không được cập nhật thường xuyên bằng.