0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Vấn đề Đồng bộ Chuỗi PostgreSQL trong Django Many-to-Many

Đăng vào 1 ngày trước

• 8 phút đọc

Bí ẩn khiến tôi gần như phát điên

Hãy tưởng tượng: Ứng dụng Django của bạn hoạt động hoàn hảo mà không gặp bất kỳ khó khăn nào khi chạy cục bộ, nhưng sau khi triển khai lên môi trường staging và UAT (Tiền sản xuất), một điều gì đó rất kỳ lạ xảy ra. Một số quan hệ nhiều-nhiều (Many-to-Many) dường như không còn hoạt động. Các biểu mẫu gửi đi như mong đợi, dữ liệu được hiển thị đầy đủ và các thao tác trong bảng quản trị cũng hoàn tất mà không có lỗi nào. Nhưng, một số mối quan hệ M2M từ chối được lưu. Không có thông báo lỗi, không có vấn đề rõ ràng, chỉ có dữ liệu biến mất một cách im lặng, để lại bạn trong sự bối rối.

Đây chính xác là kịch bản ác mộng mà tôi đã gặp phải gần đây, và giải pháp hóa ra lại tinh tế hơn nhiều so với tôi đã tưởng tượng ban đầu.

Thiết lập:

Tôi có một ứng dụng Django với bốn mô hình: Employee, SegregationType, Item, và Skill. Ba trong số các mô hình này (Employee, SegregationType, và Item) có quan hệ nhiều-nhiều với Skill:

python Copy
class Employee(models.Model):
    name = models.CharField(max_length=100)
    skills = models.ManyToManyField('Skill', blank=True)

class SegregationType(models.Model):
    name = models.CharField(max_length=100)
    skills = models.ManyToManyField('Skill', blank=True)

class Item(models.Model):
    name = models.CharField(max_length=100)
    skills = models.ManyToManyField('Skill', blank=True)

class Skill(models.Model):
    name = models.CharField(max_length=100)

Vấn đề xuất hiện

Mọi thứ hoạt động hoàn hảo trong môi trường phát triển cục bộ của tôi. Nhưng trong staging và UAT (Tiền sản xuất), việc cố gắng gán kỹ năng cho các thực thể SegregationType hoặc Item sẽ:

  • Trả về phản hồi thành công từ API.
  • Không hiển thị lỗi trong bảng quản trị Django.
  • Nhưng không thực sự tạo ra các mối quan hệ.

Những nghi ngờ ban đầu

Instinct đầu tiên của tôi là đổ lỗi cho frontend. Có thể có vấn đề với việc mã hóa payload hoặc tuần tự hóa dữ liệu? Sau một cuộc điều tra kỹ lưỡng, frontend là vô tội.

Lý thuyết mạng và đồng thời

Tiếp theo, tôi nghi ngờ các vấn đề về mạng - có thể mất gói tin gây ra yêu cầu không hoàn chỉnh. Tôi cũng xem xét các điều kiện đua: có thể giao dịch mô hình chính chưa được cam kết trước khi Django cố gắng tạo các mối quan hệ M2M. Cả hai lý thuyết đều không đúng.

Thủ phạm thực sự xuất hiện

Bước đột phá xảy ra khi tôi nhớ rằng vài ngày trước, chúng tôi đã làm đầy cơ sở dữ liệu staging và UAT bằng cách khôi phục từ bản sao lưu. Trong các thao tác tiếp theo, chúng tôi bắt đầu gặp phải lỗi này:

plaintext Copy
django.db.utils.IntegrityError: duplicate key value violates unique constraint "django_migrations_pkey"
DETAIL: Key (id)=(3) already exists.

Điều này chính là bằng chứng cho thấy vấn đề thực sự: Sự không đồng bộ chuỗi PostgreSQL.

Hiểu về Chuỗi PostgreSQL

Khi bạn khôi phục dữ liệu vào PostgreSQL từ một bản sao lưu, các bản ghi được chèn đúng, nhưng các chuỗi (điều chỉnh giá trị tiếp theo cho các khóa chính tự tăng) không được tự động cập nhật (Thật buồn!). Điều này gây ra một sự không khớp tinh tế nhưng có thể gây hại:

  • Bảng của bạn có thể có các hàng với ID lên tới hàng chục nghìn.
  • Tuy nhiên, chuỗi vẫn giả định rằng ID tiếp theo có sẵn là 1, như thể bảng còn trống.
  • Bây giờ, khi Django cố gắng chèn một bản ghi mới, nó sẽ cố gắng sử dụng ID=1, cái đã tồn tại, gây ra xung đột khóa chính.

Giải pháp ban đầu

Tôi đã giải quyết vấn đề chuỗi mô hình chính bằng cách sử dụng đoạn mã này:

python Copy
from django.db import connection
from django.apps import apps

with connection.cursor() as cursor:
    for model in apps.get_models():
        table = model._meta.db_table
        pk_field = model._meta.pk

        if not pk_field.auto_created:
            continue  # Bỏ qua nếu không có PK tự động tạo

        sequence_sql = f"SELECT pg_get_serial_sequence('{table}', '{pk_field.column}');"
        cursor.execute(sequence_sql)
        result = cursor.fetchone()

        if result and result[0]:
            sequence_name = result[0]
            update_sql = f"SELECT setval('{sequence_name}', (SELECT COALESCE(MAX({pk_field.column}), 1) FROM {table}) + 1, false);"
            print(f"✅ Đang đặt lại chuỗi cho {table} sử dụng {sequence_name}")
            cursor.execute(update_sql)
        else:
            print(f"⚠️ Không tìm thấy chuỗi cho bảng: {table}")

Vấn đề thực tế: Các Bảng Thông qua Ngầm

Sự nhận thức quan trọng mà tôi mất hàng giờ để tìm ra: Các bảng thông qua ngầm của Django cũng có các trường ID tự tăng với chuỗi riêng của chúng. Hơn nữa, apps.get_models() không trả về các bảng thông qua ngầm này.

Khi bạn định nghĩa một ManyToManyField mà không chỉ định một mô hình through, Django tự động tạo một bảng thông qua với:

  • Một trường id tự tăng
  • Khóa ngoại đến cả hai mô hình liên quan
  • Chuỗi PostgreSQL riêng của nó

Những chuỗi bảng thông qua ngầm này cũng đã không đồng bộ sau khi khôi phục cơ sở dữ liệu!

Giải pháp hoàn chỉnh

Để sửa các mối quan hệ M2M, tôi đã phải đặt lại các chuỗi cho tất cả các bảng thông qua ngầm:

sql Copy
-- Ví dụ cho mối quan hệ M2M kỹ năng SegregationType
SELECT setval(
    pg_get_serial_sequence('"jobs_segregationtype_skills"', 'id'),
    COALESCE(MAX(id), 1),
    true
) FROM "jobs_segregationtype_skills";

Bạn có thể khám phá các tên bảng thông qua theo cách lập trình:

python Copy
# Lấy tên bảng thông qua cho một trường M2M
through_table = SegregationType.skills.through._meta.db_table
print(f"Tên bảng thông qua: {through_table}")

Một Lệnh Quản lý Django Toàn diện

Dưới đây là một giải pháp hoàn chỉnh dưới dạng lệnh quản lý Django:

python Copy
from django.core.management.base import BaseCommand
from django.db import connection
from django.apps import apps

class Command(BaseCommand):
    help = 'Đặt lại các chuỗi PostgreSQL cho tất cả các mô hình và bảng thông qua M2M'

    def handle(self, *args, **options):
        with connection.cursor() as cursor:
            # Đặt lại chuỗi cho các mô hình chính
            for model in apps.get_models():
                self.reset_model_sequence(cursor, model)

                # Đặt lại chuỗi cho các bảng thông qua M2M
                for field in model._meta.get_fields():
                    if field.many_to_many and not field.remote_field.through._meta.auto_created:
                        continue  # Bỏ qua các mô hình thông qua rõ ràng
                    elif field.many_to_many:
                        self.reset_model_sequence(cursor, field.remote_field.through)

    def reset_model_sequence(self, cursor, model):
        table = model._meta.db_table
        pk_field = model._meta.pk

        if not pk_field.auto_created:
            return

        sequence_sql = f"SELECT pg_get_serial_sequence('{table}', '{pk_field.column}');"
        cursor.execute(sequence_sql)
        result = cursor.fetchone()

        if result and result[0]:
            sequence_name = result[0]
            update_sql = f"SELECT setval('{sequence_name}', (SELECT COALESCE(MAX({pk_field.column}), 1) FROM {table}) + 1, false);"
            self.stdout.write(f"✅ Đang đặt lại chuỗi cho {table}")
            cursor.execute(update_sql)

Chiến lược phòng ngừa

  1. Sao lưu cơ sở dữ liệu hợp lý: Khi tạo bản sao lưu, đảm bảo rằng các chuỗi được bao gồm và được xử lý đúng cách.
  2. Danh sách kiểm tra sau khôi phục: Luôn chạy các lệnh đặt lại chuỗi sau khi khôi phục cơ sở dữ liệu.
  3. Kiểm thử tự động: Bao gồm kiểm thử mối quan hệ M2M trong bộ xác minh triển khai của bạn.
  4. Giám sát: Thiết lập cảnh báo cho các ngoại lệ IntegrityError trong sản xuất.

Những điểm chính cần lưu ý

  • Những thất bại im lặng là tồi tệ nhất: Thất bại trong mối quan hệ M2M có thể hoàn toàn im lặng, khiến chúng cực kỳ khó debug.
  • Độ phức tạp ngầm: Các bảng thông qua ngầm của Django thêm độ phức tạp ẩn mà dễ dàng bị bỏ qua.
  • Tính đồng nhất của môi trường rất quan trọng: Các vấn đề không tái hiện được cục bộ có thể là những vấn đề khó khăn nhất để giải quyết.
  • Quản lý chuỗi: Sự không đồng bộ chuỗi PostgreSQL là một vấn đề phổ biến nhưng thường bị bỏ qua sau khi khôi phục cơ sở dữ liệu.

Kết luận

Thách thức gỡ lỗi này đã rèn dũa kỹ năng kỹ thuật của tôi và dạy tôi những bài học quý giá về DjangoPostgreSQL. Vấn đề xuất phát từ các mối quan hệ nhiều-nhiều của Django, sử dụng các bảng thông qua ngầm với các ID tự tăng và chuỗi. Nếu những chuỗi này không được quản lý đúng cách, chúng có thể gây ra xung đột dữ liệu và thất bại im lặng. Trải nghiệm này đã nhấn mạnh tầm quan trọng của việc hiểu các tương tác cơ sở dữ liệu của Django và nhu cầu quản lý chuỗi một cách cẩn thận trong PostgreSQL để duy trì tính toàn vẹn của các mối quan hệ.

Quan trọng hơn, bất kỳ khi nào bạn khôi phục dữ liệu trong một ứng dụng Django, bạn phải xem xét và đặt lại các chuỗi cho các bảng thông qua - không chỉ cho các bảng mô hình chính của bạn.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào