쿼리·조회 (Select Queries)
쿼리·조회 (Select Queries)
벌써 데이터베이스에 표 하나가 만들어져 있고, 그 안에 대략 이런 데이터가 들어 있어요.
| id | name | secret_name | age |
|---|---|---|---|
| 1 | Deadpond | Dive Wilson | null |
| 2 | Spider-Boy | Pedro Parqueador | null |
| 3 | Rusty-Man | Tommy Sharp | 48 |
점점 재미있어지고 있죠? 이제 데이터베이스에서 데이터를 읽어 오는 방법을 살펴볼게요! 🤩
이전 코드에서 이어가기
마지막에 데이터를 만들 때 썼던 코드부터 이어가 볼게요.
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, 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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def main():
create_db_and_tables()
create_heroes()
if __name__ == "__main__":
main()
SQLModel의 Hero 클래스 모델을 만들고 레코드 몇 개를 생성하고 있죠.
이번에는 Hero 모델과 engine이 필요할 거예요. 다만 데이터를 조회할 때는 새 session을 새 함수 안에서 만들어 사용할 겁니다.
SQL로 데이터 읽기
Python 코드를 쓰기 전에, SQL에서 데이터를 조회하는 게 어떻게 생겼는지 잠깐 복습해 볼게요.
SELECT id, name, secret_name, age FROM hero
이건 대략 이런 뜻이에요.
안녕, SQL 데이터베이스 👋, 나를 위해 데이터 좀
SELECT해 줘.먼저 내가 원하는 컬럼을 알려줄게:
idnamesecret_nameage그리고 그 값들을"hero"라는 이름의 테이블에서FROM으로 가져와 줘.
그러면 데이터베이스가 데이터를 가져와서 이런 표 형태로 돌려줘요.
| id | name | secret_name | age |
|---|---|---|---|
| 1 | Deadpond | Dive Wilson | null |
| 2 | Spider-Boy | Pedro Parqueador | null |
| 3 | Rusty-Man | Tommy Sharp | 48 |
이건 DB Browser for SQLite에서 직접 실행해 볼 수 있어요.

경고
지금은 모든 행을 다 가져오고 있어요.
행이 수천 개라면 데이터베이스 입장에서 계산 비용이 꽤 들 수 있어요.
보통은 원하는 행만 남기도록 필터링하게 될 텐데, 그건 다음 장에서 배울게요.
SQL 단축 표기
위 경우처럼 모든 컬럼을 가져오고 싶다면, SQL에는 단축 표기가 있어요. 컬럼 이름을 하나하나 나열하는 대신 *를 쓰면 돼요.
SELECT * FROM hero
결과는 똑같아요. 다만 SQLModel에서는 이 방식을 쓰지 않을 거예요.
컬럼을 줄여서 SELECT
컬럼을 더 적게 선택할 수도 있어요. 예를 들면:
SELECT id, name FROM hero
여기서는 id와 name 컬럼만 선택하고 있어요.
이렇게 하면 이런 표가 결과로 나와요.
| id | name |
|---|---|
| 1 | Deadpond |
| 2 | Spider-Boy |
| 3 | Rusty-Man |
여기서 재미있는 점 하나를 짚어 볼게요. SQL 데이터베이스는 데이터를 표(tables)에 저장해요. 그리고 쿼리 결과도 항상 **표(tables)**로 전달해 줘요.
SELECT의 변형
SQL 언어는 여러 곳에서 **변형(variations)**을 허용해요.
그중 하나가 SELECT 문에서 컬럼 이름을 그대로 쓰거나, 테이블 이름과 점(dot)을 붙여서 쓸 수 있다는 점이에요.
예를 들어 위 코드를 이렇게 쓸 수도 있어요.
SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero
이건 나중에 여러 테이블을 동시에 다룰 때 특히 중요해질 거예요. 서로 다른 테이블이 같은 이름의 컬럼을 가질 수 있거든요.
예를 들어 hero.id와 team.id, 또는 hero.name과 team.name처럼 말이죠.
또 하나의 변형은 SELECT 같은 SQL 키워드 대부분을 소문자로도 쓸 수 있다는 점이에요. select처럼 말이죠.
결과 표는 미리 존재하지 않아도 돼요
여기가 재미있는 부분인데, SQL 데이터베이스가 돌려주는 표들은 데이터베이스에 독립된 표로 존재할 필요가 없어요. 🧙
예를 들어 우리 데이터베이스에는 전체 컬럼(id, name, secret_name, age)을 가진 표가 하나뿐이에요. 그런데 지금은 컬럼이 더 적은 결과 표를 얻고 있죠.
SQL의 핵심 포인트 중 하나가 바로 이거예요. 데이터를 중복 없이 여러 표에 구조화해 두고, 다양한 방식으로 쿼리해서 그때그때 다른 결과 표를 얻는 거예요.
SQLModel로 데이터 읽기
이제 모든 hero를 읽는 같은 쿼리를 SQLModel로 해볼게요.
Session 만들기
첫 단계는 Session을 만드는 거예요. 행을 만들 때 했던 것과 같은 방식이에요.
새 함수 select_heroes()에서 그렇게 시작할게요.
# 위 코드는 생략 👆
def select_heroes():
with Session(engine) as session:
# 아래 코드는 생략 👇
pass
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, SQLModel, create_engine, select
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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
for hero in results:
print(hero)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
select 문 만들기
다음으로, 위에서 SQL SELECT 문을 썼던 것과 거의 같은 방식으로 이제는 SQLModel의 select 문을 만들 거예요.
먼저 파일 맨 위에서 sqlmodel로부터 select를 import해야 해요.
from sqlmodel import Field, Session, SQLModel, create_engine, select
# 아래 코드는 생략 👇
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, SQLModel, create_engine, select
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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
for hero in results:
print(hero)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
그리고 나서 Python 코드에서 SELECT 문을 만들 때 이걸 사용할 거예요.
from sqlmodel import Field, Session, SQLModel, create_engine, select
# 여기 코드는 생략 👈
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
# 아래 코드는 생략 👇
pass
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, SQLModel, create_engine, select
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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
for hero in results:
print(hero)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
짧은 코드 한 줄이지만 담고 있는 정보는 꽤 많아요.
statement = select(Hero)
이건 위에서 봤던 첫 SQL SELECT 문과 같은 뜻이에요.
SELECT id, name, secret_name, age FROM hero
select() 함수에 클래스 모델 Hero를 넘겨줘요. 그러면 Hero 클래스에 필요한 모든 컬럼을 선택하라는 뜻이 됩니다.
눈여겨볼 점은 select() 함수에서 FROM 부분을 명시적으로 지정하지 않는다는 거예요. Hero 클래스 모델에 연결된 표가 hero라는 건 SQLModel(정확히는 SQLAlchemy) 입장에서 이미 자명하기 때문이에요.
팁
select()가 돌려주는statement값은 특별한 객체라서 다른 것들도 할 수 있어요.그건 다음 장에서 설명할게요.
문 실행하기
이제 select 문이 있으니 session으로 실행할 수 있어요.
# 위 코드는 생략 👆
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
# 아래 코드는 생략 👇
pass
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, SQLModel, create_engine, select
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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
for hero in results:
print(hero)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
이렇게 하면 session이 engine을 사용해서 그 SELECT 문을 데이터베이스에서 실행하고 결과를 가져오라고 시키는 거예요.
echo=True로 engine을 만들었기 때문에, 실행하는 SQL이 출력에 나타날 거예요.
이 session.exec(statement)는 이런 출력을 만들어 냅니다.
INFO Engine BEGIN (implicit)
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero
INFO Engine [no key 0.00032s] ()
위에서 SQL을 직접 썼을 때처럼, 데이터베이스는 모든 데이터가 담긴 표를 돌려줘요.
| id | name | secret_name | age |
|---|---|---|---|
| 1 | Deadpond | Dive Wilson | null |
| 2 | Spider-Boy | Pedro Parqueador | null |
| 3 | Rusty-Man | Tommy Sharp | 48 |
결과 순회하기
results 객체는 반복 가능한(iterable) 객체라서 각 행을 하나씩 훑을 수 있어요.
이제 for 루프에 넣어서 hero를 하나씩 출력할 수 있어요.
# 위 코드는 생략 👆
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
for hero in results:
print(hero)
# 아래 코드는 생략 👇
pass
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, SQLModel, create_engine, select
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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
for hero in results:
print(hero)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
이렇게 하면 다음과 같은 출력이 나옵니다.
id=1 name='Deadpond' age=None secret_name='Dive Wilson' id=2 name='Spider-Boy' age=None secret_name='Pedro Parqueador' id=3 name='Rusty-Man' age=48 secret_name='Tommy Sharp'
select_heroes()를 main()에 추가하기
이제 main() 함수 안에 select_heroes() 호출을 넣어서, 프로그램을 명령줄에서 실행했을 때 실행되도록 할게요.
# 위 코드는 생략 👆
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
for hero in results:
print(hero)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
# 아래 코드는 생략 👇
pass
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, SQLModel, create_engine, select
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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
for hero in results:
print(hero)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
코드 검토하기
좋아요, 이제 데이터베이스에서 데이터를 읽을 수 있게 됐네요! 🎉
지금까지의 코드를 검토해 볼게요.
from sqlmodel import Field, Session, SQLModel, create_engine, select # (1)!
class Hero(SQLModel, table=True): # (2)!
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) # (3)!
def create_db_and_tables():
SQLModel.metadata.create_all(engine) # (4)!
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (5)!
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session: # (6)!
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session: # (7)!
statement = select(Hero) # (8)!
results = session.exec(statement) # (9)!
for hero in results: # (10)!
print(hero) # (11)!
# (12)!
def main():
create_db_and_tables()
create_heroes()
select_heroes() # (13)!
if __name__ == "__main__":
main()
-
sqlmodel에서 사용할 모든 것, 새select()함수를 포함해서 import해요. -
hero표를 나타내는Hero클래스 모델을 만들어요. -
engine을 만들어요. 애플리케이션 코드 전체가 공유할 수 있는 engine 하나를 쓰는 게 좋은데, 여기서 그렇게 하고 있어요.
-
SQLModel.metadata에 등록된 모델들에 대한 테이블을 전부 만들어요. 이건 데이터베이스가 아직 없으면 함께 생성해 주기도 해요. -
Hero객체를 하나씩 만들어요. 이미 데이터를 데이터베이스에 만들어 뒀다면 이 부분은 없을 수도 있어요. -
새 session을 만들어 hero를 데이터베이스에
add하고commit으로 변경 사항을 확정해요. -
데이터를 조회할 새 session을 만들어요.
팁
이 session은 위 함수의 session과는 별개로 독립된 session이라는 점을 눈여겨보세요.
하지만 같은 engine을 사용해요. 애플리케이션 전체에서 여전히 engine은 하나예요.
-
select()함수로 모든Hero객체를 선택하는 statement를 만들어요. 이건hero표의 모든 행을 선택한다는 뜻이에요. -
session.exec(statement)로 session이 engine을 사용해 내부 SQL 문을 실행하게 해요. 데이터베이스에 가서 그 SQL을 실행하고 결과를 가져오지요. 그리고 특별한 iterable 객체를 돌려주는데, 그걸results변수에 담아요. 이건 이런 출력을 만들어 냅니다.INFO Engine BEGIN (implicit) INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero INFO Engine [no key 0.00032s] () -
results의 각Hero객체를 순회해요. -
각
hero를 출력해요.for루프의 3번의 반복은 이런 출력을 만들어 냅니다.id=1 name='Deadpond' age=None secret_name='Dive Wilson' id=2 name='Spider-Boy' age=None secret_name='Pedro Parqueador' id=3 name='Rusty-Man' age=48 secret_name='Tommy Sharp' -
이 시점,
with블록이 끝나면서 session은 닫혀요. 이건 이런 출력을 만들어 냅니다.INFO Engine ROLLBACK -
이 함수
select_heroes()를main()함수에 넣어서, 프로그램을 명령줄에서 실행했을 때 호출되게 해요.
팁
번호가 매겨진 bubble을 보면 각 코드 줄이 무엇을 하는지 알 수 있어요.
여기서부터 애플리케이션 전체에는 engine 하나를, 그리고 각 작업 그룹마다 다른 session을 써야 하는 이유가 좀 더 분명해지기 시작해요.
우리가 만든 이 새 session은 같은 engine을 사용하지만, 새롭고 독립적인 session이에요.
위에서 모델을 만든 코드는, 예를 들어 웹 API 요청을 처리하고 모델을 만드는 함수에 있을 수 있어요.
그리고 데이터베이스에서 데이터를 읽는 두 번째 부분은 다른 요청을 위한 다른 함수에 있을 수 있죠.
그래서 두 부분은 서로 다른 위치에 있을 수 있고, 각자 자기만의 session이 필요해요.
참고
솔직히 말하면 이 예시에서는 그 코드 전부가 같은 session을 공유할 수도 있어요. 여기서 굳이 두 개가 필요하진 않거든요.
다만 이렇게 해야 session들을 분리할 수 있다는 걸 보여줄 수 있고, 애플리케이션마다 engine 하나, 작업 그룹마다 session 하나라는 원칙을 되새길 수 있어요.
Hero 객체의 리스트 얻기
지금까지는 results를 순회해서 썼어요.
하지만 여러 이유로, iterable 대신 Hero 객체의 리스트 전체를 바로 갖고 싶을 수 있어요. 예를 들어 웹 API에서 리스트를 돌려주고 싶은 경우죠.
특별한 results 객체에는 모든 객체를 담은 리스트를 돌려주는 results.all() 메서드도 있어요.
# 위 코드는 생략 👆
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
heroes = results.all()
print(heroes)
# 아래 코드는 생략 👇
pass
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, SQLModel, create_engine, select
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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session:
statement = select(Hero)
results = session.exec(statement)
heroes = results.all()
print(heroes)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
이제 모든 hero가 heroes 변수의 리스트에 담겨 있어요.
출력해 보면 이런 모습을 볼 수 있어요.
[Hero(id=1, name='Deadpond', age=None, secret_name='Dive Wilson'), Hero(id=2, name='Spider-Boy', age=None, secret_name='Pedro Parqueador'), Hero(id=3, name='Rusty-Man', age=48, secret_name='Tommy Sharp')]
참고
실제로는 더 압축된 형태로 보일 거예요. 데이터 전부가 담긴 리스트라는 걸 알아보기 좋게 제가 조금 정리해서 보여드리는 거예요.
압축 버전
각각이 무엇을 하는지 설명하기 위해 그동안 변수를 여러 개 만들어 뒀어요.
하지만 각 객체가 무엇이고 무엇을 하는지 알게 됐으니, 이것을 조금 더 간단하고 압축된 형태로 만들 수 있어요.
# 위 코드는 생략 👆
def select_heroes():
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
print(heroes)
# 아래 코드는 생략 👇
pass
👀 전체 파일 미리 보기
from sqlmodel import Field, Session, SQLModel, create_engine, select
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)
def create_heroes():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
def select_heroes():
with Session(engine) as session:
heroes = session.exec(select(Hero)).all()
print(heroes)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
이제 전부 한 줄에 담았어요. 아마 select 문을 이렇게 한 줄로 쓰는 경우가 더 많을 거예요.
SQLModel 또는 SQLAlchemy - 기술적 세부 사항
SQLModel은 사실 바닥에서 보면 대략 SQLAlchemy와 Pydantic을 합쳐 놓은 거예요.
두 라이브러리와 같은 타입의 객체를 사용하고 돌려주며, 둘 다와 호환돼요.
그럼에도 SQLModel은 개발자 경험을 개선하기 위해 자체적인 내부 요소 몇 가지를 정의해요.
이 장에서 그중 일부를 살펴보고 있어요.
SQLModel의 select
sqlmodel에서 select() 함수를 import하면 SQLModel 버전의 select를 쓰는 거예요.
SQLAlchemy에도 자체 select가 있고, SQLModel의 select는 내부적으로 SQLAlchemy의 select를 사용해요.
하지만 SQLModel 버전은 타입 애너테이션 관련 트릭을 많이 써서, VS Code, PyCharm, 그 밖의 편집기 어디를 쓰든 최고의 편집기 지원을 받을 수 있게 해줘요. ✨
참고
내부 코드의 여러 버전을 실험하고 연구하면서 이 부분을 최대한 개선하는 데 많은 공이 들어갔어요. 🤓
SQLModel의 session.exec
📢 여기 있는 이 내용은 특히 주의를 기울여야 할 부분이에요.
SQLAlchemy 자체 Session에는 session.execute()라는 메서드가 있어요. session.exec() 메서드는 없어요.
SQLAlchemy 튜토리얼을 보면 항상 session.execute()를 쓰지요.
SQLModel의 Session은 SQLAlchemy의 Session을 직접 상속하고, 여기에 session.exec() 메서드를 추가해요. 바닥에서는 같은 session.execute()를 사용해요.
하지만 session.exec()는 session()의 트릭과 결합된 여러 트릭을 써서, select에서 데이터를 가져온 뒤에도 어디서든 자동 완성과 인라인 오류로 최고의 편집기 지원을 제공해요. ✨
예를 들어 SQLAlchemy에서는 여기에 .scalars()를 붙여줘야 해요.
heroes = session.execute(select(Hero)).scalars().all()
하지만 여러 개를 선택할 때는(나중에 보게 될 거예요) 그걸 제거해야 해요.
SQLModel의 session.exec()는 그 부분을 대신 처리해 줘서, .scalars()를 붙일 필요가 없어요.
이건 현재 SQLAlchemy가 제공할 수 없는 부분이에요. 일반 session.execute()는 레거시를 포함한 여러 다른 사용 사례를 지원해야 하기 때문에, 이걸 지원할 만한 내부 타입 애너테이션과 트릭을 전부 담을 수 없거든요.
게다가 SQLModel의 session.exec()는 작성해야 하는 코드 양을 줄이고 최대한 직관적으로 만들기 위한 트릭도 몇 가지 해요.
그렇지만 SQLModel의 Session도 여전히 session.execute()에 접근할 수 있어요.
팁
편집기는
session.exec()와session.execute()둘 다에 자동 완성을 제공해 줄 거예요.
📢 최고의 편집기 지원과 개발자 경험을 위해 항상 session.exec()를 쓰도록 기억해 주세요.
SQLModel 스타일의 주의점(Caveats)
SQLModel은 좁은 범위의 매우 흔한 사용 사례들에서 최고의 개발자 경험을 제공하도록 설계됐어요. ✨
필요할 땐 SQLModel을 SQLAlchemy와 직접 결합해서, 더 낮은 수준의 더 "순수한" SQL 구문, 이색적인 패턴, 심지어 레거시 패턴까지 SQLAlchemy의 모든 기능을 사용할 수 있어요. 🤓
하지만 SQLModel의 설계(예: 타입 애너테이션)는 문서에서 내가 설명하는 방식대로 사용한다는 전제를 깔고 있어요.
덕분에 자동 완성과 인라인 오류를 최대한 많이 받을 수 있어요. 🚀
하지만 이 말은 반대로, SQLModel을 SQLAlchemy의 좀 더 이색적인 패턴과 함께 쓰면 편집기가 오류가 있다고 말해줄 수도 있는데, 사실 코드는 그대로 잘 동작한다는 뜻이기도 해요.
그게 바로 트레이드오프예요. 🤷
하지만 그런 이색 패턴이 필요한 상황에서는 (같은 모델을 쓰는 등으로) SQLModel과 결합해 SQLAlchemy를 직접 사용할 수 있어요.