source

MVC에서 모델을 어떻게 구성해야 합니까?

nicesource 2022. 11. 5. 17:26
반응형

MVC에서 모델을 어떻게 구성해야 합니까?

MVC 프레임워크에 대해 파악 중인데 모델 내에서 어느 정도의 코드를 사용해야 하는지 자주 고민합니다.다음과 같은 방법을 사용하는 데이터 액세스 클래스가 있는 경향이 있습니다.

public function CheckUsername($connection, $username)
{
    try
    {
        $data = array();
        $data['Username'] = $username;

        //// SQL
        $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";

        //// Execute statement
        return $this->ExecuteObject($connection, $sql, $data);
    }
    catch(Exception $e)
    {
        throw $e;
    }
}

내 모델은 데이터베이스 테이블에 매핑된 엔티티 클래스인 경우가 많습니다.

모델 오브젝트는 위의 코드뿐만 아니라 모든 데이터베이스 매핑 속성을 가져야 합니까?아니면 데이터베이스가 실제로 작동하는 코드를 분리해도 될까요?

네 겹으로 쌓이게 되나요?

면책사항: 다음은 PHP 기반 웹 응용 프로그램의 컨텍스트에서 MVC와 유사한 패턴을 이해하는 방법에 대한 설명입니다.콘텐츠에 사용되는 모든 외부 링크는 용어 및 개념을 설명하기 위한 이지 주제에 대한 내 자신의 신뢰성을 암시하는 것은 아닙니다.

가장 먼저 정리해야 할 것은 모델이 레이어라는 것입니다.

둘째, 기존 MVC와 웹 개발에서 사용하는 것 사이에는 차이가 있습니다.여기 제가 쓴 오래된 답변이 있습니다. 이 답변은 어떻게 다른지 간략하게 설명합니다.

모델이 아닌 것:

모델이 클래스나 단일 개체가 아닙니다.대부분의 프레임워크가 이러한 오해를 영속시키기 때문에 (원래의 답은 다른 것을 배우기 시작했을 때 쓰여졌지만) 저지르는 것은 매우 흔한 실수입니다.

Object-Relational Mapping Technology(ORM)도 데이터베이스 테이블의 추상화도 아닙니다.그렇지 않다고 말하는 사람은 새로운 ORM 또는 프레임워크 전체를 '판매'하려고 할 가능성이 높습니다.

모델이란?

적절한 MVC 적응에서 M은 모든 도메인 비즈니스 로직을 포함하며 모델 계층은 대부분 다음 세 가지 유형의 구조로 구성됩니다.

  • 도메인 오브젝트

    도메인 오브젝트는 순수하게 도메인 정보를 포함하는 논리 컨테이너입니다.일반적으로 문제 도메인 공간 내의 논리 엔티티를 나타냅니다.일반적으로 비즈니스 로직이라고 불립니다.

    여기서 청구서를 발송하기 전에 데이터를 검증하거나 주문의 총 비용을 계산하는 방법을 정의합니다.동시에 도메인 개체는 스토리지를 전혀 인식하지 못합니다.어디서 (SQL 데이터베이스, REST API, 텍스트 파일 등)가 어디서 저장되거나 검색되더라도 전혀 인식하지 못합니다.

  • 데이터 매퍼

    이러한 개체는 스토리지만 담당합니다.데이터베이스에 정보를 저장하는 경우 SQL이 상주합니다.또는 XML 파일을 사용하여 데이터를 저장하고 데이터 매퍼가 XML 파일 간에 구문 분석을 수행하는 경우도 있습니다.

  • 서비스

    상위 수준의 도메인 개체라고 생각할 수 있지만 비즈니스 로직 대신 서비스도메인 개체와 매퍼 간의 상호 작용을 담당합니다.이러한 구조는 결국 도메인 비즈니스 로직과 상호 작용하기 위한 "퍼블릭" 인터페이스를 만듭니다.이러한 로직은 피할 수 있지만 일부 도메인 로직이 컨트롤러에 누출되는 단점이 있습니다.

    ACL 실장 질문에는 이 주제에 관련된 답변이 있습니다.이 답변은 도움이 될 수 있습니다.

모델 레이어와 MVC 트라이애드의 다른 부분 간의 통신은 서비스를 통해서만 이루어져야 합니다.명확한 분리에는 몇 가지 추가적인 이점이 있습니다.

  • 단일 책임 원칙(SRP)을 적용하는 데 도움이 됩니다.
  • 로직이 변경되었을 경우에 대비하여 추가 '움직일 수 있는 공간'을 제공합니다.
  • 컨트롤러를 최대한 심플하게 유지
  • 외부 API가 필요한 경우 명확한 청사진을 제공합니다.

 

모델과의 상호작용 방법

전제 조건: Clean Code Talks의 "Global State and Singletons" 및 "Don't Look For Things!" 강의를 시청하십시오.

서비스 인스턴스에 대한 액세스 획득

View 인스턴스와 컨트롤러 인스턴스(호칭: "UI 계층)을 통해 이러한 서비스에 액세스하려면 다음 두 가지 일반적인 방법이 있습니다.

  1. 필요한 서비스를 뷰 및 컨트롤러의 생성자에 직접 주입할 수 있습니다(가능하면 DI 컨테이너를 사용하는 것이 좋습니다.
  2. 모든 뷰와 컨트롤러의 필수 의존관계로서 서비스 팩토리를 사용합니다.

짐작하시겠지만, DI 컨테이너는 훨씬 더 우아한 솔루션입니다(초보자에게는 쉽지 않습니다).이 기능을 고려하기 위해 권장하는2개의 라이브러리는 Syfmony의 스탠드아론 의존관계입니다.주입 컴포넌트 또는 오리엔.

팩토리와 DI 컨테이너를 사용하는 솔루션에서는 선택한 컨트롤러 간에 공유되는 다양한 서버의 인스턴스를 공유할 수도 있습니다.또한 특정 요구와 응답 사이클을 표시할 수도 있습니다.

모델 상태 변경

이제 컨트롤러의 모델 계층에 액세스할 수 있게 되었으므로 실제로 컨트롤러의 사용을 시작해야 합니다.

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $identity = $this->identification->findIdentityByEmailAddress($email);
    $this->identification->loginWithPassword(
        $identity,
        $request->get('password')
    );
}

컨트롤러에는 매우 명확한 작업이 있습니다.즉, 사용자의 입력을 받아 이 입력을 바탕으로 비즈니스 로직의 현재 상태를 변경하는 것입니다.이 예에서는 "anonymous user"와 "logged in user" 사이에서 변경되는 상태가 됩니다.

컨트롤러는 사용자의 입력을 검증할 책임이 없습니다.이는 비즈니스 규칙의 일부이며 컨트롤러는 여기서나 여기서나 볼 수 있는 과 같은 SQL 쿼리를 호출하지 않습니다(이러한 쿼리를 싫어하지 마십시오.이러한 정보는 잘못된 것이 아닙니다).

사용자에게 상태 변경을 표시합니다.

OK, 사용자가 로그인(또는 실패)했습니다.이제 어쩌죠?해당 사용자는 아직 모르고 있습니다.즉, 실제로 응답을 생성해야 합니다.그것은 뷰의 책임입니다.

public function postLogin()
{
    $path = '/login';
    if ($this->identification->isUserLoggedIn()) {
        $path = '/dashboard';
    }
    return new RedirectResponse($path); 
}

이 경우 뷰는 모형 계층의 현재 상태를 기반으로 두 가지 가능한 반응 중 하나를 생성합니다.다른 사용 사례의 경우 뷰는 "현재 선택된 기사"와 같은 항목에 따라 렌더링할 다른 템플릿을 선택할 수 있습니다.

프레젠테이션 레이어는 실제로 매우 정교해질 수 있습니다.여기서 설명한 바와 같이 "PHP에서의 MVC 뷰 이해"입니다.

그냥 REST API를 만들고 있어요!

물론 과잉 살상일 수도 있죠

MVC는 우려의 분리 원칙을 위한 구체적인 해결책일 뿐입니다.MVC는 사용자 인터페이스를 비즈니스 로직과 분리하고 UI에서 사용자 입력 처리와 프레젠테이션 처리를 분리합니다.이것은 매우 중요합니다.사람들은 종종 그것을 "삼각류"라고 표현하지만, 실제로는 세 개의 독립된 부분으로 구성되어 있지 않습니다.구조는 다음과 같습니다.

MVC 분리

즉, 프레젠테이션 층의 논리가 존재하지 않는 것에 가까울 경우, 실용적인 접근법은 이들을 단일 층으로 유지하는 것입니다.또한 모델 계층의 일부 측면을 상당히 단순화할 수 있습니다.

이 방법을 사용하면 (API의 경우) 로그인 예를 다음과 같이 기술할 수 있습니다.

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $data = [
        'status' => 'ok',
    ];
    try {
        $identity = $this->identification->findIdentityByEmailAddress($email);
        $token = $this->identification->loginWithPassword(
            $identity,
            $request->get('password')
        );
    } catch (FailedIdentification $exception) {
        $data = [
            'status' => 'error',
            'message' => 'Login failed!',
        ]
    }

    return new JsonResponse($data);
}

이는 지속 가능하지 않지만 응답 본문을 렌더링하는 데 복잡한 논리가 있는 경우 이 단순화는 더 사소한 시나리오에서 매우 유용합니다.그러나 복잡한 프레젠테이션 로직으로 대규모 코드베이스를 사용하려고 하면 이 접근법은 악몽이 될 수 있습니다.

 

모델 작성 방법

위에서 설명한 바와 같이 단일 "모델" 클래스가 없기 때문에 실제로 "모델을 빌드"하지 않습니다.대신 특정 방법을 수행할 수 있는 서비스를 만드는 것부터 시작합니다.그런 다음 도메인 개체와 매핑구현합니다.

서비스 방법의 예를 다음에 나타냅니다.

위의 두 접근 방식 모두 식별 서비스에 대한 이 로그인 방법이 있었습니다.실제로 어떻게 생겼을까?는 제가 쓴 라이브러리와 같은 기능의 약간 수정된 버전을 사용하고 있습니다.왜냐하면 나는 게을러서:

public function loginWithPassword(Identity $identity, string $password): string
{
    if ($identity->matchPassword($password) === false) {
        $this->logWrongPasswordNotice($identity, [
            'email' => $identity->getEmailAddress(),
            'key' => $password, // this is the wrong password
        ]);

        throw new PasswordMismatch;
    }

    $identity->setPassword($password);
    $this->updateIdentityOnUse($identity);
    $cookie = $this->createCookieIdentity($identity);

    $this->logger->info('login successful', [
        'input' => [
            'email' => $identity->getEmailAddress(),
        ],
        'user' => [
            'account' => $identity->getAccountId(),
            'identity' => $identity->getId(),
        ],
    ]);

    return $cookie->getToken();
}

보시다시피 이 추상화 수준에서는 데이터를 가져온 위치를 알 수 없습니다.데이터베이스일 수도 있지만 테스트용 모의 객체일 수도 있습니다.되고 있는 매퍼도, 「데이터 에는 숨겨져 .private이 서비스의 메서드.

private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
    $identity->setStatus($status);
    $identity->setLastUsed(time());
    $mapper = $this->mapperFactory->create(Mapper\Identity::class);
    $mapper->store($identity);
}

매퍼 작성 방법

지속성의 추상화를 구현하려면 가장 유연한 접근 방식은 맞춤형 데이터 매퍼를 만드는 것입니다.

매퍼도

송신원: PoEAA

실제로는 특정 클래스 또는 슈퍼 클래스와의 상호작용을 위해 구현됩니다., ㅇㅇㅇㅇㅇ가 있다고 합시다.Customer ★★★★★★★★★★★★★★★★★」Admin 다 (둘 다)에서됨)User슈퍼클래스)둘 다 다른 필드를 포함하기 때문에 서로 일치하는 매퍼가 따로 있을 수 있습니다.그러나 일반적으로 사용되는 공유 작업도 수행하게 됩니다.예를 들어, "온라인에서 마지막으로 본" 시간 업데이트입니다.또한 기존 매핑을 더욱 복잡하게 만드는 대신 일반적인 "사용자 매핑"을 갖는 것이 더 실용적인 접근법입니다. 이 접근법은 타임스탬프만 업데이트합니다.

기타 코멘트:

  1. 데이터베이스 테이블 및 모델

    데이터베이스 테이블, 도메인 개체 및 매퍼 사이에 직접 1:1:1 관계가 있는 경우도 있지만, 규모가 큰 프로젝트에서는 예상한 것보다 더 흔하지 않을 수 있습니다.

    • 단일 도메인 개체에서 사용되는 정보는 다른 테이블에서 매핑될 수 있지만 개체 자체는 데이터베이스에서 지속되지 않습니다.

      예: 월차 보고서를 생성하는 경우.다른 테이블에서 정보를 수집하지만 마법은 없습니다.MonthlyReport테이블이 표시됩니다.

    • 1개의 매퍼가 여러 테이블에 영향을 줄 수 있습니다.

      : 데이터 저장 시User개체, 이 도메인 개체는 다른 도메인 개체의 컬렉션을 포함할 수 있습니다.Group★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★.User데이터 매퍼는 여러 테이블에 엔트리를 업데이트하거나 삽입해야 합니다.

    • 단일 도메인 개체의 데이터가 여러 테이블에 저장됩니다.

      : 대규모 시스템(중규모 소셜 네트워크)에서는 사용자 인증 데이터 및 자주 액세스하는 데이터를 대규모 콘텐츠 청크와는 별도로 저장하는 것이 실용적일 수 있습니다.는 거의 필요하지 않습니다.그 경우엔 아직 싱글이 남아있을지도 몰라요User클래스이지만 포함된 정보는 전체 세부 정보를 가져왔는지 여부에 따라 달라집니다.

    • 각 도메인 개체에 대해 여러 개의 매퍼가 있을 수 있습니다.

      예: 공개용 및 관리 소프트웨어용 공유 코드를 사용하는 뉴스 사이트가 있습니다.단, 양쪽 인터페이스가 같은 것을 사용하지만Article관리직에서는 훨씬 더 많은 정보를 입력해야 합니다.'내부'는 '내부'는 '내부'는 '내부'는 '내부'는 '내부'는 '내부'는 '내부'는 '내부'는 '내부'는 '내부'입니다.각각 다른 쿼리를 수행하거나 서로 다른 데이터베이스(마스터 또는 슬레이브)를 사용합니다.

  2. 보기는 템플릿이 아닙니다.

    MVC의 보기 인스턴스(패턴의 MVP 배분을 사용하지 않는 경우)는 프레젠테이션 로직을 담당합니다.즉, 일반적으로 각 보기는 최소 몇 개의 템플릿을 저글링합니다.모델 계층에서 데이터를 가져온 다음 수신된 정보를 기반으로 템플릿을 선택하고 값을 설정합니다.

    이를 통해 얻을 수 있는 이점 중 하나는 재사용 가능성입니다.「」를 ListViewclass는코드를 기사 에 있는 할 수 .class는 사용자 목록과 코멘트를 제공합니다.왜냐하면 둘 다 같은 프레젠테이션 논리를 가지고 있기 때문입니다.템플릿을 바꾸기만 하면 됩니다.

    네이티브 PHP 템플릿을 사용하거나 서드파티 템플릿엔진을 사용할 수 있습니다.View 인스턴스를 완전히 대체할 수 있는 일부 타사 라이브러리가 있을 수도 있습니다.

  3. 이전 버전의 답변은 어떻습니까?

    유일한 큰 변화는 이전 버전에서 모델이라고 불리는 것이 실제로 서비스라는 것입니다.나머지 "도서관 비유"는 꽤 잘 들어맞는다.

    제가 본 유일한 결점은 이 라이브러리가 정말 이상한 도서관이라는 것입니다. 왜냐하면 이 라이브러리는 책으로부터 정보를 돌려주지만, 책 자체에 손을 대게 하지 않기 때문입니다. 그렇지 않으면 추상화가 "누출"되기 시작할 것이기 때문입니다.좀 더 적절한 비유를 생각해 봐야겠어요.

  4. View 인스턴스와 컨트롤러 인스턴스의 관계는 무엇입니까?

    MVC 구조는 ui와 model의 2개의 레이어로 구성됩니다.UI 계층의 주요 구조는 보기와 컨트롤러입니다.

    MVC 설계 패턴을 사용하는 웹 사이트를 다룰 때 가장 좋은 방법은 보기와 컨트롤러 간에 1:1 관계를 갖는 것입니다.각 보기는 웹 사이트의 전체 페이지를 나타내며 특정 보기에 대한 모든 수신 요청을 처리하는 전용 컨트롤러를 갖추고 있습니다.

    오픈된 기사를 하기 위해서 를 들면, '열린 기사', '열린 기사', '열린 기사', '열린 기사', '열린 기사', '열린 기사', '열린 기사', '열린 기사', '열린 기사 '열린 기사,\Application\Controller\Document ★★★★★★★★★★★★★★★★★」\Application\View\Document문서 처리에 관한 UI 계층의 주요 기능이 모두 포함되어 있습니다(물론 문서와 직접 관련이 없는 XHR 컴포넌트가 있을 수 있습니다).

비즈니스 로직인 모든 것은 데이터베이스 쿼리, 계산, REST 호출 등 모델에 속합니다.

모델 자체에서 데이터에 액세스할 수 있습니다. MVC 패턴은 이를 제한하지 않습니다.서비스나 매퍼 등으로 코팅할 수 있지만 모델의 실제 정의는 비즈니스 로직을 처리하는 레이어이며 그 이상도 이하도 아닙니다.원하는 경우 클래스, 함수 또는 수십억 개의 개체를 포함하는 전체 모듈을 사용할 수 있습니다.

데이터베이스 쿼리를 모델에서 직접 실행하는 대신 실제로 실행하는 개별 개체를 갖는 것이 항상 더 쉽습니다. 이는 특히 유닛 테스트 시 유용합니다(모델에 모의 데이터베이스 의존성을 쉽게 주입할 수 있기 때문에).

class Database {
   protected $_conn;

   public function __construct($connection) {
       $this->_conn = $connection;
   }

   public function ExecuteObject($sql, $data) {
       // stuff
   }
}

abstract class Model {
   protected $_db;

   public function __construct(Database $db) {
       $this->_db = $db;
   }
}

class User extends Model {
   public function CheckUsername($username) {
       // ...
       $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ...";
       return $this->_db->ExecuteObject($sql, $data);
   }
}

$db = new Database($conn);
$model = new User($db);
$model->CheckUsername('foo');

또한 PHP에서는 특히 예시와 같은 경우 역추적이 유지되기 때문에 예외를 포착/재시도할 필요가 거의 없습니다.예외는 그대로 두고 컨트롤러에서 검출합니다.

Web-"MVC"에서는 원하는 모든 것을 할 수 있습니다.

원래 개념은 이 모델을 비즈니스 로직으로 기술했습니다.애플리케이션 상태를 나타내고 데이터의 일관성을 적용해야 합니다.그 접근방식은 종종 "뚱뚱한 모델"로 묘사된다.

대부분의 PHP 프레임워크는 보다 얕은 접근 방식을 따르며, 이 경우 모델은 데이터베이스 인터페이스일 뿐입니다.그러나 적어도 이러한 모델은 수신 데이터와 관계를 검증해야 합니다.

어느 쪽이든 SQL 항목 또는 데이터베이스 호출을 다른 계층으로 분리하면 큰 문제가 없습니다.이렇게 하면 실제 스토리지 API가 아닌 실제 데이터/동작에만 관심을 가질 수 있습니다(너무 무리한 것은 무리입니다).예를 들어 데이터베이스 백엔드를 미리 설계하지 않으면 파일 저장소로 교체할 수 없습니다.)

에는 데이터, 및 되어 있으며, 은 모두 "Data", "Display", "Display", "Display", "Display"라는 되어 있습니다.M ,V ★★★★★★★★★★★★★★★★★」C

Model()-->M 어플리케이션 상태를 유지하는 Atribute를 가지고 있으며, 이 Atribute는 어플리케이션 상태에 대해 아무것도 모릅니다.V ★★★★★★★★★★★★★★★★★」C

View()-->V 어플리케이션의 표시 포맷이 있어 어플리케이션의 How-to-Digest 모델에 대해서만 알고 있으며 어플리케이션의 표시 포맷에 대해서는 신경 쓰지 않습니다.C.

Controller()-->C 어플리케이션의 처리 부분이 있어 M과 V 사이의 배선으로서 기능합니다.이것은 양쪽 모두에 의존합니다.M ,V와는 달리M ★★★★★★★★★★★★★★★★★」V

전체적으로 둘 사이에는 우려의 구분이 있다.향후 변경이나 기능 강화는 매우 쉽게 추가할 수 있습니다.

이 경우 쿼리, 가져오기 등의 모든 직접 데이터베이스 상호작용을 처리하는 데이터베이스 클래스가 있습니다.데이터베이스를 MySQL에서 Postgre로 변경해야 하는 경우SQL에는 문제가 없습니다.따라서 레이어를 추가하는 것이 편리합니다.

각 테이블에는 자체 클래스와 특정 메서드가 있을 수 있지만 실제로 데이터를 가져오려면 데이터베이스 클래스에서 다음과 같이 처리하도록 합니다.

★★Database.php

class Database {
    private static $connection;
    private static $current_query;
    ...

    public static function query($sql) {
        if (!self::$connection){
            self::open_connection();
        }
        self::$current_query = $sql;
        $result = mysql_query($sql,self::$connection);

        if (!$result){
            self::close_connection();
            // throw custom error
            // The query failed for some reason. here is query :: self::$current_query
            $error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n");
            $error->handleError();
        }
        return $result;
    }
 ....

    public static function find_by_sql($sql){
        if (!is_string($sql))
            return false;

        $result_set = self::query($sql);
        $obj_arr = array();
        while ($row = self::fetch_array($result_set))
        {
            $obj_arr[] = self::instantiate($row);
        }
        return $obj_arr;
    }
}

테이블 오브젝트 클래스l

class DomainPeer extends Database {

    public static function getDomainInfoList() {
        $sql = 'SELECT ';
        $sql .='d.`id`,';
        $sql .='d.`name`,';
        $sql .='d.`shortName`,';
        $sql .='d.`created_at`,';
        $sql .='d.`updated_at`,';
        $sql .='count(q.id) as queries ';
        $sql .='FROM `domains` d ';
        $sql .='LEFT JOIN queries q on q.domainId = d.id ';
        $sql .='GROUP BY d.id';
        return self::find_by_sql($sql);
    }

    ....
}

이 예가 좋은 구조를 만드는 데 도움이 되기를 바랍니다.

언급URL : https://stackoverflow.com/questions/5863870/how-should-a-model-be-structured-in-mvc

반응형