할 일, 취소 상태로 변경
할 일, 취소 상태로 변경
할 일을 취소하거나 취소 해제하는 기능은 할 일을 완료 처리하거나 완료 해제하는 기능과 거의 동일해요. completed_at 모델 필드 대신 cancelled_at 모델 필드의 값을 다룬다는 점이 다르죠. 그래서 이 부분 구현은 여러분께 과제로 남겨둘게요. 😁
할 일 상태 별 목록 가져오기
할 일을 완료 처리하거나 미완료 처리하는 기능을 구현했으니, 이번엔 완료한 일만, 완료하지 않은 일만 걸러내는 기능을 구현해보아요.
TodoService의 findall() API에 is_completed 필터 추가
지정한 조건으로 할 일을 가져오는 TodoService의 findall() API에 필터를 하나 추가할게요. 바로 is_completed로 True이면 완료한 일만, False이면 완료하지 않은 일만 걸러내 가져오도록 SQL 질의를 작성하는 데 사용해요.
완료한 일은 Todo 모델의 completed_at 모델 필드의 값이 None이 아닌 데이터예요. 데이터베이스에서는 NULL이 아닌 값이죠. 다음 SQL로 표현돼요.
SQLAlchemy로 이와 같은 SQL 질의를 작성하는 코드가 바로 다음 코드예요.
null()은 SQLAlchemy에서 제공하는 함수예요. 데이터베이스 SQL에서 NULL을 표현하는 데 사용하죠.
Todo 모델에 있는 is_completed 프로퍼티는 SQLAlchemy가 질의를 다루는 데 사용할 수 없어요. 이 프로퍼티는 Python 영역에서만 다룰 수 있으며, SQLAlchemy가 이 프로퍼티를 SQL 질의문을 작성하는 데 필요한 정보는 없죠.
list_todo HTTP API에 status 쿼리 인자 추가
TodoService의 API를 구현했으니 HTTP API에 반영합니다. 할 일의 완료 여부, 취소 여부는 서로 배타적이라서 HTTP QueryString의 status 인자 하나로 할 일의 상태를 지정하도록 할게요. FastAPI는 함수의 인자 정보를 참고하여 URL 경로 인자인지, QueryString 인자인지 따위를 알아서 처리해줘요. 그래서 list_todo() 종단점 함수에 status 인자를 추가하기만 하면 돼요.
status 인자의 값이 completed이면 findall() 메서드에 is_completed 키워드 인자 값으로 True를, incompleted이면 False를 전달합니다. 이 코드 구조에 취소한 일만 걸러내는 구현을 쉽게 추가할 수 있어요.
추가한 코드를 반영한 전체 코드는 다음과 같아요.
완료한 일만, 미완료한 일만 필터 UI 추가
마지막으로 필터 UI를 추가하면 끝나요.
SQLAlchemy의 hybrid_property로 SQL질의 선언적으로 작성
여기까지 구현하는 걸로 충분한데요. 다음 코드의 가독성이 썩 좋진 않아요.
비교적 간단한 구현이지만, 코드를 읽어야 비로소 어떤 의도인지 파악이 되거든요. 다음 의사 코드처럼 선언적으로 코드를 작성하면 의도가 잘 드러날 거예요.
-
Todo.is_completed.is_(True) : Todo이 완료됐다
-
Todo.is_completed.is_(True) : Todo이 완료되지 않았다
is_completed가 참인지 거짓인지를 판정하는 SQL 질의가 구체적으로 어떠한지를 코드로 드러내는 게 아니라 의도만 드러내는 거죠.
이러한 구현을 SQLAlchemy ORM 모델인 Todo 모델을 좀 더 선언적으로 구현해볼게요.
Todo 모델의 is_completed 를 SQLAlchemy 표현식으로
Todo 모델의 is_completed 프로퍼티는 SQLAlchemy가 질의를 작성하는 데 활용할 수 없다고 했는데요. SQLAlchemy에서 제공하는 hybrid_property 객체를 사용하면 활용할 수 있어요. 코드부터 보죠.
@property로 장식되어 있던 is_completed()를 @hybrid_property로 장식했어요. hybrid_property 클래스로 장식해 생성한 객체는 Python 영역에서 프로퍼티처럼 동작해요. 차이점은 이 객체로 SQLAlchemy의 SQL 표현식을 만드는 객체를 만들 수 있다는 점이죠.
바로 위에 있는 is_completed의 expression() 메서드를 is_completed() 메서드에 장식자로 사용했는데요. 이러면 이 is_completed()는 클래스 메서드처럼 동작해요.
이 코드를 예시로 삼아 설명해드릴게요. Todo 모델의 인스턴스 객체인 obj의 is_completed는 @hybrid_property 로 장식한 def is_completed() 예요. 프로퍼티처럼 다루죠.
Todo 클래스(모델)의 is_completed는 @is_completed.expression으로 장식한 def is_completed()예요. SQLAlchemy의 질의문을 생성할 때 사용하죠. 이 메서드의 구현을 보면 앞선 TodoService의 findall() 구현에서 Todo.completed_at.isnot(null())라고 작성한 코드와 동일해요.
findall()에 반영
이제 이 구현을 TodoService의 findall()에 반영할게요.
~Todo.is_completed는 Todo.is_completed의 부정(not) 표현이예요. 어떤가요? 코드가 좀 더 선언적으로 작성되어 선언적 문법을 따르는 SQL과 잘 어울리고, 코드 가독성도 좋지요? 🙂
여기까지 진행한 코드 커밋 : f5d0028