테이블 생성 (Create Table)

테이블 생성 (Create Table)

이제 본격적으로 코드를 작성해 볼게요. 👩‍💻

먼저 프로젝트 디렉터리 안에 있는지 확인하세요.

이번 장에서 우리가 할 일은 이렇습니다:

  • SQLModel로 테이블 정의하기
  • SQLModel로 동일한 SQLite 데이터베이스와 테이블 만들기
  • DB Browser for SQLite로 그 작업 결과 확인하기

우리가 만들려는 테이블 구조를 다시 한번 떠올려 볼게요:

id name secret_name age
1 Deadpond Dive Wilson null
2 Spider-Boy Pedro Parqueador null
3 Rusty-Man Tommy Sharp 48

테이블 모델 클래스 만들기 (Create the Table Model Class)

가장 먼저 해야 할 일은 테이블의 데이터를 표현하는 클래스를 만드는 거예요.

이렇게 어떤 데이터를 표현하는 클래스를 보통 모델(model) 이라고 부릅니다.

그래서 이 패키지 이름이 SQLModel인 거예요. 주로 SQL 모델을 만드는 데 쓰이니까요.

그러기 위해 SQLModel을 import하고(곧 쓸 다른 것들도 함께), SQLModel을 상속받아 히어로들의 테이블 모델을 표현하는 Hero 클래스를 만들어요:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None

Hero 클래스는 히어로들을 위한 테이블을 표현해요. 그리고 나중에 우리가 만드는 인스턴스 하나하나는 그 테이블 안의 행(row) 하나를 표현하게 됩니다.

여기서 table=True 설정을 써서 SQLModel에게 이건 테이블을 표현하는 테이블 모델이야라고 알려줘요.

참고

table=True 없이 만드는 모델도 가능해요. 그런 모델은 데이터베이스에 테이블이 없는, 단지 데이터 모델일 뿐이고 테이블 모델은 아니에요.

그런 데이터 모델들은 나중에 아주 유용해질 거예요. 다만 지금은 계속 table=True 설정을 붙여 나갈게요.

필드(컬럼) 정의하기 (Define the Fields, Columns)

다음 단계는 표준 파이썬 타입 어노테이션을 이용해서 클래스의 필드(또는 컬럼)를 정의하는 일이에요.

이 변수들 각각의 이름이 테이블의 컬럼 이름이 되고, 각각의 타입이 테이블 컬럼의 타입이 됩니다:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None

이 필드/컬럼 선언들을 좀 더 자세히 들여다볼게요.

None 필드, NULL 허용 컬럼 (None Fields, Nullable Columns)

age부터 시작할게요. 타입이 int | None이라는 점에 주목하세요.

이것은 파이썬에서 "int이거나 None일 수 있다"를 선언하는 표준적인 방법이에요.

그리고 age의 기본값도 None으로 정했죠.

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None

idint | None으로 정의하지만, 그것은 아래에서 다시 다룰게요.

타입이 int | None이기 때문에:

  • 데이터를 검증할 때 age 값으로 None이 허용돼요.
  • 데이터베이스에서 age 컬럼은 NULL(파이썬의 None에 해당하는 SQL 값)을 허용하게 돼요.

그리고 기본값이 = None이기 때문에:

  • 데이터를 검증할 때 age 필드는 필수가 아니고 기본적으로 None이 돼요.
  • 데이터베이스에 저장할 때 age 컬럼은 기본적으로 NULL 값을 가지게 돼요.

기본값은 다른 값이어도 됐어요, 예를 들어 = 42처럼요.

기본 키 id (Primary Key id)

이제 id 필드를 살펴볼게요. 이것이 테이블의 기본 키(primary key) 입니다.

그래서 id기본 키로 표시해 줘야 해요.

그렇게 하려면 sqlmodel의 특별한 Field 함수를 사용해서 인자 primary_key=True를 설정하면 됩니다:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None

이렇게 하면 SQLModel에게 이 id 필드/컬럼이 테이블의 기본 키라고 알려주는 거예요.

그런데 SQL 데이터베이스 안에서는 id항상 필수이고 NULL이 될 수 없어요. 그런데 왜 int | None으로 선언하는 걸까요?

id는 데이터베이스에서 필수지만, 우리 코드가 아니라 데이터베이스가 생성할 값이에요.

그래서 이 클래스의 인스턴스를 만들 때(다음 장들에서) 우리는 id를 설정하지 않을 거예요. 그리고 id의 값은 데이터베이스에 저장하기 전까지는 None이고, 저장한 뒤에야 비로소 값을 갖게 됩니다.

my_hero = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")

do_something(my_hero.id)  # Oh no! my_hero.id is None! 😱🚨

# Imagine this saves it to the database
somehow_save_in_db(my_hero)

do_something(my_hero.id)  # Now my_hero.id has a value generated in DB 🎉

그래서 우리 코드 안에서는(데이터베이스가 아니라) id의 값이 None수도 있기 때문에 int | None을 쓰는 거예요. 이렇게 하면 편집기가 우리를 도와줄 수 있어요. 예를 들어 아직 데이터베이스에 저장하지 않아서 여전히 None일 객체의 id에 접근하려 할 때 에러를 잡아주죠.

이제 기본값을 우리의 Field() 함수로 대체하고 있으니, Field()의 인자 default=None으로 id실제 기본값None으로 설정해요:

Field(default=None)

만약 default 값을 설정하지 않으면, 나중에 이 모델로 데이터 검증(Pydantic 기반)을 할 때 int 외에 None 값도 받아들이기는 하지만 그 None 값을 여전히 넘겨줘야 요구하게 돼요. 그리고 나중에 이 모델을 쓰는 사람(아마 우리 자신)에게 혼란스러울 테니까, 여기서 기본값을 정해 두는 게 낫습니다.

엔진 생성하기 (Create the Engine)

이제 SQLAlchemy 엔진(Engine) 을 만들어야 해요.

엔진은 데이터베이스와의 통신을 처리하는 객체입니다.

서버 데이터베이스(예: PostgreSQL이나 MySQL)를 쓴다면, 엔진이 그 데이터베이스로의 네트워크 연결을 관리해요.

엔진을 만드는 건 아주 간단해요. 사용할 데이터베이스의 URL로 create_engine()을 호출하면 됩니다:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)

SQLModel.metadata.create_all(engine)

보통 엔진 객체는 애플리케이션 전체에서 단 하나만 만들어 두고 어디서든 재사용해야 합니다.

**세션(Session)**이라는 연관된 것도 있는데, 이것은 보통 애플리케이션마다 하나의 객체로 두면 안 되는 것입니다.

하지만 그것은 나중에 다룰게요.

엔진 데이터베이스 URL (Engine Database URL)

지원하는 데이터베이스마다 각자의 URL 형식이 있어요. 예를 들어 SQLitesqlite:/// 다음에 파일 경로를 붙입니다. 예를 들면:

  • sqlite:///database.db
  • sqlite:///databases/local/application.db
  • sqlite:///db.sqlite

SQLite는 전부 메모리 안에 존재하는 특별한 데이터베이스를 지원해요. 그래서 아주 빠르지만, 프로그램이 종료되면 데이터베이스가 삭제되니 주의해야 해요. 이 인메모리 데이터베이스는 파일 이름 없이 슬래시 두 개(//)만 쓰면 지정할 수 있습니다:

  • sqlite://

SQLAlchemy가 지원하는(그래서 SQLModel도 지원하는) 모든 데이터베이스에 대해 더 자세히 알고 싶다면 SQLAlchemy 문서를 참고하세요.

엔진의 echo (Engine Echo)

이 예제에서는 인자 echo=True도 사용하고 있어요.

이것은 엔진이 실행하는 모든 SQL 문장을 출력하게 해서, 무슨 일이 일어나고 있는지 이해하는 데 도움을 줍니다.

특히 학습디버깅에 유용해요:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)

SQLModel.metadata.create_all(engine)

하지만 운영(production)에서는 아마 echo=True를 제거하고 싶을 거예요:

engine = create_engine(sqlite_url)

엔진 기술적 세부사항 (Engine Technical Details)

SQLAlchemy를 몰랐고 지금 막 SQLModel을 배우고 있다면, 이 절은 건너뛰고 아래로 스크롤해도 좋아요.

엔진에 대해 더 자세히 알고 싶다면 SQLAlchemy 문서를 참고하세요.

SQLModel은 자체 create_engine() 함수를 정의해요. SQLAlchemy의 create_engine()과 같지만, 기본적으로 future=True를 사용한다는 차이가 있어요(즉, 최신 SQLAlchemy인 1.4 스타일과 미래의 2.0 스타일을 쓴다는 뜻).

그리고 SQLModel 버전의 create_engine()은 내부적으로 타입 어노테이션이 되어 있어서, 편집기가 자동완성과 인라인 에러로 도와줄 수 있어요.

데이터베이스와 테이블 만들기 (Create the Database and Table)

이제 마침내 데이터베이스와 테이블을 만들 준비가 다 됐어요:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)

SQLModel.metadata.create_all(engine)

엔진을 만드는 것만으로는 database.db 파일이 생기지 않아요.

하지만 SQLModel.metadata.create_all(engine)을 실행하면 database.db 파일이 생기고 그 데이터베이스 안에 hero 테이블도 만들어집니다.

두 작업이 이 한 단계에서 모두 이뤄져요.

이걸 풀어서 살펴볼게요:

SQLModel.metadata.create_all(engine)

SQLModel의 MetaData (SQLModel MetaData)

SQLModel 클래스에는 metadata 속성이 있어요. 이것은 MetaData 클래스의 인스턴스입니다.

SQLModel을 상속받는 클래스를 만들 때, 그리고 table = True로 설정했을 때마다, 그 클래스가 이 metadata 속성에 등록됩니다.

그래서 마지막 줄이 실행되는 시점에는 이미 SQLModel.metadataHero가 등록되어 있어요.

create_all() 호출하기 (Calling create_all())

SQLModel.metadata에 있는 이 MetaData 객체는 create_all() 메서드를 가져요.

이 메서드는 엔진을 받아서 그 엔진으로 데이터베이스와 이 MetaData 객체에 등록된 모든 테이블을 만듭니다.

SQLModel MetaData의 순서가 중요하다 (SQLModel MetaData Order Matters)

이 말은 곧, SQLModel.metadata.create_all()SQLModel을 상속받는 새로운 모델 클래스를 만드는 코드 다음에 호출해야 한다는 뜻이에요.

예를 들어 이런 상황을 상상해 보세요:

  • 한 파이썬 파일 models.py에서 모델들을 만든다.
  • db.py 파일에서 엔진 객체를 만든다.
  • app.py에서 메인 앱을 만들고 SQLModel.metadata.create_all()을 호출한다.

만약 app.py에서 SQLModel만 import하고 SQLModel.metadata.create_all()을 호출한다면, 테이블이 만들어지지 않아요:

# This wouldn't work! 🚨
from sqlmodel import SQLModel

from .db import engine

SQLModel.metadata.create_all(engine)

이것이 동작하지 않는 이유는, SQLModel만 import하면 파이썬이 그것을 상속받는 클래스(우리 예제의 Hero 클래스)를 만드는 코드를 실행하지 않기 때문이에요. 그래서 SQLModel.metadata는 여전히 비어 있죠.

하지만 SQLModel.metadata.create_all()을 호출하기 전에 모델들을 import하면 동작합니다:

from sqlmodel import SQLModel

from . import models
from .db import engine

SQLModel.metadata.create_all(engine)

이것이 동작하는 이유는, 모델들을 import하면서 파이썬이 SQLModel을 상속받아 SQLModel.metadata에 등록하는 클래스들을 만드는 코드를 전부 실행하기 때문이에요.

대안으로, db.py 안에서 SQLModel과 모델들을 import해도 됩니다:

# db.py
from sqlmodel import SQLModel, create_engine
from . import models


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url)

그리고 app.py에서 db.py 로부터 SQLModel을 import한 뒤 그곳에서 SQLModel.metadata.create_all()을 호출하면 됩니다:

# app.py
from .db import engine, SQLModel

SQLModel.metadata.create_all(engine)

db.py에서 SQLModel을 import해 오는 것이 동작하는 이유는 SQLModeldb.py에서도 import되어 있기 때문이에요.

그리고 이 방법이 올바르게 동작해서 데이터베이스에 테이블을 만드는 이유는, db.py에서 SQLModel을 import하면서 파이썬이 그 db.py 파일 안에서 SQLModel을 상속받는 클래스(예: Hero)를 만드는 코드를 전부 실행하기 때문이에요.

마이그레이션 (Migrations)

이 간단한 예제와 Tutorial - User Guide의 대부분에서는 SQLModel.metadata.create_all()로 충분해요.

하지만 운영 시스템을 만든다면 데이터베이스를 마이그레이션하는 시스템을 쓰는 게 좋을 거예요.

예를 들어 컬럼을 추가하거나 제거할 때, 새 테이블을 추가할 때, 타입을 바꿀 때 등에 이것이 유용하고 중요하죠.

마이그레이션에 대해서는 나중에 Advanced User Guide에서 배우게 될 거예요.

프로그램 실행하기 (Run The Program)

프로그램을 실행해서 모두 동작하는지 확인해 볼게요.

아직 코드를 만들지 않았다면 app.py 파일에 코드를 넣으세요.

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)

SQLModel.metadata.create_all(engine)

프로젝트 환경 안에서 uv run으로 실행하세요.

이제 파이썬으로 프로그램을 실행해 보세요:

// We set echo=True, so this will show the SQL code
$ uv run python app.py

// First, some boilerplate SQL that we are not that interested in

INFO Engine BEGIN (implicit)
INFO Engine PRAGMA main.table_info("hero")
INFO Engine [raw sql] ()
INFO Engine PRAGMA temp.table_info("hero")
INFO Engine [raw sql] ()
INFO Engine

// Finally, the glorious SQL to create the table ✨

CREATE TABLE hero (
        id INTEGER,
        name VARCHAR NOT NULL,
        secret_name VARCHAR NOT NULL,
        age INTEGER,
        PRIMARY KEY (id)
)

// More SQL boilerplate

참고

위 출력을 읽기 쉽게 약간 단순화했어요.

실제로는 아래처럼 표시된다기보다는:

INFO Engine BEGIN (implicit)

이런 모습으로 표시됩니다:

2021-07-25 21:37:39,175 INFO sqlalchemy.engine.Engine BEGIN (implicit)

TEXT 또는 VARCHAR (TEXT or VARCHAR)

이전 장의 예제에서는 일부 컬럼을 만들 때 TEXT를 사용했어요.

하지만 이번 출력에서 SQLAlchemy는 VARCHAR를 사용하죠. 무슨 일이 벌어지고 있는지 살펴볼게요.

[각각의 SQL 데이터베이스마다 지원하는 것에 차이가 있다는 것]을 기억하시나요?

그 차이 중 하나가 바로 이것이에요. 각 데이터베이스는 INTEGERTEXT 같은 특정한 데이터 타입들을 지원해요.

어떤 데이터베이스들은 특정 용도에 특화된 고유한 타입을 가지기도 해요. 예를 들어 PostgreSQL과 MySQL은 TrueFalse 값을 위한 BOOLEAN을 지원해요. SQLite는 테이블 컬럼을 정의할 때도 불리언이 있는 SQL을 받아들이지만, 실제 내부적으로는 INTEGER를 사용해서 True1로, False0으로 표현해요.

마찬가지로 문자열을 저장하는 타입도 여러 가지가 가능해요. SQLite는 TEXT 타입을 사용해요. 하지만 PostgreSQL과 MySQL 같은 다른 데이터베이스들은 기본적으로 VARCHAR 타입을 쓰고, VARCHAR는 가장 흔한 데이터 타입 중 하나이기도 해요.

**VARCHAR**는 variable(가변) length character(길이 문자)에서 왔어요.

SQLAlchemy는 VARCHAR를 사용해서 테이블을 만드는 SQL 문장을 생성하고, SQLite는 그것을 받아서 내부적으로 TEXT로 변환합니다.

이 두 데이터 타입의 차이에 더해, MySQL 같은 일부 데이터베이스는 VARCHAR 타입에 최대 길이를 설정해야 해요. 예를 들어 VARCHAR(255)는 최대 문자 수를 255로 설정하죠.

어떤 데이터베이스를 쓰든(MySQL이어도) 추가 설정 없이 바로 SQLModel을 쓰기 쉽게 만들기 위해, 기본적으로 str 필드는 대부분의 데이터베이스에서 VARCHAR로, MySQL에서는 VARCHAR(255)로 해석됩니다. 이렇게 하면 같은 클래스가 추가 노력 없이 가장 널리 쓰이는 데이터베이스들과 호환된다는 걸 알 수 있어요.

문자열 컬럼의 최대 길이를 바꾸는 방법은 나중에 Advanced Tutorial - User Guide에서 배우게 될 거예요.

데이터베이스 확인하기 (Verify the Database)

이제 DB Browser for SQLite로 데이터베이스를 열어 보면, 프로그램이 이전과 똑같이 hero 테이블을 만든 것을 볼 수 있을 거예요. 🎉

데이터 생성 리팩터링 (Refactor Data Creation)

이제 나중에 재사용, 공유, 테스트하기 쉽도록 코드를 조금 재구성해 볼게요.

데이터를 바꾸는(데이터베이스와 테이블이 있는 파일을 만드는) 주요 부수 효과(side effect) 코드를 함수로 옮겨 볼게요.

이 예제에서는 그것이 바로 SQLModel.metadata.create_all(engine) 한 줄이에요.

그것을 create_db_and_tables() 함수 안에 넣어 볼게요:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)

만약 SQLModel.metadata.create_all(engine)이 함수 안에 있지 않고 이 모듈(이 파일)에서 다른 데로 뭔가를 import하려 한다면, 그 모듈을 import한 다른 파일을 실행할 때 매번 데이터베이스와 테이블을 만들려고 시도하게 돼요.

우리는 그렇게 되길 원하지 않아요. 우리가 의도한 때에만 그렇게 되길 원하죠. 그래서 함수 안에 넣는 거예요. 테이블이 그 함수를 호출할 때만 만들어지고, 이 모듈이 다른 곳에서 import될 때는 만들어지지 않도록 보장할 수 있기 때문이에요.

이제 예를 들어 다른 파일에서 Hero 클래스를 import해도 그런 부수 효과 없이 쓸 수 있게 됐어요.

😅 스포일러 주의: 함수 이름이 create_db_and_tables()인 이유는 Hero 외에 다른 클래스로 테이블이 더 생길 것이기 때문이에요. 🚀

스크립트로 데이터 생성하기 (Create Data as a Script)

app.py 파일에서 뭔가를 import할 때 부수 효과가 발생하지 않도록 막았어요.

하지만 터미널에서 독립적인 스크립트로 파이썬으로 직접 호출할 때는 여전히 데이터베이스와 테이블을 만들고 싶어요. 아까처럼요.

스크립트(script)프로그램(program) 이란 단어는 서로 바꿔 쓸 수 있다고 생각하세요.

스크립트라는 단어는 종종 그 코드를 독립적으로 쉽게 실행할 수 있음을 뜻해요. 또는 비교적 간단한 프로그램을 가리키기도 하죠.

그러기 위해 if 블록에서 특별한 변수 __name__을 사용할 수 있어요:

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: int | None = None


sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
    create_db_and_tables()

__name__ == "__main__"에 대하여 (About __name__ == "__main__")

__name__ == "__main__"의 주된 목적은, 파일이 이렇게 직접 호출될 때 실행되는 코드를 두는 것입니다:

$ uv run python app.py

// Something happens here ✨

...하지만 다른 파일이 이렇게 import할 때는 실행되지 않아요:

from app import Hero

if __name__ == "__main__":을 쓰는 그 if 블록을 "main 블록"이라고 부르기도 해요.

공식 명칭(파이썬 문서에 있는)은 "최상위 스크립트 환경(Top-level script environment)"입니다.

더 자세히 (More details)

파일 이름이 myapp.py라고 해 볼게요.

이렇게 실행하면:

$ uv run python myapp.py

// This will call create_db_and_tables()

...파이썬이 자동으로 만들어 주는 파일 안의 내부 변수 __name__의 값이 문자열 "__main__"이 됩니다.

그래서 아래의 함수가:

if __name__ == "__main__":
    create_db_and_tables()

...실행됩니다.


하지만 그 모듈(파일)을 import한다면 이렇게 되지 않아요.

그래서 importer.py라는 다른 파일이 이렇게 있다면:

from myapp import Hero

# Some more code

...그런 경우 myapp.py 안의 자동 변수 __name__"__main__" 값을 갖지 않아요.

그래서 아래의 줄은:

if __name__ == "__main__":
    create_db_and_tables()

...실행되지 않습니다.

참고

더 자세한 내용은 파이썬 공식 문서를 확인하세요.

마지막 점검 (Last Review)

이런 변경을 거친 뒤에도 다시 실행하면 이전과 같은 출력이 나올 거예요.

하지만 이제 이 모듈에서 다른 파일로 뭔가를 import할 수 있게 됐죠.

이제 코드를 마지막으로 한번 볼게요:

from sqlmodel import Field, SQLModel, create_engine  # (2)!


class Hero(SQLModel, table=True):  # (3)!
    id: int | None = Field(default=None, primary_key=True)  # (4)!
    name: str  # (5)!
    secret_name: str  # (6)!
    age: int | None = None  # (7)!


sqlite_file_name = "database.db"  # (8)!
sqlite_url = f"sqlite:///{sqlite_file_name}"  # (9)!

engine = create_engine(sqlite_url, echo=True)  # (10)!


def create_db_and_tables():  # (11)!
    SQLModel.metadata.create_all(engine)  # (12)!


if __name__ == "__main__":  # (13)!
    create_db_and_tables()  # (14)!
  1. None이 될 수 있는 필드를 선언하려고 typing에서 Optional을 import한다.
  2. sqlmodel에서 필요한 것들(Field, SQLModel, create_engine)을 import한다.
  3. 데이터베이스의 hero 테이블을 표현하는 Hero 모델 클래스를 만든다.

그리고 table=True로 이 클래스를 테이블 모델로 표시한다. 4. id 필드를 만든다:

데이터베이스가 값을 할당하기 전까지는 None일 수 있으므로 Optional(int | None, 파이썬 3.10+)로 어노테이션한다.

기본 키이므로 Field()와 인자 primary_key=True를 사용한다. 5. name 필드를 만든다.

필수이므로 기본값이 없고 Optional도 아니다. 6. secret_name 필드를 만든다.

역시 필수. 7. age 필드를 만든다.

필수가 아니고 기본값은 None이다.

데이터베이스에서 기본값은 None에 해당하는 SQL 값인 NULL이 된다.

이 필드는 None(그리고 데이터베이스에서 NULL)일 수 있으므로 Optional(int | None, 파이썬 3.10+)로 어노테이션한다. 8. 데이터베이스 파일의 이름을 적는다. 9. 데이터베이스 파일의 이름으로 데이터베이스 URL을 만든다. 10. URL로 엔진을 만든다.

이 시점에는 아직 데이터베이스가 만들어지지 않는다. 파일이나 테이블은 만들어지지 않고, 오직 이 특정 데이터베이스와의 연결을 처리하고(URL에 기반해) SQLite에 대한 특정 지원을 갖는 엔진 객체만 만들어진다. 11. 부수 효과를 만드는 코드를 함수에 넣는다.

이 경우에는 테이블이 있는 데이터베이스 파일을 만드는 한 줄뿐이다. 12. SQLModel.metadata에 자동으로 등록된 모든 테이블을 만든다. 13. main 블록, 즉 "최상위 스크립트 환경"을 추가한다.

그리고 이 파일이 파이썬으로 직접 호출될 때 실행될 로직을 넣는다. 예:

$ python app.py

// Execute all the stuff and show the output

...하지만 이 모듈에서 뭔가를 import할 때는 실행되지 않는다. 예:

from app import Hero
  1. 이 main 블록에서 데이터베이스 파일과 테이블을 만드는 함수를 호출한다.

이렇게 하면 이렇게 호출할 때:

$ python app.py

// Doing stuff ✨

...데이터베이스 파일과 테이블이 만들어진다.

코드 안의 번호 풍선을 클릭해서 각 줄이 무슨 일을 하는지 다시 확인해 보세요. 👆

요약 (Recap)

우리는 SQLModel을 사용해서 데이터베이스의 테이블이 어떻게 생겨야 하는지 정의하는 법을 배웠고, SQLModel로 데이터베이스와 테이블을 만들었어요.

또한 나중에 재사용, 공유, 테스트하기 쉽도록 코드를 리팩터링했죠.

다음 장들에서는 SQLModel이 어떻게 코드에서 SQL 데이터베이스와 상호작용하는 데 도움을 주는지 보게 될 거예요. 🤓