diff --git a/docs/i18n/rules/vi/DOL011.md b/docs/i18n/rules/vi/DOL011.md index 8580f5b..9e16ea9 100644 --- a/docs/i18n/rules/vi/DOL011.md +++ b/docs/i18n/rules/vi/DOL011.md @@ -1,24 +1,21 @@ -# DOL011 — Thêm `db_index=True` cho trường `ForeignKey` dùng trong `filter()` / `order_by()` +# DOL011 — null=True trên CharField/TextField -**Mức độ mặc định:** warning · **Khả năng áp dụng:** unsafe · **Danh mục:** model-definition +**Mức độ mặc định:** warning · **Khả năng áp dụng:** suggestion · **Danh mục:** model -Phát hiện các khai báo `ForeignKey` (và `OneToOneField`) xuất hiện trong các lời gọi `filter()`, `exclude()` hoặc `order_by()` ở nơi khác trong cùng file, nhưng khai báo trường đó không có `db_index=True`. Django tự động tạo index cho `ForeignKey`, nhưng chỉ trên chính cột đó — các pattern xuyên file hoặc đa bảng không được phát hiện. Khi FK là trục filter chính (ví dụ `orders.filter(customer=c)`), index ngầm định thường đủ dùng; rule này kích hoạt khi có thể xác nhận tĩnh rằng FK đang được filter mà không có khai báo index tường minh — đây là trường hợp có khả năng cao nhất bị bỏ sót index. - -Khả năng áp dụng là `unsafe` vì thêm index là một thay đổi schema: trên các bảng lớn, cần tạo index đồng thời (concurrent index build) và cửa sổ deploy phù hợp. +Phát hiện `null=True` trên `CharField` hoặc `TextField`. Chính tài liệu của Django cũng khuyên không nên làm vậy: một cột chuỗi cho phép null sẽ có hai giá trị "không có dữ liệu" khác biệt — `NULL` và chuỗi rỗng `''` — do đó mọi đoạn code tiêu thụ đều phải kiểm tra cả hai, và các truy vấn như `field=''` sẽ âm thầm bỏ sót các hàng `NULL`. Quy ước của Django là dùng cột `NOT NULL` kết hợp với `blank=True` để cho phép tùy chọn ở tầng form, và lưu `''` cho các giá trị bị thiếu. QuickFix ("Thay thế null=True bằng blank=True") sẽ đổi kwarg tại chỗ; đây là một đề xuất (suggestion) vì thay đổi này yêu cầu migration và, trên dữ liệu hiện có, cần một bước backfill để chuyển `NULL` thành `''`. ## Sai ```python -class Order(models.Model): - customer = models.ForeignKey(Customer, on_delete=models.CASCADE) - # ở nơi khác: Order.objects.filter(customer=c) — chỉ dựa vào index ngầm định +class Profile(models.Model): + bio = models.TextField(null=True) ``` ## Đúng ```python -class Order(models.Model): - customer = models.ForeignKey(Customer, on_delete=models.CASCADE, db_index=True) +class Profile(models.Model): + bio = models.TextField(blank=True) ``` ## Bỏ qua (Suppress) @@ -27,4 +24,4 @@ class Order(models.Model): # django-orm-lens-disable-next-line DOL011 ``` -Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL011": "off"}}`. +Trường hợp ngoại lệ hợp lệ: các trường chuỗi `unique=True` nơi nhiều giá trị bị thiếu không được phép trùng lặp — hãy bỏ qua (suppress) ở đó. Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL011": "off"}}`. diff --git a/docs/i18n/rules/vi/DOL012.md b/docs/i18n/rules/vi/DOL012.md index dcf0fd8..f3d2560 100644 --- a/docs/i18n/rules/vi/DOL012.md +++ b/docs/i18n/rules/vi/DOL012.md @@ -1,24 +1,24 @@ -# DOL012 — Thêm `db_index=True` cho các trường dùng thường xuyên trong `order_by()` +# DOL012 — Model không có phương thức __str__ -**Mức độ mặc định:** info · **Khả năng áp dụng:** unsafe · **Danh mục:** model-definition +**Mức độ mặc định:** info · **Khả năng áp dụng:** suggestion · **Danh mục:** model -Phát hiện các trường model (ngoại trừ `ForeignKey` đã được DOL011 xử lý) xuất hiện làm đối số duy nhất trong các lời gọi `order_by()` từ ba lần trở lên trong cùng file, mà không có khai báo `db_index=True` hoặc `unique=True`. Sắp xếp lặp lại trên cột không có index sẽ buộc database thực hiện filesort cho mỗi query; một index sẽ chuyển điều đó thành index scan. - -Khả năng áp dụng là `unsafe` vì thêm index là một thay đổi schema. +Phát hiện một lớp kế thừa từ `models.Model` mà phần thân không định nghĩa phương thức `__str__` (các model trừu tượng với `abstract = True` trong `Meta` sẽ bị bỏ qua). Nếu không có `__str__`, các danh sách thay đổi trong admin, dropdown của `ForeignKey`, chuỗi hiển thị trên shell và `{{ obj }}` trong template đều sẽ hiển thị dạng `ModelName object (1)` — vô dụng với người đọc và gây khó khăn khi debug. Không có QuickFix: để tạo ra một thân hàm có ý nghĩa, bạn cần chọn trường nào để hiển thị, và chỉ có bạn mới làm được điều này. ## Sai ```python class Article(models.Model): - published_at = models.DateTimeField() - # ở nơi khác: Article.objects.order_by("published_at") — từ ba lần trở lên + title = models.CharField(max_length=255) ``` ## Đúng ```python class Article(models.Model): - published_at = models.DateTimeField(db_index=True) + title = models.CharField(max_length=255) + + def __str__(self) -> str: + return self.title ``` ## Bỏ qua (Suppress) diff --git a/docs/i18n/rules/vi/DOL013.md b/docs/i18n/rules/vi/DOL013.md index a9478dc..8da6edf 100644 --- a/docs/i18n/rules/vi/DOL013.md +++ b/docs/i18n/rules/vi/DOL013.md @@ -1,24 +1,21 @@ -# DOL013 — Dùng `select_related` cho các truy cập `ForeignKey` / `OneToOneField` trong serializer +# DOL013 — ForeignKey thiếu on_delete -**Mức độ mặc định:** warning · **Khả năng áp dụng:** unsafe · **Danh mục:** model-definition +**Mức độ mặc định:** error · **Khả năng áp dụng:** suggestion · **Danh mục:** model -Phát hiện các trường serializer Django REST Framework (hoặc truy cập thuộc tính thông thường) duyệt qua `ForeignKey` hoặc `OneToOneField` mà không có `select_related()` tương ứng trên queryset truyền vào serializer. Mỗi lần duyệt mà không có prefetching sẽ kích hoạt một query riêng biệt cho mỗi đối tượng — đây là N+1 kinh điển xảy ra ở tầng serialization thay vì tầng view. - -Khả năng áp dụng là `unsafe` vì cần sửa queryset tại call site, có thể nằm ở một file khác. +Phát hiện một lời gọi `ForeignKey(...)` không có từ khóa `on_delete=`. `on_delete` là bắt buộc kể từ Django 2.0 — nếu bỏ qua nó, lỗi `TypeError` sẽ xảy ra ngay thời điểm module model được load, do đó lỗi này được bắt ngay lúc soạn code trước khi bạn chạy ứng dụng. QuickFix ("Thêm on_delete=models.CASCADE (chỉnh sửa theo policy của bạn)") sẽ chèn `on_delete=models.CASCADE` dưới dạng một template. Đây là một đề xuất (suggestion), không phải `safe`: chính sách xóa là một quyết định thiết kế thực sự — `CASCADE` âm thầm xóa các bản ghi phụ thuộc, trong khi `PROTECT`, `SET_NULL`, `SET_DEFAULT`, hoặc `DO_NOTHING` có thể mới là những gì dữ liệu thực sự cần. ## Sai ```python -class OrderSerializer(serializers.ModelSerializer): - customer_name = serializers.CharField(source="customer.name") - # queryset: Order.objects.all() — thêm một query cho mỗi order +class Book(models.Model): + author = models.ForeignKey(Author) ``` ## Đúng ```python -# trong view -queryset = Order.objects.select_related("customer") +class Book(models.Model): + author = models.ForeignKey(Author, on_delete=models.CASCADE) ``` ## Bỏ qua (Suppress) diff --git a/docs/i18n/rules/vi/DOL014.md b/docs/i18n/rules/vi/DOL014.md index 56ce8d9..b8dddd3 100644 --- a/docs/i18n/rules/vi/DOL014.md +++ b/docs/i18n/rules/vi/DOL014.md @@ -1,23 +1,21 @@ -# DOL014 — Dùng `prefetch_related` cho các truy cập ngược `ForeignKey` / `ManyToManyField` +# DOL014 — CharField thiếu max_length -**Mức độ mặc định:** warning · **Khả năng áp dụng:** unsafe · **Danh mục:** model-definition +**Mức độ mặc định:** error · **Khả năng áp dụng:** suggestion · **Danh mục:** model -Phát hiện các truy cập FK ngược hoặc M2M (ví dụ `post.comments.all()`, `user.groups.all()`) bên trong vòng lặp hoặc serializer mà không có `prefetch_related()` tương ứng. Mỗi lần truy cập sẽ kích hoạt một query riêng biệt cho mỗi đối tượng cha. - -Khả năng áp dụng là `unsafe` vì cần thêm `prefetch_related()` tại call site của queryset. +Phát hiện một lời gọi `CharField(...)` không có từ khóa `max_length=`. Django yêu cầu `max_length` trên `CharField`; nếu không có nó, model sẽ không qua được các bước kiểm tra của Django lúc load — lại một lớp lỗi nữa mà thông thường bạn chỉ gặp vào lần chạy `runserver` hoặc `makemigrations` tiếp theo. QuickFix ("Thêm max_length=255 (chỉnh sửa khi cần)") sẽ chèn `max_length=255` dưới dạng một template — 255 là một quy ước phổ biến, không phải là một hằng số kỳ diệu; hãy xác định kích thước cột phù hợp với dữ liệu. Đây là một đề xuất vì giới hạn đúng là do bạn chọn, và nếu văn bản thực sự không có giới hạn, `TextField` sẽ là trường tốt hơn. ## Sai ```python -for post in Post.objects.all(): - comments = post.comments.all() # một query cho mỗi post +class Tag(models.Model): + name = models.CharField() ``` ## Đúng ```python -for post in Post.objects.prefetch_related("comments"): - comments = post.comments.all() # chỉ hai query tổng cộng +class Tag(models.Model): + name = models.CharField(max_length=255) ``` ## Bỏ qua (Suppress) diff --git a/docs/i18n/rules/vi/DOL015.md b/docs/i18n/rules/vi/DOL015.md index 7c3b4a0..1412e5c 100644 --- a/docs/i18n/rules/vi/DOL015.md +++ b/docs/i18n/rules/vi/DOL015.md @@ -1,23 +1,27 @@ -# DOL015 — Tránh lưu trữ dữ liệu văn bản hoặc nhị phân lớn trực tiếp trên model +# DOL015 — TextField có max_length không tác động đến DB -**Mức độ mặc định:** info · **Khả năng áp dụng:** unsafe · **Danh mục:** model-definition +**Mức độ mặc định:** hint · **Khả năng áp dụng:** suggestion · **Danh mục:** model -Phát hiện các khai báo `TextField` hoặc `BinaryField` không có giới hạn `max_length`, đặc biệt khi tên trường gợi ý lưu nội dung (ví dụ `body`, `content`, `data`, `blob`, `payload`). Lưu trữ payload lớn trực tiếp làm phình kích thước row, tăng I/O cho mọi query trên bảng đó, và có thể gây TOAST thrashing trong PostgreSQL. Giải pháp thông thường là chuyển payload sang object storage và chỉ lưu URL hoặc key trên model. - -Khả năng áp dụng là `unsafe` vì đây là thay đổi kiến trúc. +Phát hiện `max_length=` trên một `TextField(...)`. `TextField` tương ứng với `TEXT`/`CLOB`; Django chỉ thực thi `max_length` của nó trong widget form được tạo tự động, không bao giờ ở cấp độ cơ sở dữ liệu. Do đó, kwarg này đọc có vẻ như là một giới hạn cứng nhưng thực chất không phải vậy — các lệnh ghi ORM thô, các phép toán hàng loạt (bulk operations) và các lệnh lưu trực tiếp từ admin đều có thể vượt quá giới hạn này. Nếu bạn cần một giới hạn được thực thi ở cơ sở dữ liệu, hãy sử dụng `CharField(max_length=...)`; nếu văn bản thực sự không có giới hạn, hãy bỏ kwarg này. QuickFix ("Xóa max_length khỏi TextField") sẽ gỡ bỏ kwarg; đây là một đề xuất vì bạn có thể sẽ muốn chuyển đổi sang `CharField` thay thế. ## Sai ```python -class Document(models.Model): - content = models.TextField() # không giới hạn — có thể chiếm hàng megabyte mỗi row +class Comment(models.Model): + body = models.TextField(max_length=500) ``` ## Đúng ```python -class Document(models.Model): - storage_key = models.CharField(max_length=255) # trỏ đến S3 / GCS / v.v. +class Comment(models.Model): + body = models.TextField() +``` + +Hoặc, khi giới hạn phải được giữ ở cấp độ DB: + +```python + body = models.CharField(max_length=500) ``` ## Bỏ qua (Suppress) @@ -26,4 +30,4 @@ class Document(models.Model): # django-orm-lens-disable-next-line DOL015 ``` -Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL015": "off"}}`. +Bỏ qua (suppress) nếu bạn cố ý sử dụng `max_length` hoàn toàn như một giới hạn ở tầng form. Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL015": "off"}}`.