원문 캡처 · github.com
원문 캡처 · github.com

DoveORM은 검증 기능을 갖춘 모델, 연결해 쓰는 쿼리, 관계와 데이터베이스 마이그레이션을 하나의 API로 제공하는 경량 파이썬 ORM이다. Python 3.9 이상에서 사용할 수 있으며 SQLite는 추가 의존성이 없다. MySQL에는 mysqlclient, PostgreSQL에는 psycopg가 필요하다. Flask·FastAPI에 연결하거나 웹 프레임워크 없이 독립적으로 쓸 수 있다.

저장소와 전체 사용 설명서는 DoveORM GitHub 저장소를 참고하면 된다.

설치는 pip install doveorm으로 한다. MySQL/MariaDB 지원은 pip install "doveorm[mysql]", PostgreSQL 지원은 pip install "doveorm[postgres]"로 추가한다. SQLite는 별도 패키지가 필요 없다. PostgreSQL은 psycopg 3을 사용하며, 이미 psycopg2가 설치돼 있으면 이를 사용할 수도 있다. Python 3.9·3.10에서는 명령줄 도구가 설정 파일을 읽도록 tomli도 설치된다. 패키지에는 doveorm 명령이 포함되며 python -m doveorm으로도 실행할 수 있다.

데이터베이스 위치와 접속 정보는 애플리케이션이 명시해야 한다. SQLite에는 DATABASE_PATH, MySQL과 PostgreSQL에는 데이터베이스 이름·사용자·비밀번호·호스트 설정이 필요하다. 비밀번호가 없는 경우에도 빈 문자열을 명시해야 한다. 설정이 빠지거나 알 수 없는 설정 이름을 쓰면 ConfigurationError가 발생하며, ORM은 기본 계정이나 메모리 데이터베이스를 임의로 선택하지 않는다. configure()를 호출하기 전에는 get_database()도 오류를 낸다. 비밀값은 코드에 직접 넣지 말고 환경 변수나 비밀 관리자를 이용한다. 설정은 키워드 인수, DATABASES 딕셔너리, DOVEORM_* 환경 변수 또는 자체 설정 모듈에서 전달할 수 있다. configure_from_file()은 파일을 실행하므로 사용이 중단될 예정이다. 테스트나 한 프로세스에서 여러 데이터베이스를 쓸 때는 Database(":memory:")를 만든 뒤 User.use(db)처럼 모델에 직접 연결할 수 있다.

모델은 @table() 데코레이터나 class Meta로 정의한다. 테이블 이름, 인덱스, 유일성 제약, 기본 정렬 순서를 지정할 수 있고, 기본 테이블 이름은 클래스 이름에서 복수형 스네이크 케이스로 만든다. 기본 키를 지정하지 않으면 id = AutoField()가 자동으로 추가된다. 필드 유형은 30개 이상이며 문자열, 수치, 날짜·시간, UUID, JSON, 바이너리, 파일 경로, 외래 키 등을 지원한다. 필드에는 nullable, default, unique, db_index, choices, sensitive, editable 같은 옵션을 지정한다. 파일·이미지 필드는 파일 자체가 아니라 경로만 저장한다.

마이그레이션 대상이 아닌 데이터베이스 뷰나 다른 서비스가 관리하는 테이블은 migrate = False로 선언할 수 있다. 이 모델은 조회·저장·관계에는 쓸 수 있지만 DoveORM이 만들거나 변경하거나 삭제하지 않는다. generate, verify, 스키마 검사도 이를 건너뛰며 doveorm models에서는 external로 표시한다. 테이블은 먼저 직접 만들어야 한다. 마이그레이션 대상 모델은 이런 모델을 가리키는 ForeignKey를 선언할 수 없다. 이 설정은 추상 모델과 달리 하위 모델에 상속되지 않는다.

DoveORM은 SQLite, MySQL/MariaDB, PostgreSQL을 플러그형 드라이버로 지원한다. SQLite는 Python 표준 라이브러리의 sqlite3를 쓴다. MySQL/MariaDB는 MySQL 5.7 이상 또는 MariaDB와 mysqlclient, PostgreSQL은 PostgreSQL 10 이상과 psycopg 3 또는 psycopg2를 사용한다. 연결은 스레드마다 따로 관리하고, 자식 프로세스는 부모 프로세스의 연결을 재사용하지 않는다. 중첩 트랜잭션은 SAVEPOINT를 이용한다.

쿼리셋은 지연 실행된다. where(), exclude(), order_by(), limit(), offset() 등을 연결한 뒤 .get()이나 순회, len(), 인덱싱 등으로 평가한다. 결과는 캐시되지만 .get()은 매번 쿼리를 다시 실행한다. Q 객체로 AND·OR·NOT 조건을 묶고, findByEmail, findAllByActive, countBy…, existsBy… 같은 동적 조회 메서드도 사용할 수 있다. prefetch_related()는 HasMany, HasOne, BelongsTo, ManyToMany 관계를 미리 불러온다. ID가 많은 경우 500개씩 나눠 조회한다.

필드 값은 저장할 때 검증한다. 형식·범위·길이·선택 항목을 확인하며, 여러 필드에 걸친 검증은 clean()에서 정의할 수 있다. full_clean()은 저장 없이 같은 검사를 한다. 요청 본문을 그대로 create(**data)에 넘기지 말고 from_input()이나 assign()을 이용한다. 이 메서드는 수정 가능한 필드만 설정하며, 허용 목록을 지정할 수 있다. 기본 키, 버전 필드, 자동 갱신 날짜 필드와 editable=False인 필드는 입력으로 바꿀 수 없다. sensitive=True인 값은 to_dict()에서 기본적으로 빠지고 검증 오류에서는 가려진다.

SQL 값은 바인딩 매개변수로 전달하며 테이블·열·인덱스 이름은 검증한다. 원시 SQL에서도 모든 데이터베이스에서 ? 자리표시자를 쓸 수 있고, MySQL과 PostgreSQL에서는 드라이버가 이를 변환한다. 사용자 입력은 반드시 매개변수로 전달해야 한다. 필터 없는 대량 update()와 delete()는 Model.all()에서 만든 쿼리셋이 아닌 한 거부하며, 대량 쓰기와 limit()·offset()을 함께 쓰는 것도 막는다. 잘못된 필드나 조회 연산자는 조용히 무시되지 않고 ValueError를 일으킨다. 부정 조건은 NULL 행도 포함한다. 텍스트의 NUL 문자는 거부하고, 낙관적 잠금은 오래된 객체의 저장·삭제를 ConcurrencyError로 막는다.

트랜잭션은 with db.transaction(): 또는 db.atomic()으로 연다. 성공하면 커밋하고 예외가 나면 롤백한다. db.run_in_transaction(fn, retries=3)은 MySQL의 교착 상태·잠금 대기 시간 초과, PostgreSQL의 직렬화 실패·교착 상태·잠금 불가 오류, SQLite의 데이터베이스 잠금 오류가 발생하면 지수형 대기 후 함수를 다시 실행한다. 함수가 여러 번 실행될 수 있으므로 데이터베이스 밖의 부수 효과는 함수 안에 두지 않아야 한다.

마이그레이션과 명령줄 도구

모델을 바꾸면 doveorm generate가 변경 내용을 담은 파이썬 마이그레이션 파일을 만들고, doveorm apply가 이를 적용한다. 파일은 프로젝트에 저장해 버전 관리한다. 적용 기록은 doveorm_migrations 테이블에 남는다. 이전 버전에서 쓰던 migrations 테이블은 첫 마이그레이션 적용 때 자동으로 이름이 바뀐다.

프로젝트 루트에서 doveorm init -m myapp.models -s myapp.settings:DOVEORM을 한 번 실행하면 doveorm.toml과 마이그레이션 폴더를 만든다. doveorm init --pyproject를 쓰면 같은 설정을 pyproject.toml의 [tool.doveorm]에 저장한다. 설정 파일에는 모델 모듈, 마이그레이션 폴더, 데이터베이스 설정 위치를 지정한다. settings는 설정 딕셔너리를 내놓는 모듈 속성, 모듈 또는 설정 딕셔너리를 반환하거나 doveorm.configure()를 호출하는 함수일 수 있다. 설정을 생략하면 DOVEORM_* 환경 변수를 사용한다.

주요 흐름은 doveorm generate로 변경을 기록하고 doveorm apply로 적용하는 것이다. doveorm history는 적용·대기 상태를 보여주고, doveorm sql 0002는 실행 없이 SQL을 출력한다. doveorm apply 0001은 지정한 마이그레이션으로 이동하고, doveorm revert 2는 마지막 두 개를 되돌린다. doveorm revert --all은 전부 되돌리며 --dry-run은 실행할 내용을 보여준다. doveorm verify는 모델 변경에 마이그레이션이 빠졌으면 CI에서 종료 코드 1을 반환한다. doveorm squash 0007은 이전 마이그레이션을 최적화해 합친다. 마이그레이션 대상은 전체 이름 대신 고유한 접두어로 지정할 수 있다.

doveorm status는 연결·서버 버전·데이터베이스 크기·마이그레이션 상태를 보여준다. doveorm doctor는 연결, 마이그레이션, 스키마, 데이터베이스별 무결성 항목을 검사하며 실패하면 종료 코드 1을 반환한다. doveorm models는 모델과 테이블 정보를, doveorm console은 모델이 불러와진 파이썬 셸을 제공한다. doveorm connect는 설정된 자격 증명으로 sqlite3, mysql 또는 psql 클라이언트를 연다. status, doctor, models, verify는 읽기 전용이며 데이터를 바꾸지 않는다. 다만 SQLite의 전체 무결성·외래 키 검사는 큰 파일에서 오래 걸릴 수 있다.

마이그레이션의 안전 조건

마이그레이션 파일은 일반 파이썬 파일이라 검토·수정할 수 있다. 변경 탐지기는 마이그레이션 작업을 메모리에서 재생해 모델과 비교하며 데이터베이스 연결이나 별도 스냅샷 파일을 요구하지 않는다. 데이터베이스 구조에 영향을 주지 않는 help_text, choices, sensitive, verbose_name, upload_to 변경은 마이그레이션을 만들지 않는다. 이름 변경은 자동으로 추측하지 않는다. 대화형 확인이나 --rename User.handle=nick 같은 명시적 지정이 필요하다. 기존 행이 있는 테이블에 기본값 없는 NOT NULL 열을 추가하거나 열을 NOT NULL로 바꿀 때는 채울 값을 지정해야 한다. 생성된 변경 목록은 모델 구조를 정확히 재현하는지 검증한다.

SQLite와 PostgreSQL에서는 각 마이그레이션과 기록을 함께 커밋하며 실패하면 롤백한다. SQLite에서 직접 변경할 수 없는 구조는 외래 키 검사를 일시 중지한 복사·교체 방식으로 바꾸고 커밋 전에 PRAGMA foreign_key_check를 실행한다. PostgreSQL의 DDL은 트랜잭션 안에서 처리한다. MySQL의 DDL은 암묵적으로 커밋되므로 마이그레이션이 원자적이지 않다. 따라서 MySQL에서는 논리적으로 하나의 변경만 각 마이그레이션에 담는 것이 권장된다. 되돌리는 코드가 없는 RunSQL·RunPython 마이그레이션은 실행 전에 롤백을 거부한다. 동시에 배포할 때 MySQL은 이름 있는 잠금, PostgreSQL은 어드바이저리 잠금을 사용한다.

모델 자동 탐색은 models.py 파일과 models/ 패키지를 찾으며 숨김 폴더, 가상 환경, tests, migrations, build, dist는 건너뛴다. 탐색에서 모듈을 놓치면 삭제된 모델로 오인할 수 있으므로, 자동 탐색 모드의 generate는 --yes를 지정하지 않는 한 모델 삭제를 거부한다. 모델 모듈을 설정에 명시하면 이 보호가 해제된다. 여러 모듈·패키지·프로젝트 내부 글로브를 지정하거나 discover = true로 자동 탐색을 더할 수 있다. 설치된 패키지의 모델 진입점은 기본적으로 불러오지 않으며 entry_points = true로 명시해야 한다.

버전과 보안

0.8.0에는 migrate = False 지원이 추가됐다. 0.7.0에서는 여러 패키지의 모델, 글로브, 자동 탐색과 선택적 진입점 등록을 지원했고 on_delete 상수도 추가했다. 0.6.0에서는 모델 자동 탐색과 삭제 보호, 마이그레이션 이력 테이블 이름 변경이 도입됐다.

DoveORM은 바인딩 매개변수, 식별자 검증, 엄격한 MySQL SQL 모드, 입력 필드 허용 목록, 민감 정보 가림, 파일 경로 검증 등을 보안 기능으로 내세운다. SQLite 데이터베이스 파일은 새로 만들 때 권한을 0600으로 설정한다. 원격 MySQL·PostgreSQL 연결에서 TLS가 없으면 경고한다. 명령줄 도구는 지정하거나 탐색한 설정·모델 모듈을 가져오므로 신뢰하는 프로젝트에서만 실행해야 한다. status와 doctor는 비밀번호를 출력하지 않으며 connect는 비밀번호를 명령줄 인수에 노출하지 않고 권한 0600인 임시 설정 파일을 사용한 뒤 삭제한다.

테스트는 pip install -e . pytest와 python3 -m pytest tests/로 실행한다. 테스트 모음은 SQLite 전체 흐름과 MySQL 드라이버의 가짜 연결을 다루므로 MySQL 서버가 없어도 일부 검사를 할 수 있다. PostgreSQL 통합 테스트는 테스트용 스키마를 만들 수 있는 서버가 필요하다. 전체 예제 프로젝트는 example/에 있다. 라이선스는 MIT다.

부록: 원문 예시

원문에 실린 예시 1

text
DecimalField(max_digits=10, decimal_places=2)

원문에 실린 예시 2

text
ImageField(upload_to="avatars/", width_field=…, height_field=…)

원문에 실린 예시 3

text
FilePathField(path="/etc/app", match=r"\.conf$", recursive=False)

원문에 실린 예시 4

text
CreateModel(name, fields, table=, options=)

원문에 실린 예시 5

text
AlterTableOptions(name, engine=, charset=)

원문에 실린 예시 6

text
RunSQL(sql, reverse_sql=None, state_operations=())