|
| 1 | +# Python Micro Framework Data |
| 2 | + |
| 3 | +* [English](README.md) |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | +<br /> |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +Python Micro Framework Data는 [Falcon](https://falcon.readthedocs.io/en/stable/), [FastAPI](https://fastapi.tiangolo.com/)와 같은 마이크로프레임워크 계열에서 데이터베이스 연결을 쉽게 해주는 라이브러리입니다. |
| 12 | + |
| 13 | +이 라이브러리는 [Spring Data Commons](https://docs.spring.io/spring-data/commons/docs/current/reference/html/)를 모티브로 개발하였으며 NoSQL과 관계형 데이터베이스에 대해 지원하고자 합니다. 현재, SQLAlchemy로 구현된 관계형 데이터베이스의 코드는 안정적이며 NoSQL은 현재 MongoDB부터 지원을 계획하고 있습니다. |
| 14 | + |
| 15 | + |
| 16 | + |
| 17 | +<br /> |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +## Connection Example (rdb) |
| 22 | + |
| 23 | +아래의 코드는 pymfdata에 있는 SQLAlchemy의 커넥션을 생성하는 예제입니다. |
| 24 | + |
| 25 | +```python |
| 26 | +from pymfdata.rdb.connection import AsyncSQLAlchemy |
| 27 | + |
| 28 | +connection = AsyncSQLAlchemy(db_uri='postgresql+asyncpg://{}:{}@{}:{}/{}'.format( |
| 29 | + 'postgres', 'postgres', '127.0.0.1', '5432', 'test')) |
| 30 | +``` |
| 31 | + |
| 32 | +```python |
| 33 | +from pymfdata.rdb.connection import SyncSQLAlchemy |
| 34 | + |
| 35 | +connection = SyncSQLAlchemy(db_uri='postgresql+psycopg2://{}:{}@{}:{}/{}'.format( |
| 36 | + 'postgres', 'postgres', '127.0.0.1', '5432', 'test')) |
| 37 | +``` |
| 38 | + |
| 39 | +SQLAlchemy는 비동기 커넥션을 지원합니다. 따라서 pymfdata에서도 이에 맞게 동기 커넥션과 비동기 커넥션 클래스가 나뉘어져 있으며 이에 맞게 사용할 수 있습니다. |
| 40 | + |
| 41 | +(위 코드는 PostgreSQL을 사용했을 때의 예제입니다. 만약, 다른 데이터베이스의 연결이 필요하다면 아래의 링크를 참고하십시오) |
| 42 | + |
| 43 | +* MySQL (https://docs.sqlalchemy.org/en/14/dialects/mysql.html#module-sqlalchemy.dialects.mysql.pymysql) |
| 44 | +* Oracle (https://docs.sqlalchemy.org/en/14/dialects/oracle.html) |
| 45 | +* PostgreSQL (https://docs.sqlalchemy.org/en/14/dialects/postgresql.html) |
| 46 | +* Microsoft SQL Server (https://docs.sqlalchemy.org/en/14/dialects/mssql.html) |
| 47 | +* SQLite (https://docs.sqlalchemy.org/en/14/dialects/sqlite.html) |
| 48 | + |
| 49 | +커넥션 풀에 대한 설정은 아래의 코드를 참고하십시오. |
| 50 | + |
| 51 | +```python |
| 52 | +await connection.connect(pool_size=5, pool_recycle=3600) |
| 53 | +``` |
| 54 | + |
| 55 | +```python |
| 56 | +connection.connect(pool_size=5, pool_recycle=3600) |
| 57 | +``` |
| 58 | + |
| 59 | +만들어진 객체는 ```connect``` 메서드 호출 후 사용할 수 있으며, 커넥션 메서드에서 커넥션 풀의 갯수 등을 설정할 수 있습니다. |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | +<br /> |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | +## Session Factory Example (rdb) |
| 68 | + |
| 69 | +SQLAlchemy에서 제공하는 DB Session 객체를 직접 사용하고자 하는 경우, 아래의 session_factory 예제 코드를 참고하십시오. |
| 70 | + |
| 71 | +```python |
| 72 | +connection.init_session_factory() |
| 73 | +``` |
| 74 | + |
| 75 | +위에서 만든 커넥션 객체에서 ```init_session_factory()``` 메서드를 통해 매 요청마다 session을 만들 수 있습니다. 이 메서드는 SQLAlchemy에서 제공하는 작업 단위 패턴인 ```scoped_session()``` 및 ```async_scoped_session()```을 사용하기 때문에 **기본적으로 스레드 혹은 태스크 별로 session이 할당**됩니다. |
| 76 | + |
| 77 | +```python |
| 78 | +async with connection.session() as session: |
| 79 | + stmt = "SELECT * from test" |
| 80 | + result = await session.execute(stmt) |
| 81 | +``` |
| 82 | + |
| 83 | +```python |
| 84 | +with connection.session() as session: |
| 85 | + stmt = "SELECT * from test" |
| 86 | + result = session.execute(stmt) |
| 87 | +``` |
| 88 | + |
| 89 | +할당된 세션은 커넥션 객체의 session을 파이썬의 컨텍스트 매니저를 이용해 할당 받아 사용할 수 있습니다. |
| 90 | + |
| 91 | + |
| 92 | + |
| 93 | +<br /> |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | +## Repository Example (rdb) |
| 98 | + |
| 99 | +pymfdata에서 제공하는 저장소(Repository) 패턴을 이용하기 위해서는 먼저 Entity를 생성하고 제네릭에 그 타입과 기본키 타입을 지정해줘야 합니다. |
| 100 | + |
| 101 | +```python |
| 102 | +from pymfdata.rdb.connection import Base |
| 103 | +from sqlalchemy import BigInteger, Column, String, Text |
| 104 | +from typing import Union |
| 105 | + |
| 106 | + |
| 107 | +class MemoEntity(Base): |
| 108 | + __tablename__ = 'memo' |
| 109 | + |
| 110 | + id: Union[int, Column] = Column(BigInteger, primary_key=True, autoincrement=True) |
| 111 | + title: Union[str, Column] = Column(String(128), nullable=True) |
| 112 | + content: Union[str, Column] = Column(Text, nullable=True) |
| 113 | +``` |
| 114 | + |
| 115 | +SQLAlchemy를 이용해 ORM 모델을 만들 때는 ```declarative_base```를 이용합니다. pymfdata에서 이미 정의된 변수가 있으므로 이를 import하여 사용할 수도 있습니다. |
| 116 | + |
| 117 | +```python |
| 118 | +from pymfdata.rdb.repository import AsyncRepository, AsyncSession |
| 119 | +from typing import Optional |
| 120 | + |
| 121 | + |
| 122 | +class MemoRepository(AsyncRepository[MemoEntity, int]): |
| 123 | + def __init__(self, session: Optional[AsyncSession]): |
| 124 | + self._session = session |
| 125 | + |
| 126 | + async def find_by_title(title: str) -> MemoEntity: |
| 127 | + # Todo: define session code |
| 128 | +``` |
| 129 | + |
| 130 | +pymfdata에서 제공하는 Repository Protocol에는 아래의 기본 메서드들이 제공됩나다. |
| 131 | + |
| 132 | +* ```delete(entity_model)``` : ORM 모델을 인자로 받아 데이터베이스에서 해당 모델을 제거하는 메서드 |
| 133 | + |
| 134 | +* ```find_by_pk(pk)``` : 기본키에 해당하는 ORM 모델을 반환하는 메서드 (**Spring Data JPA** 의 ```find_by_id```와 동일) |
| 135 | + |
| 136 | +* ```find_by_col(**kwargs)``` : 컬럼 이름을 인자 키로 지정 후, 찾고자 하는 값을 넣으면 해당 컬럼의 값과 매칭되는 ORM 모델을 반환하는 메서드 (***단, 한 개만 반환***) |
| 137 | + |
| 138 | +* ```find_all(**kwargs)``` : 컬럼 이름을 인자 키로 지정 후, 찾고자 하는 값을 넣으면 해당 컬럼의 값과 매칭되는 ORM 모델들을 반환하는 메서드. (***모든 데이터 반환***) |
| 139 | + |
| 140 | + (**Spring Data JPA**의 ```find_all```과 유사) |
| 141 | + |
| 142 | +* ```is_exists(**kwargs)``` : 컬럼 이름을 인자 키로 지정 후, 찾고자하는 값을 넣으면 해당 컬럼의 값과 매칭되는 행이 있으면 ```True```, 없으면 ```False``` 반환하는 메서드. |
| 143 | + |
| 144 | +* ```create(entity_model)``` : ORM 모델을 인자로 받아 데이터베이스에 해당 모델을 추가하는 메서드 |
| 145 | + |
| 146 | +* ```update(entity_model, req_dict)``` : ORM 모델과 딕셔너리 데이터를 인자로 받고, ORM 모델의 데이터를 업데이트하는 메서드 |
| 147 | + |
| 148 | +pymfdata에서 제공하는 기본 메서드들 외에도 구현한 Repository 클래스에 원하는 메서드를 구현할 수 있습니다. |
| 149 | + |
| 150 | +또한 Repository 클래스는 Java의 Interface와 유사한 Python의 [Protocol](https://www.python.org/dev/peps/pep-0544/#using-protocols)로 구현되어 있어 이를 이용해 Interface처럼 구현할 수도 있습니다. |
| 151 | + |
| 152 | +```python |
| 153 | +from pymfdata.rdb.repository import AsyncRepository, AsyncSession |
| 154 | +from pymfdata.rdb.transaction import async_transactional |
| 155 | +from typing import Optional |
| 156 | + |
| 157 | + |
| 158 | +class MemoRepository(AsyncRepository[MemoEntity, int]): |
| 159 | + def __init__(self, session: Optional[AsyncSession]): |
| 160 | + self.session = session |
| 161 | + |
| 162 | + @async_transactional(read_only=True) |
| 163 | + async def find_by_title(title: str) -> MemoEntity: |
| 164 | + # Todo: Implement the session code, but omit the session begin and commit code. |
| 165 | +``` |
| 166 | + |
| 167 | +pymfdata에서 제공하는 session은 autocommit 옵션이 주어져 있지 않아, ```commit()``` 호출이 필요한 경우 메서드에서 ```commit()``` 메서드를 호출해야 합니다. 여기서 pymfdata의 ```transactional``` 데코레이터를 이용하면 이러한 보일러 플레이트 코드를 신경쓰지 않아도 됩니다. |
| 168 | + |
| 169 | +단, 커넥션의 동기, 비동기 여부에 따라서 ```async_transactional```, ```sync_transactional```을 맞춰서 사용해야 합니다. |
| 170 | + |
| 171 | + |
| 172 | + |
| 173 | +<br /> |
| 174 | + |
| 175 | + |
| 176 | + |
| 177 | +## Unit Of Work Example (rdb) |
| 178 | + |
| 179 | +SQLAlchemy에서 기본적으로 제공하는 작업 단위 패턴 외에 직접 작업 단위 패턴을 구현하여 사용하고자 하는 경우 이 라이브러리에서 제공하는 작업 단위 패턴을 이용해 보십시오. |
| 180 | + |
| 181 | +```python |
| 182 | +from sqlalchemy.engine import Engine |
| 183 | +from sqlalchemy.ext.asyncio import AsyncEngine |
| 184 | + |
| 185 | +from pymfdata.rdb.command import AsyncSQLAlchemyUnitOfWork, SyncSQLAlchemyUnitOfWork |
| 186 | +from pymfdata.rdb.transaction import async_transactional |
| 187 | + |
| 188 | + |
| 189 | +class AsyncMemoUseCaseUnitOfWork(AsyncSQLAlchemyUnitOfWork): |
| 190 | + def __init__(self, engine: AsyncEngine) -> None: |
| 191 | + super().__init__(engine) |
| 192 | + |
| 193 | + async def __aenter__(self): |
| 194 | + await super().__aenter__() |
| 195 | + |
| 196 | + self.memo_repository: MemoRepository = MemoRepository(self.session) |
| 197 | + |
| 198 | + |
| 199 | +class MemoUseCase: |
| 200 | + def __init__(self, uow: AsyncMemoUseCaseUnitOfWork) -> None: |
| 201 | + self.uow = uow |
| 202 | + |
| 203 | + @async_transactional(read_only=True) |
| 204 | + async def find_by_id(self, item_id: int): |
| 205 | + return await self.uow.memo_repository.find_by_pk(item_id) |
| 206 | +``` |
| 207 | + |
| 208 | +작업 단위 패턴 또한 비동기, 동기에 따라 클래스가 별도로 구현되어 있습니다. 생성한 커넥션에 맞춰 사용하시면 됩니다. |
| 209 | + |
| 210 | +이렇게 만들어진 작업 단위 클래스는 애플리케이션의 비즈니스 로직을 정의할 ***UseCase*** 클래스에 담아 사용할 수 있습니다. 사실상 비즈니스 로직에서 트랜잭션이 필요로 하는 경우의 코드이기 때문에 작업 단위 패턴에 있는 메서드에 ```transactional``` 데코레이터를 사용합니다. |
0 commit comments