복붙노트

[PYTHON] django에서 비즈니스 로직과 데이터 액세스 분리

PYTHON

django에서 비즈니스 로직과 데이터 액세스 분리

내가 장고에 프로젝트를 쓰고 있는데 코드의 80 %가 models.py에 있다는 것을 알았다. 이 코드는 혼란스럽고 특정 시간이 지나면 어떤 일이 실제로 일어나고 있는지 이해하지 않습니다.

여기 저를 괴롭히는 것이 있습니다 :

다음은 간단한 예입니다. 처음에는 User 모델이 다음과 같았습니다.

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

시간이 지남에 따라 다음과 같이 바뀌 었습니다.

class User(db.Models):

    def get_present_name(self): 
        # property became non-deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

    def activate(self):
        # method now has a side effect (send message to user)
        self.status = 'activated'
        self.save()
        send_mail('Your account is activated!', '…', [self.email])

내가 원하는 것은 코드에서 엔티티를 분리하는 것입니다.

장고에 적용 할 수있는 접근법을 구현하는 좋은 방법은 무엇입니까?

해결법

  1. ==============================

    1.데이터 모델과 도메인 모델의 차이점에 대해 묻는 것처럼 보입니다. 후자는 최종 사용자가 인식하는 비즈니스 논리 및 엔티티를 찾을 수 있고, 전자는 실제로 데이터를 저장하는 곳입니다.

    데이터 모델과 도메인 모델의 차이점에 대해 묻는 것처럼 보입니다. 후자는 최종 사용자가 인식하는 비즈니스 논리 및 엔티티를 찾을 수 있고, 전자는 실제로 데이터를 저장하는 곳입니다.

    또한, 나는 당신의 질문의 세 번째 부분을 어떻게 해석 할 것인가?

    이 두 가지 개념은 매우 다른 개념이며 항상 구분하기가 어렵습니다. 그러나이 목적을 위해 사용할 수있는 몇 가지 일반적인 패턴과 도구가 있습니다.

    인식해야 할 첫 번째 사항은 도메인 모델이 실제로 데이터에 관한 것이 아니라는 것입니다. "이 사용자 활성화", "이 사용자 비활성화", "어떤 사용자가 현재 활성화되어 있습니까?"및 "이 사용자 이름은 무엇입니까?"와 같은 동작 및 질문에 관한 것입니다. 클래식 용어로는 쿼리와 명령에 관한 것입니다.

    예제에서 "이 사용자 활성화"및 "이 사용자 비활성화"명령을 살펴 보겠습니다. 명령에 대한 좋은 점은 작은 주어진 시점 시나리오로 쉽게 표현할 수 있다는 것입니다.

    이러한 시나리오는 하나의 명령 (이 경우 데이터베이스 (일종의 '활성'플래그), 메일 서버, 시스템 로그 등)으로 인프라의 다른 부분이 어떻게 영향을 받는지 확인하는 데 유용합니다.

    이러한 시나리오는 테스트 중심 개발 환경을 설정하는 데 정말로 도움이됩니다.

    마지막으로 명령을 생각하면 실제로 작업 지향 응용 프로그램을 만드는 데 도움이됩니다. 귀하의 사용자는이 점에 감사 할 것입니다 :-)

    Django는 명령을 표현하는 두 가지 쉬운 방법을 제공합니다. 둘 다 유효한 옵션이며 두 가지 접근 방법을 혼합하는 것이 일반적이지 않습니다.

    서비스 모듈은 @Hedde에 의해 이미 설명되었습니다. 여기에서는 별도의 모듈을 정의하며 각 명령은 함수로 표시됩니다.

    services.py

    def activate_user(user_id):
        user = User.objects.get(pk=user_id)
    
        # set active flag
        user.active = True
        user.save()
    
        # mail user
        send_mail(...)
    
        # etc etc
    

    다른 방법은 각 명령에 대해 Django Form을 사용하는 것입니다. 여러 가지 밀접한 관련이있는 요소가 결합되어 있으므로이 방법을 선호합니다.

    forms.py

    class ActivateUserForm(forms.Form):
    
        user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
        # the username select widget is not a standard Django widget, I just made it up
    
        def clean_user_id(self):
            user_id = self.cleaned_data['user_id']
            if User.objects.get(pk=user_id).active:
                raise ValidationError("This user cannot be activated")
            # you can also check authorizations etc. 
            return user_id
    
        def execute(self):
            """
            This is not a standard method in the forms API; it is intended to replace the 
            'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern. 
            """
            user_id = self.cleaned_data['user_id']
    
            user = User.objects.get(pk=user_id)
    
            # set active flag
            user.active = True
            user.save()
    
            # mail user
            send_mail(...)
    
            # etc etc
    

    예제에는 쿼리가 없으므로 몇 가지 유용한 쿼리를 작성했습니다. 나는 "질문"이라는 용어를 사용하는 것을 선호하지만, 질의는 고전 용어이다. 흥미로운 질문은 "이 사용자의 이름은 무엇입니까?", "이 사용자는 로그인 할 수 있습니까?", "비활성화 된 사용자 목록 표시"및 "비활성화 된 사용자의 지리적 분포는 무엇입니까?"

    이러한 쿼리에 착수하기 전에 먼저 템플릿에 대한 프리젠 테이션 쿼리 및 / 또는 내 명령을 실행하는 비즈니스 논리 쿼리 및 / 또는보고 쿼리에 대해 스스로에게 질문해야합니다.

    프리젠 테이션 쿼리는 사용자 인터페이스를 개선하기위한 것입니다. 비즈니스 논리 쿼리에 대한 응답은 명령 실행에 직접 영향을줍니다. 보고 쿼리는 분석 목적으로 만 사용되며 시간 제약이 느슨합니다. 이 카테고리는 상호 배타적이지 않습니다.

    다른 질문은 "답변을 완전히 제어 할 수 있습니까?" 예를 들어 사용자 이름을 쿼리 할 때 (이 문맥에서) 우리는 외부 API를 사용하기 때문에 결과를 제어 할 수 없습니다.

    Django에서 가장 기본적인 쿼리는 Manager 객체의 사용입니다 :

    User.objects.filter(active=True)
    

    물론 데이터가 데이터 모델에 실제로 표시되는 경우에만 작동합니다. 항상 그런 것은 아닙니다. 이 경우 아래 옵션을 고려할 수 있습니다.

    첫 번째 대안은 사용자 지정 태그 및 템플릿 필터와 같이 단순히 프리젠 테이션 인 쿼리에 유용합니다.

    template.html

    <h1>Welcome, {{ user|friendly_name }}</h1>
    

    template_tags.py

    @register.filter
    def friendly_name(user):
        return remote_api.get_cached_name(user.id)
    

    검색어가 단순한 프리젠 테이션이 아닌 경우 services.py에 검색어를 추가하거나 (사용중인 경우) queries.py 모듈을 추가 할 수 있습니다.

    queries.py

    def inactive_users():
        return User.objects.filter(active=False)
    
    
    def users_called_publysher():
        for user in User.objects.all():
            if remote_api.get_cached_name(user.id) == "publysher":
                yield user 
    

    프록시 모델은 비즈니스 로직 및보고의 컨텍스트에서 매우 유용합니다. 기본적으로 모델의 향상된 하위 집합을 정의합니다. Manager.get_queryset () 메서드를 재정 의하여 Manager의 기본 QuerySet을 재정의 할 수 있습니다.

    models.py

    class InactiveUserManager(models.Manager):
        def get_queryset(self):
            query_set = super(InactiveUserManager, self).get_queryset()
            return query_set.filter(active=False)
    
    class InactiveUser(User):
        """
        >>> for user in InactiveUser.objects.all():
        …        assert user.active is False 
        """
    
        objects = InactiveUserManager()
        class Meta:
            proxy = True
    

    본질적으로 복잡하지만 자주 실행되는 쿼리의 경우 쿼리 모델이있을 수 있습니다. 쿼리 모델은 하나의 쿼리에 대한 관련 데이터가 별도의 모델에 저장되는 비정규 화의 한 형태입니다. 물론 트릭은 비정규 화 된 모델을 기본 모델과 동기화하여 유지하는 것입니다. 쿼리 모델은 변경 사항이 완전히 통제되는 경우에만 사용할 수 있습니다.

    models.py

    class InactiveUserDistribution(models.Model):
        country = CharField(max_length=200)
        inactive_user_count = IntegerField(default=0)
    

    첫 번째 옵션은 명령에서 이러한 모델을 업데이트하는 것입니다. 이 모델이 하나 또는 두 개의 명령으로 만 변경되는 경우 매우 유용합니다.

    forms.py

    class ActivateUserForm(forms.Form):
        # see above
    
        def execute(self):
            # see above
            query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
            query_model.inactive_user_count -= 1
            query_model.save()
    

    더 나은 옵션은 맞춤 신호를 사용하는 것입니다. 이 신호는 물론 명령에 의해 방출됩니다. 신호에는 원본 모델과 동기화 된 여러 쿼리 모델을 유지할 수 있다는 이점이 있습니다. 또한 신호 처리는 셀러리 또는 유사한 프레임 워크를 사용하여 백그라운드 작업으로 오프로드 할 수 있습니다.

    signals.py

    user_activated = Signal(providing_args = ['user'])
    user_deactivated = Signal(providing_args = ['user'])
    

    forms.py

    class ActivateUserForm(forms.Form):
        # see above
    
        def execute(self):
            # see above
            user_activated.send_robust(sender=self, user=user)
    

    models.py

    class InactiveUserDistribution(models.Model):
        # see above
    
    @receiver(user_activated)
    def on_user_activated(sender, **kwargs):
            user = kwargs['user']
            query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
            query_model.inactive_user_count -= 1
            query_model.save()
    

    이 방법을 사용하면 코드가 깨끗하게 유지되는지 쉽게 판단 할 수 있습니다. 다음 지침을 따르십시오.

    보기가 동일한 문제로 인해 종종 고통 받기 때문에보기에도 동일하게 적용됩니다.

    장고 문서 : 프록시 모델

    장고 문서 : 신호

    아키텍처 : 도메인 기반 디자인

  2. ==============================

    2.나는 보통 뷰와 모델 사이에 서비스 레이어를 구현한다. 이것은 프로젝트의 API와 같은 역할을하며 진행중인 상황에 대한 훌륭한 헬기 뷰를 제공합니다. 이 작업은 Java Project (JSF)에서이 레이어링 기법을 많이 사용하는 동료 중 한 사람에게 물려 받았습니다. 예를 들면 다음과 같습니다.

    나는 보통 뷰와 모델 사이에 서비스 레이어를 구현한다. 이것은 프로젝트의 API와 같은 역할을하며 진행중인 상황에 대한 훌륭한 헬기 뷰를 제공합니다. 이 작업은 Java Project (JSF)에서이 레이어링 기법을 많이 사용하는 동료 중 한 사람에게 물려 받았습니다. 예를 들면 다음과 같습니다.

    models.py

    class Book:
       author = models.ForeignKey(User)
       title = models.CharField(max_length=125)
    
       class Meta:
           app_label = "library"
    

    services.py

    from library.models import Book
    
    def get_books(limit=None, **filters):
        """ simple service function for retrieving books can be widely extended """
        if limit:
            return Book.objects.filter(**filters)[:limit]
        return Book.objects.filter(**filters)
    

    views.py

    from library.services import get_books
    
    class BookListView(ListView):
        """ simple view, e.g. implement a _build and _apply filters function """
        queryset = get_books()
    
  3. ==============================

    3.우선, 반복하지 마십시오.

    우선, 반복하지 마십시오.

    그런 다음, overengineer하지 않도록주의하십시오, 때로는 그냥 시간 낭비이며, 누군가가 중요한 일에 집중하지 못하게합니다. Python의 zen을 수시로 검토하십시오.

    진행중인 프로젝트보기

    MVC에 대한 추가 정보

    미들웨어 / templatetags 활용

    모델 관리자 활용

    예:

    class UserManager(models.Manager):
       def create_user(self, username, ...):
          # plain create
       def create_superuser(self, username, ...):
          # may set is_superuser field.
       def activate(self, username):
          # may use save() and send_mail()
       def activate_in_bulk(self, queryset):
          # may use queryset.update() instead of save()
          # may use send_mass_mail() instead of send_mail()
    

    가능한 경우 서식 사용

    모델에 매핑되는 양식이 있으면 많은 상용구 코드를 제거 할 수 있습니다. ModelForm 문서는 꽤 좋습니다. 모델 코드와 양식을 구분하는 것은 사용자 정의가 많은 경우 (또는 고급 사용을 위해 순환 가져 오기 오류가 발생하지 않는 경우) 유용 할 수 있습니다.

    가능한 경우 관리 명령 사용

    비즈니스 논리가 있다면 분리 할 수 ​​있습니다.

    백엔드 예 :

    class User(db.Models):
        def get_present_name(self): 
            # property became not deterministic in terms of database
            # data is taken from another service by api
            return remote_api.request_user_name(self.uid) or 'Anonymous' 
    

    될 수있다 :

    class User(db.Models):
       def get_present_name(self):
          for backend in get_backends():
             try:
                return backend.get_present_name(self)
             except: # make pylint happy.
                pass
          return None
    

    디자인 패턴에 대한 추가 정보

    인터페이스 경계에 대한 추가 정보

    요컨대, 당신은

    또는 당신을 돕는 다른 것; 필요한 인터페이스와 경계를 찾는 것이 도움이 될 것입니다.

  4. ==============================

    4.Django는 약간 수정 된 MVC를 사용합니다. 장고에는 "컨트롤러"라는 개념이 없습니다. 가장 가까운 프록시는 MVC에서보기가 Django의 "템플릿"과 더 비슷하기 때문에 MVC 변환과 혼란을 야기하는 경향이있는 "보기"입니다.

    Django는 약간 수정 된 MVC를 사용합니다. 장고에는 "컨트롤러"라는 개념이 없습니다. 가장 가까운 프록시는 MVC에서보기가 Django의 "템플릿"과 더 비슷하기 때문에 MVC 변환과 혼란을 야기하는 경향이있는 "보기"입니다.

    Django에서 "모델"은 단순한 데이터베이스 추상화가 아닙니다. 어떤 측면에서는 Django의 "뷰"와 MVC의 컨트롤러 역할을 공유합니다. 인스턴스와 관련된 전체 동작을 유지합니다. 해당 인스턴스가 동작의 일부로 외부 API와 상호 작용해야하는 경우 모델 코드입니다. 실제로 모델은 데이터베이스와 전혀 상호 작용할 필요가 없으므로 모델이 외부 API와의 상호 작용 계층으로 존재할 수 있습니다. 그것은 "모델"이라는 훨씬 더 자유로운 개념입니다.

  5. ==============================

    5.장고에서 MVC 구조는 다른 프레임 워크에서 사용되는 고전적인 MVC 모델과는 달리 Chris Pratt가 말한 것처럼 CakePHP와 같은 다른 MVC 프레임 워크 에서처럼 너무 엄격한 응용 프로그램 구조를 피하는 것이 주된 이유라고 생각합니다.

    장고에서 MVC 구조는 다른 프레임 워크에서 사용되는 고전적인 MVC 모델과는 달리 Chris Pratt가 말한 것처럼 CakePHP와 같은 다른 MVC 프레임 워크 에서처럼 너무 엄격한 응용 프로그램 구조를 피하는 것이 주된 이유라고 생각합니다.

    Django에서 MVC는 다음과 같이 구현되었습니다.

    뷰 레이어는 두 개로 분할됩니다. 보기는 HTTP 요청을 관리하기 위해서만 사용되어야하며 호출되고 응답됩니다. 뷰는 나머지 애플리케이션 (양식, 모델 형식, 사용자 정의 클래스, 간단한 경우 모델과 직접 연결)과 통신합니다. 인터페이스를 생성하기 위해 템플릿을 사용합니다. 템플릿은 Django와 같은 문자열이며, 문맥을 Django에 매핑합니다.이 컨텍스트는 애플리케이션이 뷰에 알리는대로 전달됩니다.

    모델 계층은 캡슐화, 추상화, 유효성 검사, 인텔리전스를 제공하고 데이터 객체 지향적입니다 (언젠가 DBMS도이를 말합니다). 이것은 거대한 models.py 파일을 만들어야한다는 것을 의미하지 않는다. (모델을 여러 파일로 나눠서 'models'폴더에 넣고 '__init__.py'파일을 폴더에서 모든 모델을 가져오고 마지막으로 models.Model 클래스의 'app_label'특성을 사용합니다. 모델은 데이터로 작업하는 것을 추상화해야합니다. 그러면 응용 프로그램이 더 단순 해집니다. 또한 필요한 경우 모델의 "도구"와 같은 외부 클래스를 만들어야합니다. 모델의 메타 클래스의 '추상'속성을 'True'로 설정하여 모델에서 유산을 사용할 수도 있습니다.

    나머지는 어디 있습니까? 글쎄, 작은 웹 응용 프로그램은 일반적으로 데이터에 대한 인터페이스의 일종으로, 쿼리를 사용하거나 데이터를 삽입하는 뷰를 사용하는 일부 작은 프로그램의 경우 충분합니다. 더 일반적인 경우는 실제로 "컨트롤러"인 Forms 또는 ModelForms를 사용합니다. 이것은 일반적인 문제에 대한 실용적인 해결책과 매우 빠른 해결책이 아닙니다. 웹 사이트에서하는 일입니다.

    양식이 효과적이지 않다면 자신 만의 클래스를 만들어야 마술을 할 수 있습니다.이 좋은 예는 관리자 애플리케이션입니다. ModelAmin 코드를 읽을 수 있습니다.이 코드는 실제로 컨트롤러로 작동합니다. 표준 구조가 없기 때문에 기존 Django 응용 프로그램을 검사 할 것을 제안합니다. 각 응용 프로그램마다 다릅니다. Django 개발자가 의도 한 것입니다. XML 파서 클래스, API 커넥터 클래스를 추가하고, 작업 수행을 위해 Celery를 추가하고, 원자로 기반 응용 프로그램을 위해 비틀고, ORM 만 사용하고, 웹 서비스를 만들고, 관리 응용 프로그램을 수정하는 등의 작업을 할 수 있습니다. .. 양질의 코드를 만들고, MVC 철학을 존중하며, 모듈 기반으로 만들고, 자신 만의 추상화 레이어를 만드는 것은 당신의 책임입니다. 매우 유연합니다.

    내 충고 : 할 수있는 한 많은 코드를 읽고 거기에 장고 애플리케이션이 많이 있지만 심각하게 생각하지 마십시오. 각각의 경우는 다르지만 패턴과 이론이 도움이되지만 항상 그런 것은 아닙니다. 이것은 부정확 한 정신입니다. 장고는 관리 인터페이스, 웹 양식 유효성 검사, i18n, 관찰자 ​​패턴 구현, 모두와 같은 일부 통증을 완화시키는 데 사용할 수있는 훌륭한 도구를 제공합니다. 이전에 언급 한 것과 다른 것들). 그러나 좋은 디자인은 경험 많은 디자이너들에게서 나온다.

    추신 : (표준 django에서) 인증 응용 프로그램에서 '사용자'클래스를 사용하여, 예를 들어 사용자 프로필을 만들거나, 적어도 코드를 읽고, 그것은 당신의 사건에 유용 할 것입니다.

  6. ==============================

    6.나는 대부분 선택된 답변 (https://stackoverflow.com/a/12857584/871392)에 동의하지만 쿼리 작성 섹션에 옵션을 추가하려고합니다.

    나는 대부분 선택된 답변 (https://stackoverflow.com/a/12857584/871392)에 동의하지만 쿼리 작성 섹션에 옵션을 추가하려고합니다.

    Make 필터 쿼리와 son on을위한 모델에 대한 QuerySet 클래스를 정의 할 수 있습니다. 그런 다음 build-in Manager 및 QuerySet 클래스처럼 모델 관리자에 대해이 queryset 클래스를 프록시 할 수 있습니다.

    비록 하나의 도메인 모델을 얻기 위해 여러 개의 데이터 모델을 쿼리해야한다면, 이전에 제안 된 것과 같이 별도의 모듈에 이것을 두는 것이 더 합리적인 것처럼 보입니다.

  7. ==============================

    7.오래된 질문이지만 어쨌든 내 솔루션을 제공하고 싶습니다. 그것은 모델 객체가 모델 기능을 추가로 필요로한다는 것을 받아들이는 것에 기반을두고 있지만, models.py 내에 배치하는 것은 어색합니다. 무거운 비즈니스 논리는 개인적인 취향에 따라 별도로 작성 될 수 있지만 필자는 적어도 모델 자체가 관련된 모든 것을 수행하는 것을 좋아합니다. 이 솔루션은 또한 모든 논리를 모델 자체에 배치하려는 사람들을 지원합니다.

    오래된 질문이지만 어쨌든 내 솔루션을 제공하고 싶습니다. 그것은 모델 객체가 모델 기능을 추가로 필요로한다는 것을 받아들이는 것에 기반을두고 있지만, models.py 내에 배치하는 것은 어색합니다. 무거운 비즈니스 논리는 개인적인 취향에 따라 별도로 작성 될 수 있지만 필자는 적어도 모델 자체가 관련된 모든 것을 수행하는 것을 좋아합니다. 이 솔루션은 또한 모든 논리를 모델 자체에 배치하려는 사람들을 지원합니다.

    따라서 모델 정의와 논리를 분리하고 IDE에서 모든 힌트를 얻을 수있는 해킹을 고안했습니다.

    이점은 분명해야하지만, 여기에 제가 지켜본 몇 가지 사항이 나와 있습니다 :

    나는 이것을 Python 3.4 이상과 Django 1.8 이상에서 사용 해왔다.

    app / models.py

    ....
    from app.logic.user import UserLogic
    
    class User(models.Model, UserLogic):
        field1 = models.AnyField(....)
        ... field definitions ...
    

    app / logic / user.py

    if False:
        # This allows the IDE to know about the User model and its member fields
        from main.models import User
    
    class UserLogic(object):
        def logic_function(self: 'User'):
            ... code with hinting working normally ...
    

    내가 알아낼 수없는 유일한 방법은 내 IDE (이 경우에는 PyCharm)를 만들어 UserLogic이 실제로 User 모델임을 인식하는 것입니다. 그러나 이것은 분명히 해킹이기 때문에, 나는 항상 자기 매개 변수에 대해 타입을 지정하는 약간의 성가신을 받아 들일만큼 행복합니다.

  8. ==============================

    8.장고는 웹 페이지를 쉽게 전달할 수 있도록 설계되었습니다. 이 기능이 마음에 들지 않으면 다른 솔루션을 사용해야합니다.

    장고는 웹 페이지를 쉽게 전달할 수 있도록 설계되었습니다. 이 기능이 마음에 들지 않으면 다른 솔루션을 사용해야합니다.

    모델의 컨트롤러에있는 다른 인터페이스와 동일한 인터페이스를 사용하기 위해 모델에 루트 또는 공통 연산을 작성하고 있습니다. 다른 모델에서 작업이 필요한 경우 해당 컨트롤러를 가져옵니다.

    이 접근법으로 저에게 충분하며 응용 프로그램의 복잡성이 있습니다.

    Hedde의 응답은 장고와 파이썬 자체의 유연성을 보여주는 예입니다.

    어쨌든 아주 재미있는 질문!

  9. from https://stackoverflow.com/questions/12578908/separation-of-business-logic-and-data-access-in-django by cc-by-sa and MIT license