Skip to content

Commit ce11765

Browse files
authored
Transactional 데코레이터 추가 (#10)
* Add transactional decorator.. * Add transactional decorator test.. * Set public session in repository.. * Fix conditional code.. * Refactor code.. * Fix session is not defined bug.. * Add README korean.. * Add ignore dist.. * Refactor common package.. * Add PyPI deploy code..
1 parent 4466ae6 commit ce11765

File tree

13 files changed

+479
-59
lines changed

13 files changed

+479
-59
lines changed

.github/workflows/release.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
- 'main'
77

88
jobs:
9-
build:
9+
deploy:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: rymndhng/release-on-push-action@master
@@ -15,3 +15,22 @@ jobs:
1515
with:
1616
bump_version_scheme: norelease
1717
tag_prefix: v
18+
19+
- name: Test environment Setup Python..
20+
uses: actions/setup-python@v2
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Pull github repositories...
25+
uses: actions/checkout@v1
26+
27+
- name: Test environment Setup Poetry
28+
uses: abatilo/actions-poetry@v2.0.0
29+
with:
30+
poetry-version: ${{ matrix.poetry-version }}
31+
32+
- name: Set deploy config Python Micro Framework Data
33+
run: |
34+
poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}
35+
poetry build
36+
poetry publish

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea
22
__pycache__
33
test.db
4-
venv/*
4+
venv/*
5+
dist/*

README.ko.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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``` 데코레이터를 사용합니다.

README.md

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,20 @@
11
# Python Micro Framework Data
22

3-
Python Micro Framework Data makes database connections easier in microframeworks like [Falcon](https://falcon.readthedocs.io/en/stable/) and [FastAPI](https://fastapi.tiangolo.com/).
4-
5-
This library is created with the motive of [Spring Data Commons](https://docs.spring.io/spring-data/commons/docs/current/reference/html/), and the relational database is implemented based on [SQLAlchemy](https://www.sqlalchemy.org/)
6-
7-
8-
9-
Currently, this library is **still under development**. The only stable database, **SQLAlchemy**, is the relational database, and we plan to implement it so that it can be used separately according to synchronous and asynchronous processing.
3+
* [한국어](https://github.com/NEONKID/python-mf-data/blob/main/README.ko.md)
104

115

126

137
<br />
148

159

1610

17-
## How to install (rdb)
11+
Python Micro Framework Data makes database connections easier in microframeworks like [Falcon](https://falcon.readthedocs.io/en/stable/) and [FastAPI](https://fastapi.tiangolo.com/).
1812

19-
pymfdata can use the extra option to specify which database to use. When using this library for relational database connection, use the command below.
13+
This library is created with the motive of [Spring Data Commons](https://docs.spring.io/spring-data/commons/docs/current/reference/html/), and the relational database is implemented based on [SQLAlchemy](https://www.sqlalchemy.org/)
2014

21-
```python
22-
$ pip install pymfdata[rdb]
23-
```
2415

25-
Or if you are using poetry, use the following command
2616

27-
```python
28-
$ poetry add "pymfdata[rdb]"
29-
```
17+
Currently, this library is **still under development**. The only stable database, **SQLAlchemy**, is the relational database, and we plan to implement it so that it can be used separately according to synchronous and asynchronous processing.
3018

3119

3220

@@ -164,3 +152,62 @@ When using the Repository protocol provided by pymfdata, the following basic met
164152
In addition to the methods provided by default in pymfdata, you can also create and use methods as in the code above.
165153

166154
Since the repository of ```pymfdata``` uses the Python [Protocol](https://www.python.org/dev/peps/pep-0544/#using-protocols), it can be used like a Java interface by implementing a separate Protocol.
155+
156+
```python
157+
from pymfdata.rdb.repository import AsyncRepository, AsyncSession
158+
from typing import Optional
159+
160+
161+
class MemoRepository(AsyncRepository[MemoEntity, int]):
162+
def __init__(self, session: Optional[AsyncSession]):
163+
self.session = session
164+
165+
@async_transactional(read_only=True)
166+
async def find_by_title(title: str) -> MemoEntity:
167+
# Todo: Implement the session code, but omit the session begin and commit code.
168+
```
169+
170+
Additionally, it provides a way to reduce duplicated code in a session by using the ```transactional``` decorator. The ```transactional``` decorator is divided into asynchronous and synchronous, and must be used according to the implemented connection.
171+
172+
173+
174+
<br />
175+
176+
177+
178+
## Unit Of Work Example (rdb)
179+
180+
SQLAlchemy uses the unit of work pattern by default, but if you want to define your own unit of work, you can use this library to define it.
181+
182+
```python
183+
from sqlalchemy.engine import Engine
184+
from sqlalchemy.ext.asyncio import AsyncEngine
185+
186+
from pymfdata.rdb.command import AsyncSQLAlchemyUnitOfWork, SyncSQLAlchemyUnitOfWork
187+
from pymfdata.rdb.transaction import async_transactional
188+
189+
190+
class AsyncMemoUseCaseUnitOfWork(AsyncSQLAlchemyUnitOfWork):
191+
def __init__(self, engine: AsyncEngine) -> None:
192+
super().__init__(engine)
193+
194+
async def __aenter__(self):
195+
await super().__aenter__()
196+
197+
self.memo_repository: MemoRepository = MemoRepository(self.session)
198+
199+
200+
class MemoUseCase:
201+
def __init__(self, uow: AsyncMemoUseCaseUnitOfWork) -> None:
202+
self.uow = uow
203+
204+
@async_transactional(read_only=True)
205+
async def find_by_id(self, item_id: int):
206+
return await self.uow.memo_repository.find_by_pk(item_id)
207+
```
208+
209+
The unit of work pattern is also divided into asynchronous and synchronous classes, and it must be used according to the connection.
210+
211+
The created unit of work class can be used in the class containing business logic, and we define them as **UseCase**. This class contains the business logic of the application.
212+
213+
When using the unit of work pattern, use the transactional decorator on business logic methods to handle transaction processing.

0 commit comments

Comments
 (0)