LGTM Devlog 29:Firestore 및 init 하위 클래스 dunders 및 메타클래스의 ORM

33989 단어 pythondevjournal
나는 또 재구성했다.나는 그것이 얼마나 어수선한지, 모든 게임, 사용자, 작업 실체가 그들의 데이터를 어떻게 처리하는지, 데이터베이스와 통신할 때 중복된 코드가 매우 많은데, 이런 상황이 발생할 때, 나는 그다지 만족스럽지 않다.
예를 들어 최근에 변경된 게임 대상은 다음과 같다.
class Game:
    @classmethod
    def from_user(cls, user: User) -> Union[Game, NoGameType]:
        """ Create a game from a user """
        key = cls.make_key(user)
        game = cls(key)
        game.user = user

        docs = db.collection("game").where("user_key", "==", user.key).stream()
        for _ in docs:
            return game
        return NoGame

    @classmethod
    def new_from_fork(cls, user: User, fork_url: str) -> Game:
        """ Save game with fork """

        if not fork_url:
            raise ValueError("Fork can't be blank")

        key = cls.make_key(user)
        game = cls(key)
        game.user = user

        game_doc = db.collection("game").document(game.key).get()
        if game_doc.exists:
            game_doc.reference.set(
                {
                    "fork_url": fork_url,
                    "user_uid": user.uid,
                    "user_key": user.key,
                },
                merge=True,
            )

        else:
            game_doc.reference.set(
                {
                    "fork_url": fork_url,
                    "user_uid": user.uid,
                    "user_key": user.key,
                    "joined": firestore.SERVER_TIMESTAMP,
                }
            )

        return game

    @staticmethod
    def make_key(user: User) -> str:
        """ Game's key ARE user key due to 1:1 relationship """
        return user.key

    key: str
    user: Union[User, NoUserType]

    def __init__(self, key: str):
        self.key = key
        self.user = NoUser

    def assign_to_uid(self, uid: str) -> None:
        """ Assign a user to this game """
        doc = db.collection("game").document(self.key).get()
        if doc.exists:
            doc.reference.set({"user_uid": uid}, merge=True)

    def start_first_quest(self) -> None:
        """ Create starting quest if not exist """
        QuestClass = Quest.get_first_quest()
        quest = QuestClass.from_game(self)
        quest.execute_stages(TickType.FULL)
        quest.save()

    def __repr__(self):
        return f"{self.__class__.__name__}(key={self.key})"
우리는 창설Game 대상의 두 가지 함수가 있다. new_from_fork()from_user()이다.또 하나의 방법assign_to_uid()은 하나의 속성을 데이터베이스에 기록하는 것이다.
보기에는 괜찮은데User 대상과 Quest 대상에는 중복된 코드가 많다.
따라서 나는 많은 중복 함수를 ORM 기류로 비뚤어져서 User, QuestGame 모두 이 함수를 계승할 수 있고 이 함수들은 데이터베이스에서 자신들을 저장하고 복원할 수 있다고 결정했다.
변경한 새 객체Game는 다음과 같습니다.
class Game(Orm, collection="game", parent_orm=User):
    data: GameData
    storage_model = GameData

    @classmethod
    def from_user(cls, user: User) -> Game:
        key = cls.make_key(user)
        game = cls(key)
        game.parent_key = user.key
        return game

    @staticmethod
    def make_key(user: User) -> str:
        """ Game's key ARE user key due to 1:1 relationship """
        return user.key

    def set_fork_url(self, fork_url: str) -> None:
        self.data.fork_url = fork_url
훨씬 깨끗해졌어!네가 알아차릴 수 있는 첫 번째 말은:
class Game(Orm, collection="game", parent_orm=User):
이것은 Python의 간결한 특성으로, PEP 487 과 같이 사용자 정의 원류를 허용합니다.이것은 __init_subclass__dunder가 동력을 제공한다. 이 dunder는 초기화 클래스에서 운행할 때(초기화 실례 때__init__에 비해) 이 예에서 보면 이렇다(Orm기류에서 나온다).
    def __init_subclass__(
        cls, collection: str, parent_orm: Union[Type[Orm], NoParentType] = NoParent
    ):
        """ Set collection and parent """
        cls.collection = collection
        cls.parent_orm = parent_orm
        cls.col_ref = db.collection(collection)
그래서 여기서 발생하는 것은 Game클래스가 초기화될 때 collection클래스 변수와 parent_orm클래스 변수를 설정하는 것이다.비록 우리는 __init__를 사용하여 집합과 부모 대상을 설정할 수 있지만 이 방법은 Game클래스 사이에 좋은 관심사 분리를 제공했다. 그의 클래스 속성은 게임 집합과 관련이 있다.그리고 Game류의 특정한 실례로 이 집합의 특정한 항목과 관련이 있다.col_ref의 설정__init_subclass__은 우리가 Game.col_ref 조작을 실행할 수 있도록 허락한다. 실례화된 게임 대상을 필요로 하지 않는다(우리는 모든 게임을 인용할 때 이렇게 할 필요가 없다).
다른 변화는 삭제new_from_fork()와 짧은set_fork_url() 설정기로 교체하는 것이다.왜냐하면 new_from_fork()from_user() 모두 user 파라미터가 필요하다는 것을 깨달았기 때문에 여기에 중복이 하나 있다.assign_to_uid() 현재 Orm 기류의 parent_key 속성을 사용할 수 있습니다.임무의 물건은 새로운QuestPage 물체로 옮겨졌는데, 그곳에서 더욱 관련이 있다.

ORM
기본 ORM 기본 클래스의 작업은Firestore 대상과 Python 대상 사이에 데이터를 비추는 것입니다.그것은 Game, User와 새로운QuestPage 대상에 필요한 모든 공유 코드를 가지고 있다.그것의 상반부는 이렇게 보인다.

class Orm(ABC):
    """ ORM base class links stuff together """

    @property
    @abstractmethod
    def storage_model(cls) -> Type[BaseModel]:
        """ Storage model """
        return NotImplemented

    collection: ClassVar[str]
    parent_orm: ClassVar[Union[Type[Orm], NoParentType]]
    col_ref: CollectionReference

    def __init_subclass__(
        cls, collection: str, parent_orm: Union[Type[Orm], NoParentType] = NoParent
    ):
        """ Set collection and parent """
        cls.collection = collection
        cls.parent_orm = parent_orm
        cls.col_ref = db.collection(collection)

    key: Union[str, NoKeyType]
    parent_key: Union[str, NoKeyType]
    data: BaseModel

    def __init__(self, key: Union[str, NoKeyType] = NoKey):
        self.key = key
        self.data = self.storage_model()
        self.parent_key = NoKey

    @property
    def parent(self) -> Union[Orm, OrmNotFoundType]:
        if self.parent_orm is not NoParent and self.parent_key is not NoKey:
            return self.parent_orm(self.parent_key)

        return OrmNotFound

    ...
하반부는 데이터베이스에서 데이터를 저장하고 불러오는 메커니즘과 관련이 있다.
저는 __init_subclass__에 설명했습니다. 본 ABC의 나머지 부분은 구체적으로 자신의 storage_model를 제공해야 한다고 요구했습니다. Pydantic 모델이고 기본 클래스의 방법은 이 모델을 사용하여 데이터 저장소를 만들고 복구할 것입니다.이것은 parent 속성이라는 속성을 가지고 있으며, parent_key 속성으로 연결된 부모 대상의 실례를 되돌려줍니다.예를 들어, 인스턴스화된Game 객체가 있으면 다음과 같은 방법으로 사용자를 가져올 수 있습니다.
game = Game(key)
game.load()

user = game.parent
Game 대상은 아버지 ORM 대상이User인 것을 알고 Game 대상도 parent_key 속성을 저장하기 때문이다.이것은 사용자 대상을 되돌릴 수 있는 충분한 상세함이다.

임무서
나는 앞의 Quest 대상에 대해 약간의 큰 재구축을 진행하였다.그것은 현재 두 부분으로 나뉜다. Quest 운행 임무와 관련된 모든 내용을 포함하고 단일 임무로 구체적으로 실현되는 기류로 한다.QuestPage, 이것은 Orm의 하위 클래스로 데이터베이스에 저장되고 불러오는 작업과 관련이 있습니다.나는 QuestPage를 임무 로그의 한 페이지로 상상하는데 Quest와 관련된'데이터 저장'을 포함하고 후자는 임무의 실제 실현이다.
여기에 일부 합성이 있는데 QuestPage에는 quest의 실례가 속성으로 되어 있기 때문이다.

class QuestPage(Orm, collection="quest", parent_orm=Game):
    data: QuestData
    storage_model = QuestData
    quest: Quest

    @staticmethod
    def make_key(game: Game, quest_name: str) -> str:
        if not quest_name:
            raise ValueError("quest_name must be valid")
        return f"{game.key}:{quest_name}"

    @classmethod
    def from_game_get_first_quest(cls, game: Game) -> QuestPage:
        return cls.from_game_get_quest(game, FIRST_QUEST_NAME)

    @classmethod
    def from_game_get_quest(cls, game: Game, quest_name: str) -> QuestPage:
        key = cls.make_key(game, quest_name)
        quest = cls(key, quest_name)
        return quest

    @classmethod
    def iterate_all(cls) -> Generator[QuestPage, None, None]:
        """ Iterate over all quests, the generator yields loaded quest_pages """
        docs = cls.col_ref.where("complete", "!=", True).stream()
        for doc in docs:
            data = cls.storage_model.parse_obj(doc.to_dict())
            quest_page = cls(doc.id, data.quest_name)
            quest_page.data = data
            quest_page.quest.load_raw(data.version, data.serialized_data)
            yield quest_page

    def __init__(self, key: str, quest_name):
        super().__init__(key)
        self.quest = Quest.from_name(quest_name, self)
        self.data.quest_name = quest_name

    def load(self) -> None:
        """ Additionally parse the quest storage """
        super().load()
        if isinstance(self.quest, Quest):
            self.quest.load_raw(self.data.version, self.data.serialized_data)

    def save(self) -> None:
        """ Additionally parse out the quest storage """
        if isinstance(self.quest, Quest):
            self.data.serialized_data = self.quest.save_raw()
            self.data.version = str(self.quest.version)
        super().save()
    ...
이것은 ORM 기본 클래스__init__(), save()load()를 확장하여quest 페이지 데이터 모델과 다른quest 데이터 모델을 저장하고 불러올 수 있도록 합니다.이러한 분할의 원인은 임무 자체는 자신의 변수 공간을 가지고 임무와 관련된 데이터를 저장하고 모든 메타데이터인 버전 번호, 완성된 임무 목록 등은 단독으로 저장되기 때문이다.
게다가 다른 변화까지 겹쳐 이 재구성은 새로운 기능을 추가하지 않고 코드를 다시 정리했다.이런 거 많이 안 했으면 좋겠어요.

좋은 웹페이지 즐겨찾기