django-pgtrigger를 이용한 model field 업데이트 막기

haremeat·2022년 12월 28일
0

Django

목록 보기
15/16
post-thumbnail
post-custom-banner

바로 어제 포스팅에서 django-pgtrigger는 어째서 필드별 업데이트 제한 기능을 넣지 않았는가를 설파한지 하루만에
슬프게도
document에 처음부터 떡하니 나와있는 read_only 기능을 사용하면 된다는 것을 깨닫게 되었다.

그리고 pgtrigger는 첫 설치시에만 makemigration이 필요하다.
이후 trigger를 추가할 때에는 그때마다 바로 migrate를 하면 된다.

참고 : https://django-pgtrigger.readthedocs.io/en/latest/cookbook.html#read-only-models-and-fields

이걸 좀 빨리 알았으면 참 좋았을텐데
나는 이미 먼 길을 돌아가서
custom한 class를 만들고 pr까지 날린 뒤였다.

my custom trigger

class ProtectUpdateFields(pgtrigger.Protect):
    """
    특정 Model field의 update를 막는 trigger

    Attributes:
        fields: update를 막을 필드들 (tuple)
        name: 해당 trigger의 이름 (str)
    """
    def __init__(self, *, name=None, fields=None):
        self.fields = fields
        self.name = name or self.name
        self.operation = pgtrigger.Update

        if not self.fields:
            raise ValueError('Must provide "fields" for ProtectUpdateFields')

        super().__init__(name=name, operation=self.operation)

    def get_func(self, model):
        validation_uri = self._distinct_validation_uri(model)
        exception_message = self._exception_message(model)

        sql = f'''
                    IF ({validation_uri}) THEN
                        RAISE EXCEPTION
                            '{exception_message}',
                            TG_TABLE_NAME;
                    ELSE
                        RETURN NEW;
                    END IF;
                '''
        return sql

    def _distinct_validation_uri(self, model):
        cols = []
        for field in self.fields:
            cols.append(model._meta.get_field(field).column)

        separator = ' OR '
        conditions = []
        for col in cols:
            conditions.append(f'OLD.{col} IS DISTINCT FROM NEW.{col}')
        return separator.join(conditions)

    def _exception_message(self, model):
        cols = []
        for field in self.fields:
            cols.append(model._meta.get_field(field).column)

        val = []
        for field in self.fields:
            val.append(field)
        val = ', '.join(val)

        return f'pgtrigger: Cannot Update {val} from %'

기껏 만들었는데 쓰이지 않은 게 아쉬워 여기에라도 올린다.

사용방법은 아래와 같다.

class MyModel(models.Model):
    test_field1 = models.CharField(_('테스트1'), max_length=50)
    test_field2 = models.CharField(_('테스트2'), max_length=50)

    class Meta:
        triggers = [
            ProtectFieldsUpdate(
                name='protect_update_in_post_fields',
                fields=['test_field1', 'test_field2']
            )
        ]

Read-only models and fields

위에서 말했듯 저렇게 커스텀할 필요도 없이,
그냥 django-pgtrigger에서 자체적으로 제공해주는 기능인 read-only trigger를 아래처럼 사용하면 된다.

class TimestampedModel(models.Model):
    """Ensure created_at timestamp is read only"""
    created_at = models.DateTimeField(auto_now_add=True)
    editable_value = models.TextField()

    class Meta:
        triggers = [
            pgtrigger.ReadOnly(
                name="read_only_created_at",
                fields=["created_at"]
            )
        ]
  • fields: A list of read-only fields.
  • exclude: Fields to exclude. All other fields will be read-only.

paramter는 둘 중 하나를 선택해서
만약 특정 몇 개의 필드만 read-only로 만들고 싶으면 fields를 정의하고,
대부분의 필드를 read-only로 만들고 싶으면 exclude를 정의해서 exclude에 넣은 필드를 제외한 나머지 필드들은 모두 read-only로 만들면 된다.

구차한 변명

사실 document를 보면서 read-only 기능이 있다는 건 알고있었다.
근데 왜 쓸 생각을 못했느냐...

정말 바보같게도 django-read-only 라이브러리를 사용했던 걸 trigger로 시도했던 걸로 착각했던 것이었다.
django-read-only를 사용하면 기본적으로 생성시에도 필드를 직접 지정할 수 없게 막는다.
생성시에도 작동하게 하려면 enable_writes()를 호출하여 사용해야하는데 아무튼 뭐.. 많이 다르다.

아마 이후로 read-only쪽은 거들떠도 안 본 것 같은데
pgtrigger.ReadOnly는 operation도 core.Update로 되어있으니 너무 당연하게도 수정될 때만 작동한다.

슬프다.
document를 꼼꼼하게 읽자

profile
버그와 함께하는 삶
post-custom-banner

0개의 댓글