Thiết kế Soft Delete pattern trong Flask và SQLAlchemy

Thanks https://unsplash.com/photos/7ySd00IGyx4

Mỗi lần lập trình các chức năng để xóa dữ liệu record trong dữ liệu thì mình hồi hộp lắm. Đầu tiên, chỉ cần sai sót một chút thì dữ liệu ra đi không trở lại. Thứ 2, mình không biết dữ liệu được xóa đi có cần thiết khi hệ thống cần audit trong tương lai không. Theo mình biết thì một số hệ thống làm tài chính, hoặc dự án chính phủ thì việt truy xuất lại dữ liệu, log của 6 tháng hoặc 1 năm trước là hoàn toàn bình thường.

Khi đấy có lẽ thiết kế Soft delete là một giải pháp hợp lý. Tuy nhiên mình nghĩ soft delete cũng có ưu điểm và khuyết điểm nên khi sử dụng cũng phải cân nhắc cẩn thận

Tham khảo: http://abstraction.blog/2015/06/28/soft-vs-hard-delete

Soft Delete Pattern

Gần đây mình có làm dự án sử dụng Flask framework & SQLAlchemy để kết nối đến Postgres nên muốn chia sẻ một chút về thiết kế soft delete trong SQLAlchemy.

Để đơn giản thì ta chỉ cần thêm một field thường flag là deleted dạng bool để lưu thông tin record đã bị xóa hay chưa.

Tuy nhiên, mình thường dùng cột 1 timestamp là deleted_at lưu thời gian xóa record thay cho flag deleted . Nhằm mục đích quản lý được thời gian dữ liệu bị xóa, khi cần thì có thể query lọc theo ngày, giờ nhất định. Như thế sẽ tiện cho việc thống kê, phân tích sau này.

Ví dụ: Mình có một model user như sau

models/user.py

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column('id',
                   db.BigInteger().with_variant(sqlite.INTEGER(), 'sqlite'),
                   primary_key=True)
    email = db.Column('email', db.String(255), nullable=False)
    password = db.Column('password', db.String(255), nullable=False)

Cách đơn giản nhất là thêm cột mới deleted_at vào bảng users rồi khi query chỉ lọc lấy những record có deleted_at là null.

Nhưng như thế thì sẽ không tối ưu được code + mỗi câu query mình phải thêm bộ lọc như trên vào

Để tối ưu hóa code mình sẽ thiết kế một class abstract tên là SoftDeleteModel với query_class là một class đã được tùy biến lọc deleted_at != null

soft_delete_model.py


from flask_sqlalchemy import BaseQuery

from ..database import db


class QueryWithSoftDelete(BaseQuery):
    def __new__(cls, *args, **kwargs):
        obj = super(QueryWithSoftDelete, cls).__new__(cls)

        if len(args) > 0:
            super(QueryWithSoftDelete, obj).__init__(*args, **kwargs)
            return obj.filter_by(deleted_at=None)
        return obj

    def __init__(self, *args, **kwargs):
        pass


class SoftDeleteModel(db.Model):
    __abstract__ = True

    __soft_delete__ = True

    # override default query class
    query_class = QueryWithSoftDelete

    deleted_at = db.Column('deleted_at', db.TIMESTAMP, nullable=True)

Khi đấy model User sẽ được viết lại như thế này

from .soft_delete_model import SoftDeleteModel

class User(SoftDeleteModel)
    __tablename__ = 'users'

    id = db.Column('id',
                   db.BigInteger().with_variant(sqlite.INTEGER(), 'sqlite'),
                   primary_key=True)
    email = db.Column('email', db.String(255), nullable=False)
    password = db.Column('password', db.String(255), nullable=False)

Chỉ cần kế thừa model SoftDeleteModel thì User sẽ có tính năng Soft Delete. Đơn giản quá đúng không?!

Kết

Tóm lại, trong bài viết mình đã giới thiệu về cách implement Soft Delete pattern trong Flask framework & SQLAlchemy. Nếu bạn có feedback gì thì xin để lại comment hoặc liên lạc cho mình theo email tuantranf{@}gmail.com nhé.

Tham khảo:

https://blog.miguelgrinberg.com/post/implementing-the-soft-delete-pattern-with-flask-and-sqlalchemy

http://abstraction.blog/2015/06/28/soft-vs-hard-delete

https://medium.com/@chrissoemma/laravel-5-8-delete-and-soft-delete-practical-examples-b9b71c0a97f

Hãy cho trẻ cơ hội học chấp nhận và bình thường hóa mọi việc

Holiday song

Đây là bài hát của con gái 3 tuổi, tự sáng tác trong chuyến đi Yên Tử cuối tuần vừa rồi. Bài hát có nội dung chỉ vài dòng nhưng tâm huyết lắm. Lời bài hát có kèm cả hình minh họa. Mới nhìn bố mẹ cháu cứ tưởng kẹo mút nhưng hóa ra là bóng bay.

Chuyến đi lần này vợ chồng mình đã cho con được thật nhiều trải nghiệm mới lạ như chợ quê, nhảy sạp, đi cáp treo, … Nhưng mình ấn tượng nhất là khi đang leo núi thì gặp mưa to. Nhà mình leo lên được một chặng thì trời mưa như trút nước. Lúc đầu, mình bắt đầu thấy lo vì trời mưa càng lúc càng to, gió mỗi lúc một lớn, trời cũng lạnh hơn. Cũng hơi hối hận vì không chuẩn bị cẩn thận hơn khi đi.

Con gái thấy mưa to, gió lớn cũng hơi sợ khóc nhè một chút nhưng nhờ mẹ và mọi người bên cạnh động viên thì hết buồn lại quay ra nghịch nước mưa rất thích thú.

Mình chợt nhớ lại ra một đoạn trong cuốn “Dám bị ghét” – Bạn bất hạnh không phải do quá khứ và hoàn cảnh, càng không phải do thiếu năng lực. Bạn chỉ thiếu “can đảm” mà thôi. Nói một cách khác, bạn không đủ “can đảm” để dám hạnh phúc.”

Hiểu nôm na là bản thân hoàn cảnh khó khăn không quan trọng bằng cách con người nhìn nhận nó như thế nào. Nếu bạn đủ can đảm để chọn hạnh phúc thì bạn sẽ hạnh phúc.

Dù chuẩn bị kỹ lưỡng đến thế nào thì không phải chuyến đi nào cũng thành công mỹ mãn. Những yếu tố khách quan như thời tiết, điều kiện giao thông là điều không tránh khỏi. Nhưng đây cũng chính là cơ hội để bố mẹ tận dụng biến nguy thành cơ để giúp trẻ có positive thinking, chấp nhận các vấn đề một cách tích cực hơn.

Mình nghĩ đi du lịch thì ngoài những cơ hội cho con có những trải nghiệm mới thì tất cả những khó khăn, vấp ngã trong chuyến đi cũng sẽ giúp trẻ học được cách bình thản, chấp nhận mọi chuyện như một lẽ đương nhiên và bình thường hóa tất cả mọi chuyện. 

Linux Cron thực tiễn – Tất tần tật những gì bạn cần biết

You have a cron – you have a job
Image from: https://www.reddit.com/r/ProgrammerHumor/comments/6ng8f7/cron_job/

Đợt vừa rồi hết cách ly xã hội dự án lại đi vào phase release, nên lu bu quá không viết được bài nào. Hôm nay mình sẽ note lại một số kỹ thuật chú ý khi dùng Cron job trong Linux như thiết định job theo giờ, cài đặt crontab trong Docker, thiêt định job theo đơn vị giây, …

Cron là gì

Cron job là những tác vụ, chương trình cần chạy hằng ngày, hằng giờ hoặc vài phút 1 lần, …

Trong Linux (Ubuntu) thì crontab là daemon để quản lý thiết định cron. Bài này của mình chủ yếu trình bày các thiết định trên Ubuntu nếu bạn cần thiết định trên CentOS, Redhat thì có thể trao đổi với mình hoặc chịu khó google một chút sẽ tìm thấy cách làm tương ứng ở hệ điều hành khác.

Các file thiết định cron

/etc/
├── cron.d
│   ├── anacron
├── cron.daily
├── cron.hourly
├── cron.monthly
├── crontab
├── cron.weekly

cron.d

Thông thường mọi người sẽ tạo quản lý file thiết định tại thư mục này.

cron.***

Các file thiết định được chia theo ngày, tháng, giờ. Bạn có thể tạo file thiết định trong các thư mục này cho dễ quản lý. Tuy nhiên theo kinh nghiệm của mình thì nên dùng cron.d thì các file thiết định tập trung một chỗ sẽ dễ quản lý hơn.

crontab

Đây là file thiết định chính của cron trên Linux, ngoài ra bạn có thể dùng command crontab để thiết đinh.

Tạo một cron của bạn

Hãy thử tạo một cron job bằng cách tạo 1 file trong thư mục /etc/cron.d. Mình khuyến khích nên copy từ file crontab và chỉnh sửa

$ cp /etc/crontab /etc/cron.d/test_cron

Xong. bạn đã có 1 file thiết định cron. Đơn giản quá phải không?

Tiếp theo mình sẽ cũng xem chi tiết cách thiết định thời gian, chương trình chạy, và khởi động file cron vừa tạo nhé.

Cách viết cron

Hãy check nội dung file /etc/cron.d/test_cron

# cat /etc/cron.d/test_cron
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow user  command
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#

Hãy thử thay đổi nội dung file test_cron như sau.

# m h dom mon dow user  command
Minute Hour DayOfMonth Month DayOfWeek User Command

# every 1 minute
* *    * * *  root   touch /home/ubuntu/test.txt

# every minute in 1:00〜1:59 everyday
* 1    * * *  root   touch /home/ubuntu/test.txt

# 1:00 AM everyday
0 1    * * *  root   touch /home/ubuntu/test.txt

# at 00:00 from day 12 to 20 every month
0 0    12-20 * *  root   touch /home/ubuntu/test.txt

# at 00:00 from Monday to Friday every week
0 0    * * 1-5  root   touch /home/ubuntu/test.txt
#

Chú ý 1: cú pháp sử dụng Tab, Space trong thiết đinh. Vị trí của Tab,Space như sau nhé.

*[Space]*[Tab]*[Space]*[Space]*[Tab]root[Tab]Commands

Chú ý 2: Cuối file thiết định cron luôn thêm một dòng trống.

Cập nhật thay đổi Cron

Cập nhật thay đổi của cron bằng cách khởi động lại dịch vụ (daemon) cron.

$ sudo service cron restart

Thiết định file log cho cron

Dùng syslog

Thêm thiết định cho phép syslog thu thập cron log bằng cách bỏ comment out trong file thiết định syslog.

Nhớ khởi động lại service rsyslog sau khi lưu file thiết định. Log này sẽ giúp bạn debug cron nếu xảy ra lỗi trong file thiết định

$ vim /etc/rsyslog.d/50-default.conf

# Remove this comment out
# cron.*                          /var/log/cron.log

$ sudo service rsyslog restart

Thiết định log cho command trong Cron

Mình hay thiết định log cho các tác vụ cron để đảm bảo job chạy đúng cho phép xem lại log khi có lỗi hay vấn đề gì.

Ta có thể thay đổi file /etc/cron.d/test_cron như sau

* *    * * *  root   touch /home/ubuntu/test.txt >> /var/log/test-cron.log 2>> /var/log/test-cron-error.log

File /var/log/test-cron.log sẽ chứa các output thường của cron job. File /var/log/test-cron-error.log sẽ chứa các lỗi khi thực hiện cron job

Thiết định cron job theo đơn vị giây

Thông thường, cron job chỉ cho phép thiết định theo đơn vị phút. Trường hợp bạn cần thiết định 10 giây chạy một lần thì phải làm thế nào?

Chúng ta cần một trick khi thiết định.

Hãy thử thay đổi file test_cron để chạy 10 giây 1 lần nhé.

# for i in `seq [second_from] [num_of_seconds] [second_to]`;do (sleep ${i}; [your command] ) & done;
# every 10 seconds
* *    * * *  root   for i in `seq 0 10 59`;do (sleep ${i}; touch /home/ubuntu/test.txt ) & done;

Xong. Quá đơn giản phải không? Ta sẽ thiết định một vòng for chạy từ giây second_from đến giây thứ second_to theo khoảng cách num_of_seconds. Trong ví dụ trên, mình đã thiết định vòng for chạy theo seq từ giây 0 đến giây 59 theo khoảng cách 10 giây 1. Bạn có thể sửa lại cho phép chạy 5s 1 lần hay từ giây thứ 10 đến giây thứ 30, …

Bạn còn có thể dùng cấu trúc while … do để thiết định. Ví dụ:

while : ; do sleep 10 ; some_command || break ; done

Thiết định cron duy nhất 1 process 1 lần

Một số trường hợp bắt buộc chỉ được chạy job 1 process 1 lần tránh bị duplicate

Đối với Ubuntu bạn có thể dùng run-one hoặc flock để đảm bảo chỉ 1 process duy nhất. Ví dụ

* *    * * *  root   run-once touch /home/ubuntu/test.txt >> /var/log/test-cron.log 2>> /var/log/test-cron-error.log
* *    * * *  root   run-once flock -n /tmp/test-cron.lock  /home/ubuntu/test.txt >> /var/log/test-cron.log 2>> /var/log/test-cron-error.log

Bạn có thể tìm hiểu thêm thêm về run-one hoặc flock nếu cần.

Đọc biến môi trường từ file thiết định bên ngoài

Bạn cũng có thể thiết định cron job cho phép đọc các thiết định từ file bên ngoài. Mình thường dùng các file .env để quản lý môi trường

Ví dụ: Mình sẽ lưu khoảng cách thực hiện 10 giây ở ví dụ trên vào file /home/ubuntu/test-cron.env và load nó khi chạy cron job.

TIME=10

Sửa lại file thiết định test_cron

# Thiết định Shell type là bash
SHELL=/bin/bash
# Load biến môi trường TIME từ file .env trước khi chạy cronjob
* *    * * *  root   for i in `source /home/ubuntu/test-cron.env && seq 0 ${TIME} 59`;do (sleep ${i}; touch /home/ubuntu/test.txt ) & done;

Thiết định cron job cho trong Dockerfile

Bạn có thể tham khảo Stackoverflow topic sau

https://stackoverflow.com/questions/37458287/how-to-run-a-cron-job-inside-a-docker-container

Ví dụ trường hợp test-cron thì có thể thiết định như sau.
Dockerfile

FROM ubuntu:latest

RUN apt-get update && apt-get -y install cron

# Copy test-cron file to the cron.d directory
COPY test-cron /etc/cron.d/test-cron

# Give execution rights on the cron job
RUN chmod 0644 /etc/cron.d/test-cron

# Apply cron job
RUN crontab /etc/cron.d/test-cron

# Create the log file to be able to run tail
RUN touch /var/log/cron.log

# Run the command on container startup
CMD ["cron", "-f"]

File test-cron sẽ được sửa như sau để đảm bảo redirect log sang standard output của Docker.

* *    * * *  root   run-once touch /home/ubuntu/test.txt > /proc/1/fd/1 2>/proc/1/fd/2

Conclusion

Mình đã chia sẻ tất tần tật những kinh nghiệm thiết định cron job mình cóp nhặt được. Nếu bạn làm chủ được các kỹ thuật trên thì có thể mạnh dạn áp dụng vào sản phẩm hiện có, hoặc tự tin chém gió khi phỏng vấn.

Happy hacking.

Bài viết có tham khảo từ: