행 하나 읽기
행 하나 읽기 (Read One Row)
이미 .where()로 행을 필터링해서 선택하는 법은 알아요.
그리고 select()를 실행하면 보통 이터러블(iterable) 객체를 반환한다는 것도 봤죠.
또는 이터러블 대신 results.all()을 호출해 모든 행의 리스트를 바로 얻을 수도 있어요.
하지만 많은 경우 정말로 행 하나만 읽고 싶은데, 이터러블이나 리스트를 다루는 건 불편할 때가 많아요.
행 하나를 읽는 유틸리티들을 살펴볼게요.
출처: 공식문서
이전 코드에서 이어가기
지금까지 챕터들에서 데이터를 만들고 조회할 때 쓰던 예제를 그대로 이어서, 계속 수정해 나갈게요.
👀 전체 파일 미리 보기
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 = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
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)
hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32)
hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35)
hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36)
hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.add(hero_4)
session.add(hero_5)
session.add(hero_6)
session.add(hero_7)
session.commit()
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.age <= 35)
results = session.exec(statement)
for hero in results:
print(hero)
def main():
create_db_and_tables()
create_heroes()
select_heroes()
if __name__ == "__main__":
main()
혹시 앞선 예제를 이미 실행해서 데이터가 있는 데이터베이스가 있다면, 각 예제를 실행하기 전에 데이터베이스 파일을 지워주세요. 그래야 데이터가 중복되지 않고 같은 결과를 얻을 수 있어요.
첫 번째 행 읽기
지금까지 result 객체의 행들을 이렇게 순회하고 있었어요:
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.age <= 35)
results = session.exec(statement)
for hero in results:
print(hero)
그런데 모든 행이 아니라 첫 번째 것만 필요하다고 해 봅시다.
results 객체에서 .first() 메서드를 호출해 첫 번째 행을 얻을 수 있어요:
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.age <= 35)
results = session.exec(statement)
hero = results.first()
print("Hero:", hero)
이건 results의 첫 번째 객체를 반환해요(있다면).
그래서 이터러블이나 리스트를 다룰 필요가 없어져요.
!!! tip "팁"
`.first()`는 `select()` 문장이 아니라 `results` **객체의 메서드**라는 점을 눈여겨보세요.
이 쿼리는 행 두 개를 찾겠지만, .first()를 쓰면 첫 번째 행만 가져와요.
커맨드 라인에서 실행하면 이렇게 출력돼요:
$ uv run python app.py
// 부수적인 출력은 생략했어요 😉
// WHERE가 있는 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.age <= ?
INFO Engine [no key 0.00021s] (35,)
// 첫 번째 항목만 프린트
Hero: secret_name='Natalia Roman-on' age=32 id=4 name='Tarantula'
첫 번째 또는 None
SQL 쿼리가 어떤 행도 찾지 못하는 경우가 있을 수 있어요.
그 경우 .first()는 None을 반환해요:
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.age < 25)
results = session.exec(statement)
hero = results.first()
print("Hero:", hero)
이 경우 25보다 어린 히어로가 없으니, .first()는 None을 반환해요.
커맨드 라인에서 실행하면 이렇게 출력돼요:
$ uv run python app.py
// 부수적인 출력은 생략했어요 😉
// WHERE가 있는 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.age <= ?
INFO Engine [no key 0.00021s] (35,)
// 찾은 행이 없으니 first는 None
Hero: None
정확히 하나 (Exactly One)
쿼리와 일치하는 행이 정확히 하나임을 보장하고 싶은 경우가 있을 수 있어요.
만약 하나보다 많다면, 그건 시스템에 오류가 있다는 뜻이니 오류를 내고 종료해야 해요.
그런 경우 .first() 대신 .one()을 쓸 수 있어요:
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.name == "Deadpond")
results = session.exec(statement)
hero = results.one()
print("Hero:", hero)
여기서는 "Deadpond"가 하나뿐이고 그 이상은 없어야 한다는 걸 알고 있어요.
한 번 실행하면 이렇게 출력돼요:
$ uv run python app.py
// 부수적인 출력은 생략했어요 😉
// WHERE가 있는 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.name = ?
INFO Engine [no key 0.00015s] ('Deadpond',)
// 행이 하나만 찾아졌어요, 좋아요 ✅
Hero: secret_name='Dive Wilson' age=None id=1 name='Deadpond'
하지만 다시 실행하면, 모든 히어로를 데이터베이스에 또 넣게 되니 중복이 생겨 "Deadpond"가 하나보다 많아져요. 😱
그래서 database.db 파일을 먼저 삭제하지 않고 다시 실행하면 이렇게 출력돼요:
$ uv run python app.py
// 부수적인 출력은 생략했어요 😉
// WHERE가 있는 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.name = ?
INFO Engine [no key 0.00015s] ('Deadpond',)
// 아뿔싸, 데이터베이스가 중복 때문에 깨진 상태네요! 🚨
Traceback (most recent call last):
// 오류에 대한 세부 내용은 생략했어요
sqlalchemy.exc.MultipleResultsFound: Multiple rows were found when exactly one was required
더 많은 데이터로 정확히 하나
물론 데이터를 중복시키지 않아도, 행을 하나보다 많이 찾는 쿼리를 보내면서 .one()으로 정확히 하나를 기대하면 같은 오류가 날 수 있어요:
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.age <= 35)
results = session.exec(statement)
hero = results.one()
print("Hero:", hero)
그러면 행 2개를 찾게 되고, 결국 같은 오류가 날 거예요.
데이터가 없을 때 정확히 하나
그리고 .one()으로 행을 전혀 얻지 못하는 경우에도 오류가 나요:
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.age < 25)
results = session.exec(statement)
hero = results.one()
print("Hero:", hero)
이 경우 25보다 어린 히어로가 없으니, .one()은 오류를 내요.
커맨드 라인에서 실행하면 이걸 보게 돼요:
$ uv run python app.py
// 부수적인 출력은 생략했어요 😉
// WHERE가 있는 SELECT
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.age < ?
INFO Engine [no key 0.00014s] (25,)
// 아뿔싸, 행이 하나 있으리라 기대했는데 아무것도 없어요! 🚨
Traceback (most recent call last):
// 오류에 대한 세부 내용은 생략했어요
sqlalchemy.exc.NoResultFound: No row was found when one was required
간결한 버전
물론 .first()와 .one()을 쓸 때는 대부분 더 간결한 형태로, 한 줄(또는 적어도 한 개의 파이썬 문장)로 다 쓰게 될 거예요:
def select_heroes():
with Session(engine) as session:
hero = session.exec(select(Hero).where(Hero.name == "Deadpond")).one()
print("Hero:", hero)
위의 몇몇 예제와 같은 결과가 돼요.
.where()로 Id로 조회하기
많은 경우 기본 키인 Id 열로 행 하나를 조회하고 싶을 거예요.
지금까지 해온 것처럼 .where()를 쓰고 .first()로 첫 항목을 얻는 방식으로 할 수 있어요:
def select_heroes():
with Session(engine) as session:
statement = select(Hero).where(Hero.id == 1)
results = session.exec(statement)
hero = results.first()
print("Hero:", hero)
기대한 대로 올바르게 동작해요. 하지만 더 짧은 버전이 있어요. 👇
.get()으로 Id로 조회하기
기본 키인 Id 열로 행 하나를 조회하는 건 흔한 작업이라, 그 전용 단축키가 있어요:
def select_heroes():
with Session(engine) as session:
hero = session.get(Hero, 1)
print("Hero:", hero)
session.get(Hero, 1)은 select()를 만들고, .where()로 Id로 필터링한 다음 .first()로 첫 항목을 얻는 것과 동일해요.
실행하면 이렇게 출력돼요:
$ uv run python app.py
// 부수적인 출력은 생략했어요 😉
// WHERE가 있는 SELECT
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00021s] (1,)
// 프린트된 결과
Hero: secret_name='Dive Wilson' age=None id=1 name='Deadpond'
데이터가 없을 때 .get()으로 Id로 조회하기
.get()은 .first()와 비슷하게 동작해요. 데이터가 없으면 (오류를 내는 대신) 그냥 None을 반환해요:
def select_heroes():
with Session(engine) as session:
hero = session.get(Hero, 9001)
print("Hero:", hero)
실행하면 이렇게 출력돼요:
$ uv run python app.py
// 부수적인 출력은 생략했어요 😉
// WHERE가 있는 SELECT
INFO Engine BEGIN (implicit)
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00024s] (9001,)
// 데이터를 찾지 못했으니 값은 None
Hero: None
요약
SQL 데이터베이스에서 행 하나를 조회하는 건 흔한 작업이니, 이제 그걸 짧고 간단하게 할 수 있는 도구가 여러 개 생겼어요. 🎉