committed by
GitHub
19 changed files with 1383 additions and 40 deletions
@ -0,0 +1,258 @@ |
|||
# Parameter Path |
|||
|
|||
"parameter" atau "variabel" path didefinisikan dengan sintaksis Python format string: |
|||
|
|||
{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} |
|||
|
|||
Nilai parameter path `item_id` akan dikirim ke fungsi sebagai argument `item_id`: |
|||
|
|||
Jika anda menjalankan contoh berikut dan kunjungi <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, anda akan melihat respon: |
|||
|
|||
```JSON |
|||
{"item_id":"foo"} |
|||
``` |
|||
|
|||
## Parameter path dengan tipe data |
|||
|
|||
Tipe data parameter path bisa didefinisikan di dalam fungsi, menggunakan anotasi tipe data standar Python: |
|||
|
|||
{* ../../docs_src/path_params/tutorial002.py hl[7] *} |
|||
|
|||
Dalam hal ini `item_id` didefinisikan sebagai `int`. |
|||
|
|||
/// check | Periksa |
|||
|
|||
Penyunting kode anda bisa membantu periksa di dalam fungsi seperti pemeriksaan kesalahan, kelengkapan kode, dll. |
|||
|
|||
/// |
|||
|
|||
## <abbr title="juga disebut: serialization, parsing, marshalling">Konversi</abbr> data |
|||
|
|||
Jika contoh berikut dijalankan dan diakses browser melalui <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a>, anda akan melihat respon: |
|||
|
|||
```JSON |
|||
{"item_id":3} |
|||
``` |
|||
|
|||
/// check | Periksa |
|||
|
|||
Perhatikan nilai fungsi yang diterima (dan dihasilkan) adalah `3`, sebagai `int` di Python, dan bukan string `"3"`. |
|||
|
|||
Sehingga dengan deklarasi tipe data **FastAPI** memberikan request otomatis <abbr title="konversi string dari request HTTP menjadi data Python">"parsing"</abbr>. |
|||
|
|||
/// |
|||
|
|||
## Validasi Data |
|||
|
|||
Tetapi jika di browser anda akses <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, anda akan melihat pesan kesalahan HTTP: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "int_parsing", |
|||
"loc": [ |
|||
"path", |
|||
"item_id" |
|||
], |
|||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|||
"input": "foo", |
|||
"url": "https://errors.pydantic.dev/2.1/v/int_parsing" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
Karena parameter path `item_id` bernilai `"foo"` yang bukan tipe data `int`. |
|||
|
|||
Kesalahan yang sama akan muncul jika menggunakan `float` daripada `int`, seperti di: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> |
|||
|
|||
/// check | Periksa |
|||
|
|||
Dengan deklarasi tipe data Python, **FastAPI** melakukan validasi data. |
|||
|
|||
Perhatikan kesalahan tersebut juga menjelaskan validasi apa yang tidak sesuai. |
|||
|
|||
Validasi ini sangat membantu ketika mengembangkan dan men-*debug* kode yang berhubungan dengan API anda. |
|||
|
|||
/// |
|||
|
|||
## Dokumentasi |
|||
|
|||
Ketika anda membuka browser di <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>, anda melihat dokumentasi API interaktif otomatis berikut: |
|||
|
|||
<img src="/img/tutorial/path-params/image01.png"> |
|||
|
|||
/// check | Periksa |
|||
|
|||
Dengan deklarasi tipe data Python yang sama, **FastAPI** membuat dokumentasi interaktif otomatis (terintegrasi Swagger UI). |
|||
|
|||
Perhatikan parameter path dideklarasikan sebagai integer. |
|||
|
|||
/// |
|||
|
|||
## Keuntungan basis-standar, dokumentasi alternatif |
|||
|
|||
Karena skema yang dibuat berasal dari standar <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md" class="external-link" target="_blank">OpenAPI</a>, maka banyak alat lain yang kompatibel. |
|||
|
|||
Sehingga **FastAPI** menyediakan dokumentasi alternatif (menggunakan ReDoc), yang bisa diakses di <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>: |
|||
|
|||
<img src="/img/tutorial/path-params/image02.png"> |
|||
|
|||
Cara yang sama untuk menggunakan tools kompatibel lainnya. Termasuk alat membuat kode otomatis untuk banyak bahasa. |
|||
|
|||
## Pydantic |
|||
|
|||
Semua validasi data dikerjakan di belakang layar oleh <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>, sehingga anda mendapatkan banyak kemudahan. Anda juga tahu proses ini akan ditangani dengan baik. |
|||
|
|||
Anda bisa mendeklarasikan tipe data dengan `str`, `float`, `bool` dan banyak tipe data kompleks lainnya. |
|||
|
|||
Beberapa tipe di atas akan dibahas pada bab berikutnya tutorial ini. |
|||
|
|||
## Urutan berpengaruh |
|||
|
|||
Ketika membuat *operasi path*, anda bisa menghadapi kondisi dimana *path* nya sudah tetap. |
|||
|
|||
Seperti `/users/me`, untuk mendapatkan data user yang sedang aktif. |
|||
|
|||
Kemudian anda bisa memiliki path `/users/{user_id}` untuk mendapatkan data user tertentu melalui user ID. |
|||
|
|||
karena *operasi path* dievaluasi melalui urutan, anda harus memastikan path untuk `/users/me` dideklarasikan sebelum `/user/{user_id}`: |
|||
|
|||
{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} |
|||
|
|||
Sebaliknya, path `/users/{user_id}` juga akan sesuai dengan `/users/me`, "menganggap" menerima parameter `user_id` dengan nilai `"me"`. |
|||
|
|||
Serupa, anda juga tidak bisa mendefinisikan operasi path: |
|||
|
|||
{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} |
|||
|
|||
Path pertama akan selalu digunakan karena path sesuai dengan yang pertama. |
|||
|
|||
## Nilai terdefinisi |
|||
|
|||
Jika ada *operasi path* yang menerima *parameter path*, tetapi anda ingin nilai valid *parameter path* sudah terdefinisi, anda bisa menggunakan standar Python <abbr title="Enumeration">`Enum`</abbr>. |
|||
|
|||
### Membuat class `Enum` |
|||
|
|||
Import `Enum` dan buat *sub-class* warisan dari `str` dan `Enum`. |
|||
|
|||
Dengan warisan dari `str` dokumen API mengetahui nilai nya harus berjenis `string` supaya bisa digunakan dengan benar. |
|||
|
|||
Kemudian buat atribut *class* dengan nilai tetap *string* yang benar: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} |
|||
|
|||
/// info |
|||
|
|||
<a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">Enumerasi (atau enum) tersedia di Python</a> sejak versi 3.4. |
|||
|
|||
/// |
|||
|
|||
/// tip | Tips |
|||
|
|||
"AlxexNet", "ResNet", dan "LeNet" adalah nama <abbr title="Secara teknis, arsitektur model Deep Learning">model</abbr> *Machine Learning*. |
|||
|
|||
/// |
|||
|
|||
### Mendeklarasikan *parameter path* |
|||
|
|||
Kemudian buat *parameter path* dengan tipe anotasi menggunakan *class* enum dari (`ModelName`) |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[16] *} |
|||
|
|||
### Periksa dokumentasi |
|||
|
|||
Karena nilai yang tersedia untuk *parameter path* telah terdefinisi, dokumen interatik bisa memunculkan: |
|||
|
|||
<img src="/img/tutorial/path-params/image03.png"> |
|||
|
|||
### Bekerja dengan *enumarasi* Python |
|||
|
|||
Nilai *parameter path* akan menjadi *anggota enumerasi*. |
|||
|
|||
#### Membandingkan *anggota enumerasi* |
|||
|
|||
Anda bisa membandingkan parameter *path* dengan *anggota enumerasi* di enum `ModelName` yang anda buat: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[17] *} |
|||
|
|||
#### Mendapatkan *nilai enumerasi* |
|||
|
|||
Anda bisa mendapatkan nilai (`str` dalam kasus ini) menggunakan `model_name.value`, atau secara umum `anggota_enum_anda.value`: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[20] *} |
|||
|
|||
/// tip | Tips |
|||
|
|||
Anda bisa mengakses nilai `"lenet"` dnegan `ModelName.lenet.value`. |
|||
|
|||
/// |
|||
|
|||
#### Menghasilkan *anggota enumerasi* |
|||
|
|||
Anda bisa menghasilkan *anggota enumerasi* dari *operasi path* bahkan di body JSON bersarang (contoh `dict`). |
|||
|
|||
They will be converted to their corresponding values (strings in this case) before returning them to the client: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} |
|||
|
|||
Klien akan mendapatkan respon JSON seperti berikut: |
|||
|
|||
```JSON |
|||
{ |
|||
"model_name": "alexnet", |
|||
"message": "Deep Learning FTW!" |
|||
} |
|||
``` |
|||
|
|||
## Parameter path berisi path |
|||
|
|||
Misalkan terdapat *operasi path* dengan path `/files/{file_path}`. |
|||
|
|||
Tetapi anda memerlukan `file_path` itu berisi *path*, seperti like `home/johndoe/myfile.txt`. |
|||
|
|||
Sehingga URL untuk file tersebut akan seperti: `/files/home/johndoe/myfile.txt`. |
|||
|
|||
### Dukungan OpenAPI |
|||
|
|||
OpenAPI tidak bisa mendeklarasikan *parameter path* berisi *path* di dalamnya, karena menyebabkan kondisi yang sulit di*test* dan didefinisikan. |
|||
|
|||
Tetapi, di **FastAPI** anda tetap bisa melakukannya dengan menggunakan *tools* internal dari Starlette. |
|||
|
|||
Dan dokumentasi tetap berfungsi walaupun tidak menambahkan keterangan bahwa parameter harus berisi *path*. |
|||
|
|||
### Konverter path |
|||
|
|||
Melalui Starlette anda bisa mendeklarasikan *parameter path* berisi *path* dengan URL seperti: |
|||
|
|||
``` |
|||
/files/{file_path:path} |
|||
``` |
|||
|
|||
Dikondisi ini nama parameter adalah `file_path` dan bagian terakhir `:path` menginformasikan parameter harus sesuai dengan setiap *path*. |
|||
|
|||
Sehingga anda bisa menggunakan: |
|||
|
|||
{* ../../docs_src/path_params/tutorial004.py hl[6] *} |
|||
|
|||
/// tip | Tips |
|||
|
|||
Anda mungkin perlu parameter berisi `/home/johndoe/myfile.txt` di awali garis belakang (`/`). |
|||
|
|||
Di kondisi ini, URL nya menjadi: `/files//home/johndoe/myfile.txt`, dengan dua garis belakang (`//`) di antara `files` dan `home`. |
|||
|
|||
/// |
|||
|
|||
## Ringkasan |
|||
|
|||
Di **FastAPI** dengan menggunakan deklarasi tipe Python standar, pendek, intuitif, anda mendapatkan: |
|||
|
|||
* Dukungan editor: pemeriksaan kesalahan, autocompletion, dll. |
|||
* "<abbr title="konversi string dari request HTTP menjadi data Python">Parsing</abbr>" data. |
|||
* Validasi data. |
|||
* Annotasi API dan dokumentasi otomatis. |
|||
|
|||
Semua itu anda hanya perlu mendeklarasikan sekali saja. |
|||
|
|||
Ini adalah salah satu keunggulan **FastAPI** dibandingkan dengan *framework* lainnya (selain dari performa Python *native*c) |
@ -0,0 +1,360 @@ |
|||
# SQL (관계형) 데이터베이스 |
|||
|
|||
**FastAPI**에서 SQL(관계형) 데이터베이스 사용은 필수가 아닙니다. 여러분이 원하는 **어떤 데이터베이스든** 사용할 수 있습니다. |
|||
|
|||
여기서는 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a>을 사용하는 예제를 살펴보겠습니다. |
|||
|
|||
**SQLModel**은 <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a>와 Pydantic을 기반으로 구축되었습니다.SQLModel은 **SQL 데이터베이스**를 사용하는 FastAPI 애플리케이션에 완벽히 어울리도록 **FastAPI**의 제작자가 설계한 도구입니다. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
다른 SQL 또는 NoSQL 데이터베이스 라이브러리를 사용할 수도 있습니다 (일부는 <abbr title="객체 관계 매퍼(Object Relational Mapper), SQL 테이블을 나타내는 클래스를 제공하고 테이블의 행을 인스턴스로 표현하는 라이브러리를 지칭하는 용어">"ORM"</abbr>이라고도 불립니다), FastAPI는 특정 라이브러리의 사용을 강요하지 않습니다. 😎 |
|||
|
|||
/// |
|||
|
|||
SQLModel은 SQLAlchemy를 기반으로 하므로, SQLAlchemy에서 **지원하는 모든 데이터베이스**를 손쉽게 사용할 수 있습니다(SQLModel에서도 동일하게 지원됩니다). 예를 들면: |
|||
|
|||
* PostgreSQL |
|||
* MySQL |
|||
* SQLite |
|||
* Oracle |
|||
* Microsoft SQL Server 등. |
|||
|
|||
이 예제에서는 **SQLite**를 사용합니다. SQLite는 단일 파일을 사용하고 파이썬에서 기본적으로 지원하기 때문입니다. 따라서 이 예제를 그대로 복사하여 실행할 수 있습니다. |
|||
|
|||
나중에 실제 프로덕션 애플리케이션에서는 **PostgreSQL**과 같은 데이터베이스 서버를 사용하는 것이 좋습니다. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
**FastAPI**와 **PostgreSQL**를 포함하여 프론트엔드와 다양한 도구를 제공하는 공식 프로젝트 생성기가 있습니다: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a> |
|||
|
|||
/// |
|||
|
|||
이 튜토리얼은 매우 간단하고 짧습니다. 데이터베이스 기본 개념, SQL, 또는 더 복잡한 기능에 대해 배우고 싶다면, <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 문서</a>를 참고하세요. |
|||
|
|||
## `SQLModel` 설치하기 |
|||
|
|||
먼저, [가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, `sqlmodel`을 설치하세요: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install sqlmodel |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 단일 모델로 애플리케이션 생성하기 |
|||
|
|||
우선 단일 **SQLModel** 모델을 사용하여 애플리케이션의 가장 간단한 첫 번째 버전을 생성해보겠습니다. |
|||
|
|||
이후 **다중 모델**을 추가하여 보안과 유연성을 강화할 것입니다. 🤓 |
|||
|
|||
### 모델 생성하기 |
|||
|
|||
`SQLModel`을 가져오고 데이터베이스 모델을 생성합니다: |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *} |
|||
|
|||
`Hero` 클래스는 Pydantic 모델과 매우 유사합니다 (실제로 내부적으로 *Pydantic 모델이기도 합니다*). |
|||
|
|||
몇 가지 차이점이 있습니다: |
|||
|
|||
* `table=True`는 SQLModel에 이 모델이 *테이블 모델*이며, 단순한 데이터 모델이 아니라 SQL 데이터베이스의 **테이블**을 나타낸다는 것을 알려줍니다. (다른 일반적인 Pydantic 클래스처럼) 단순한 *데이터 모델*이 아닙니다. |
|||
|
|||
* `Field(primary_key=True)`는 SQLModel에 `id`가 SQL 데이터베이스의 **기본 키**임을 알려줍니다 (SQL 기본 키에 대한 자세한 내용은 SQLModel 문서를 참고하세요). |
|||
|
|||
`int | None` 유형으로 설정하면, SQLModel은 해당 열이 SQL 데이터베이스에서 `INTEGER` 유형이며 `NULLABLE` 값이어야 한다는 것을 알 수 있습니다. |
|||
|
|||
* `Field(index=True)`는 SQLModel에 해당 열에 대해 **SQL 인덱스**를 생성하도록 지시합니다. 이를 통해 데이터베이스에서 이 열으로 필터링된 데이터를 읽을 때 더 빠르게 조회할 수 있습니다. |
|||
|
|||
SQLModel은 `str`으로 선언된 항목이 SQL 데이터베이스에서 `TEXT` (또는 데이터베이스에 따라 `VARCHAR`) 유형의 열로 저장된다는 것을 인식합니다. |
|||
|
|||
### 엔진 생성하기 |
|||
|
|||
SQLModel의 `engine` (내부적으로는 SQLAlchemy `engine`)은 데이터베이스에 대한 **연결을 유지**하는 역할을 합니다. |
|||
|
|||
**하나의 단일 engine 객체**를 통해 코드 전체에서 동일한 데이터베이스에 연결할 수 있습니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *} |
|||
|
|||
`check_same_thread=False`를 사용하면 FastAPI에서 여러 스레드에서 동일한 SQLite 데이터베이스를 사용할 수 있습니다. 이는 **하나의 단일 요청**이 **여러 스레드**를 사용할 수 있기 때문에 필요합니다(예: 의존성에서 사용되는 경우). |
|||
|
|||
걱정하지 마세요. 코드가 구조화된 방식으로 인해, 이후에 **각 요청마다 단일 SQLModel *세션*을 사용**하도록 보장할 것입니다. 실제로 그것이 `check_same_thread`가 하려는 것입니다. |
|||
|
|||
### 테이블 생성하기 |
|||
|
|||
그 다음 `SQLModel.metadata.create_all(engine)`을 사용하여 모든 *테이블 모델*의 **테이블을 생성**하는 함수를 추가합니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} |
|||
|
|||
### 세션 의존성 생성하기 |
|||
|
|||
**`Session`**은 **메모리에 객체**를 저장하고 데이터에 필요한 모든 변경 사항을 추적한 후, **`engine`을 통해** 데이터베이스와 통신합니다. |
|||
|
|||
`yield`를 사용해 FastAPI의 **의존성**을 생성하여 각 요청마다 새로운 `Session`을 제공합니다. 이는 요청당 하나의 세션만 사용되도록 보장합니다. 🤓 |
|||
|
|||
그런 다음 이 의존성을 사용하는 코드를 간소화하기 위해 `Annotated` 의존성 `SessionDep`을 생성합니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} |
|||
|
|||
### 시작 시 데이터베이스 테이블 생성하기 |
|||
|
|||
애플리케이션 시작 시 데이터베이스 테이블을 생성합니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *} |
|||
|
|||
여기서는 애플리케이션 시작 이벤트 시 테이블을 생성합니다. |
|||
|
|||
프로덕션 환경에서는 애플리케이션을 시작하기 전에 실행되는 마이그레이션 스크립트를 사용할 가능성이 높습니다. 🤓 |
|||
|
|||
/// tip | 팁 |
|||
|
|||
SQLModel은 Alembic을 감싸는 마이그레이션 유틸리티를 제공할 예정입니다. 하지만 현재 <a href="https://alembic.sqlalchemy.org/en/latest/" class="external-link" target="_blank">Alembic</a>을 직접 사용할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
### Hero 생성하기 |
|||
|
|||
각 SQLModel 모델은 Pydantic 모델이기도 하므로, Pydantic 모델을 사용할 수 있는 **타입 어노테이**션에서 동일하게 사용할 수 있습니다. |
|||
|
|||
예를 들어, 파라미터를 `Hero` 타입으로 선언하면 **JSON 본문**에서 값을 읽어옵니다. |
|||
|
|||
마찬가지로, 함수의 **반환 타입**으로 선언하면 해당 데이터의 구조가 자동으로 생성되는 API 문서의 UI에 나타납니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} |
|||
|
|||
</details> |
|||
|
|||
여기서 `SessionDep` 의존성 (즉, `Session`)을 사용하여 새로운 `Hero`를 `Session` 인스턴스에 추가하고, 데이터베이스에 변경 사항을 커밋하고, `hero` 데이터의 최신 상태를 갱신한 다음 이를 반환합니다. |
|||
|
|||
### Heroes 조회하기 |
|||
|
|||
`select()`를 사용하여 데이터베이스에서 `Hero`를 **조회**할 수 있습니다. 결과에 페이지네이션을 적용하기 위해 `limit`와 `offset`을 포함할 수 있습니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} |
|||
|
|||
### 단일 Hero 조회하기 |
|||
|
|||
단일 `Hero`를 **조회**할 수도 있습니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} |
|||
|
|||
### Hero 삭제하기 |
|||
|
|||
`Hero`를 **삭제**하는 것도 가능합니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} |
|||
|
|||
### 애플리케이션 실행하기 |
|||
|
|||
애플리케이션을 실행하려면 다음 명령을 사용합니다: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
그런 다음 `/docs` UI로 이동하면, **FastAPI**가 해당 **model들**을 사용하여 API **문서를 생성**하는 것으르 확인할 수 있습니다. 또한 이 모델들은 데이터를 직렬화하고 검증하는 데에도 사용됩니다. |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image01.png"> |
|||
</div> |
|||
|
|||
## 여러 모델로 애플리케이션 업데이트 |
|||
|
|||
이제 애플리케이션을 약간 **리팩토링**하여 **보안**과 **유연성**을 개선해 보겠습니다. |
|||
|
|||
이전 애플리케이션의 UI를 보면, 지금까지는 클라이언트가 생성할 `Hero`의 `id`를 직접 지정할 수 있다는 것을 알 수 있습니다. 😱 |
|||
|
|||
이는 허용되어선 안 됩니다. 클라이언트가 이미 데이터베이스에 저장된 `id`를 덮어쓸 위험이 있기 때문입니다. `id`는 **백엔드** 또는 **데이터베이스**가 결정해야 하며, **클라이언트**가 결정해서는 안 됩니다. |
|||
|
|||
또한 hero의 `secret_name`을 생성하긴 했지만, 지금까지는 이 값을 어디에서나 반환하고 있습니다. 이는 그다지 **비밀스럽지** 않습니다... 😅 |
|||
|
|||
이러한 문제를 해결하기 위해 몇 가지 **추가 모델**을 추가할 것입니다. 바로 여기서 SQLModel이 빛을 발하게 됩니다. ✨ |
|||
|
|||
### 여러 모델 생성하기 |
|||
|
|||
**SQLModel**에서 `table=True`가 설정된 모델 클래스는 **테이블 모델**입니다. |
|||
|
|||
`table=True`가 없는 모델 클래스는 **데이터 모델**로, 이는 실제로 몇 가지 추가 기능이 포함된 Pydantic 모델에 불과합니다. 🤓 |
|||
|
|||
SQLModel을 사용하면 **상속**을 통해 모든 경우에 필드를 **중복 선언하지 않아도** 됩니다. |
|||
|
|||
#### `HeroBase` - 기본 클래스 |
|||
|
|||
모든 모델에서 **공유되는 필드**를 가진 `HeroBase` 모델을 시작해 봅시다: |
|||
|
|||
* `name` |
|||
* `age` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} |
|||
|
|||
#### `Hero` - *테이블 모델* |
|||
|
|||
다음으로 실제 *테이블 모델*인 `Hero`를 생성합니다. 이 모델은 다른 모델에는 항상 포함되는 건 아닌 **추가 필드**를 포함합니다: |
|||
|
|||
* `id` |
|||
* `secret_name` |
|||
|
|||
`Hero`는 `HeroBase`를 상속하므로 `HeroBase`에 선언된 필드도 포함합니다. 따라서 `Hero`는 다음 **필드들도** 가지게 됩니다: |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} |
|||
|
|||
#### `HeroPublic` - 공개 *데이터 모델* |
|||
|
|||
다음으로 `HeroPublic` 모델을 생성합니다. 이 모델은 API 클라이언트에 **반환**되는 모델입니다. |
|||
|
|||
`HeroPublic`은 `HeroBase`와 동일한 필드를 가지며, `secret_name`은 포함하지 않습니다. |
|||
|
|||
마침내 우리의 heroes의 정체가 보호됩니다! 🥷 |
|||
|
|||
또한 `id: int`를 다시 선언합니다. 이를 통해, API 클라이언트와 **계약**을 맺어 `id`가 항상 존재하며 항상 `int` 타입이라는 것을 보장합니다(`None`이 될 수 없습니다). |
|||
|
|||
/// tip | 팁 |
|||
|
|||
반환 모델이 값이 항상 존재하고 항상 `int`(`None`이 아님)를 보장하는 것은 API 클라이언트에게 매우 유용합니다. 이를 통해 API와 통신하는 개발자가 훨씬 더 간단한 코드를 작성할 수 있습니다. |
|||
|
|||
또한 **자동으로 생성된 클라이언트**는 더 단순한 인터페이스를 제공하므로, API와 소통하는 개발자들이 훨씬 수월하게 작업할 수 있습니다. 😎 |
|||
|
|||
/// |
|||
|
|||
`HeroPublic`의 모든 필드는 `HeroBase`와 동일하며, `id`는 `int`로 선언됩니다(`None`이 아님): |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} |
|||
|
|||
#### `HeroCreate` - hero 생성용 *데이터 모델* |
|||
|
|||
이제 `HeroCreate` 모델을 생성합니다. 이 모델은 클라이언트로부터 받은 데이터를 **검증**하는 역할을 합니다. |
|||
|
|||
`HeroCreate`는 `HeroBase와` 동일한 필드를 가지며, 추가로 `secret_name을` 포함합니다. |
|||
|
|||
클라이언트가 **새 hero을 생성**할 때 `secret_name`을 보내고, 이는 데이터베이스에 저장되지만, 해당 비밀 이름은 API를 통해 클라이언트에게 반환되지 않습니다. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
이 방식은 **비밀번호**를 처리하는 방법과 동일합니다. 비밀번호를 받지만, 이를 API에서 반환하지는 않습니다. |
|||
|
|||
비밀번호 값을 저장하기 전에 **해싱**하여 저장하고, **평문으로 저장하지 마십시오**. |
|||
|
|||
/// |
|||
|
|||
`HeroCreate`의 필드는 다음과 같습니다: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} |
|||
|
|||
#### `HeroUpdate` - hero 수정용 *데이터 모델* |
|||
|
|||
이전 애플리케이션에서는 **hero를 수정**할 방법이 없었지만, 이제 **다중 모델**을 통해 수정 기능을 추가할 수 있습니다. 🎉 |
|||
|
|||
`HeroUpdate` *데이터 모델*은 약간 특별한데, 새 hero을 생성할 때 필요한 **모든 동일한 필드**를 가지지만, 모든 필드가 **선택적**(기본값이 있음)입니다. 이렇게 하면 hero을 수정할 때 수정하려는 필드만 보낼 수 있습니다. |
|||
|
|||
모든 **필드가 변경되기** 때문에(타입이 `None`을 포함하고, 기본값이 `None`으로 설정됨), 모든 필드를 **다시 선언**해야 합니다. |
|||
|
|||
엄밀히 말하면 `HeroBase`를 상속할 필요는 없습니다. 모든 필드를 다시 선언하기 때문입니다. 일관성을 위해 상속을 유지하긴 했지만, 필수는 아닙니다. 이는 개인적인 취향의 문제입니다. 🤷 |
|||
|
|||
`HeroUpdate`의 필드는 다음과 같습니다: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} |
|||
|
|||
### `HeroCreate`로 생성하고 `HeroPublic` 반환하기 |
|||
|
|||
이제 **다중 모델**을 사용하므로 애플리케이션의 관련 부분을 업데이트할 수 있습니다. |
|||
|
|||
요청에서 `HeroCreate` *데이터 모델*을 받아 이를 기반으로 `Hero` *테이블 모델*을 생성합니다. |
|||
|
|||
이 새 *테이블 모델* `Hero`는 클라이언트에서 보낸 필드를 가지며, 데이터베이스에서 생성된 `id`도 포함합니다. |
|||
|
|||
그런 다음 함수를 통해 동일한 *테이블 모델* `Hero`를 반환합니다. 하지만 `response_model`로 `HeroPublic` *데이터 모델*을 선언했기 때문에, **FastAPI**는 `HeroPublic`을 사용하여 데이터를 검증하고 직렬화합니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} |
|||
|
|||
/// tip | 팁 |
|||
|
|||
이제 **반환 타입 주석** `-> HeroPublic` 대신 `response_model=HeroPublic`을 사용합니다. 반환하는 값이 실제로 `HeroPublic`이 *아니기* 때문입니다. |
|||
|
|||
만약 `-> HeroPublic`으로 선언했다면, 에디터와 린터에서 반환값이 `HeroPublic`이 아니라 `Hero`라고 경고했을 것입니다. 이는 적절한 경고입니다. |
|||
|
|||
`response_model`에 선언함으로써 **FastAPI**가 이를 처리하도록 하고, 타입 어노테이션과 에디터 및 다른 도구의 도움에는 영향을 미치지 않도록 설정합니다. |
|||
|
|||
/// |
|||
|
|||
### `HeroPublic`으로 Heroes 조회하기 |
|||
|
|||
이전과 동일하게 `Hero`를 **조회**할 수 있습니다. 이번에도 `response_model=list[HeroPublic]`을 사용하여 데이터가 올바르게 검증되고 직렬화되도록 보장합니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} |
|||
|
|||
### `HeroPublic`으로 단일 Hero 조회하기 |
|||
|
|||
단일 hero을 **조회**할 수도 있습니다: |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} |
|||
|
|||
### `HeroUpdate`로 Hero 수정하기 |
|||
|
|||
**hero를 수정**할 수도 있습니다. 이를 위해 HTTP `PATCH` 작업을 사용합니다. |
|||
|
|||
코드에서는 클라이언트가 보낸 데이터를 딕셔너리 형태(`dict`)로 가져옵니다. 이는 **클라이언트가 보낸 데이터만 포함**하며, 기본값으로 들어가는 값은 제외합니다. 이를 위해 `exclude_unset=True`를 사용합니다. 이것이 주요 핵심입니다. 🪄 |
|||
|
|||
그런 다음, `hero_db.sqlmodel_update(hero_data)`를 사용하여 `hero_data`의 데이터를 `hero_db`에 업데이트합니다. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} |
|||
|
|||
### Hero 다시 삭제하기 |
|||
|
|||
hero **삭제**는 이전과 거의 동일합니다. |
|||
|
|||
이번에는 모든 것을 리팩토링하고 싶은 욕구를 만족시키지 못할 것 같습니다. 😅 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} |
|||
|
|||
### 애플리케이션 다시 실행하기 |
|||
|
|||
다음 명령을 사용해 애플리케이션을 다시 실행할 수 있습니다: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
`/docs` API UI로 이동하면 업데이트된 것을 확인할 수 있습니다. 클라이언트가 영웅을 생성할 때 `id`를 제공할 필요가 없게 되는 등의 변화도 보입니다. |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image02.png"> |
|||
</div> |
|||
|
|||
## 요약 |
|||
|
|||
<a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">**SQLModel**</a>을 사용하여 SQL 데이터베이스와 상호작용하고, *데이터 모델* 및 *테이블 모델*로 코드를 간소화할 수 있습니다. |
|||
|
|||
더 많은 내용을 배우고 싶다면, **SQLModel** 문서를 참고하세요. <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">SQLModel을 **FastAPI**와 함께 사용하는 것에 대한 더 긴 미니 튜토리얼</a>도 제공합니다. 🚀 |
@ -0,0 +1,358 @@ |
|||
# SQL (реляционные) базы данных |
|||
|
|||
**FastAPI** не требует использования реляционной базы данных. Вы можете воспользоваться любой базой данных, которой хотите. |
|||
|
|||
В этом разделе мы продемонстрируем, как работать с <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a>. |
|||
|
|||
Библиотека **SQLModel** построена на основе <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a> и Pydantic. Она была разработана автором **FastAPI** специально для приложений на основе FastAPI, которые используют **реляционные базы данных**. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Вы можете воспользоваться любой библиотекой для работы с реляционными (SQL) или нереляционными (NoSQL) базами данных. (Их ещё называют <abbr title="ORM = Object Relational Mapper, этот термин для библиотеки, в которой классы представляют SQL-таблицы, а экземпляры классов представляют строки в этих таблицах.">**ORM**</abbr> библиотеками). FastAPI не принуждает вас к использованию чего-либо конкретного. 😎 |
|||
|
|||
/// |
|||
|
|||
В основе SQLModel лежит SQLAlchemy, поэтому вы спокойно можете использовать любую базу данных, поддерживаемую SQLAlchemy (и, соответственно, поддерживаемую SQLModel), например: |
|||
|
|||
* PostgreSQL |
|||
* MySQL |
|||
* SQLite |
|||
* Oracle |
|||
* Microsoft SQL Server, и т.д. |
|||
|
|||
В данном примере мы будем использовать базу данных **SQLite**, т.к. она состоит из единственного файла и поддерживается встроенными библиотеками Python. Таким образом, вы сможете скопировать данный пример и запустить его как он есть. |
|||
|
|||
В дальнейшем, для продакшн-версии вашего приложения, возможно, вам стоит использовать серверную базу данных, например, **PostgreSQL**. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Существует официальный генератор проектов на **FastAPI** и **PostgreSQL**, который также включает frontend и дополнительные инструменты <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a> |
|||
|
|||
/// |
|||
|
|||
Это очень простое и короткое руководство, поэтому, если вы хотите узнать о базах данных в целом, об SQL, разобраться с более продвинутым функционалом, то воспользуйтесь <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">документацией SQLModel</a>. |
|||
|
|||
## Установка `SQLModel` |
|||
|
|||
Создайте виртуальное окружение [virtual environment](../virtual-environments.md){.internal-link target=_blank}, активируйте его и установите `sqlmodel`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install sqlmodel |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Создание приложения с единственной моделью |
|||
|
|||
Мы начнем с создания наиболее простой первой версии нашего приложения с одной единственной моделью **SQLModel**. |
|||
|
|||
В дальнейшем с помощью **дополнительных моделей** мы его улучшим и сделаем более безопасным и универсальным. 🤓 |
|||
|
|||
### Создание моделей |
|||
|
|||
Импортируйте `SQLModel` и создайте модель базы данных: |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *} |
|||
|
|||
Класс `Hero` очень напоминает модель Pydantic (фактически, под капотом, *это и есть модель Pydantic*). |
|||
|
|||
Но есть и некоторые различия |
|||
|
|||
* `table=True` для SQLModel означает, что это *модель-таблица*, которая должна представлять **таблицу** в реляционной базе данных. Это не просто *модель данных* (в отличие от обычного класса в Pydantic). |
|||
|
|||
* `Field(primary_key=True)` для SQLModel означает, что поле `id` является первичным ключом в таблице базы данных (вы можете подробнее узнать о первичных ключах баз данных в документации по SQLModel). |
|||
|
|||
Тип `int | None` сигнализирует для SQLModel, что столбец таблицы базы данных должен иметь тип `INTEGER`, или иметь пустое значение `NULL`. |
|||
|
|||
* `Field(index=True)` для SQLModel означает, что нужно создать **SQL индекс** для данного столбца. Это обеспечит более быстрый поиск при чтении данных, отфильтрованных по данному столбцу. |
|||
|
|||
SQLModel будет знать, что данные типа `str`, будут представлены в базе данных как `TEXT` (или `VARCHAR`, в зависимости от типа базы данных). |
|||
|
|||
### Создание соединения с базой данных (Engine) |
|||
|
|||
В SQLModel объект соединения `engine` (по сути это `Engine` из SQLAlchemy) **содержит пул соединений** к базе данных. |
|||
|
|||
Для обеспечения всех подключений приложения к одной базе данных нужен только один объект соединения `engine`. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *} |
|||
|
|||
Использование настройки `check_same_thread=False` позволяет FastAPI использовать одну и ту же SQLite базу данных в различных потоках (threads). Это необходимо, когда **один запрос** использует **более одного потока** (например, в зависимостях). |
|||
|
|||
Не беспокойтесь, учитывая структуру кода, мы позже позаботимся о том, чтобы использовать **отдельную SQLModel-сессию на каждый отдельный запрос**, это как раз то, что пытается обеспечить `check_same_thread`. |
|||
|
|||
### Создание таблиц |
|||
|
|||
Далее мы добавляем функцию, использующую `SQLModel.metadata.create_all(engine)`, для того, чтобы создать **таблицы** для каждой из **моделей таблицы**. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} |
|||
|
|||
### Создание зависимости Session |
|||
|
|||
Сессия базы данных (**`Session`**) хранит **объекты в памяти** и отслеживает любые необходимые изменения в данных, а затем **использует `engine`** для коммуникации с базой данных. |
|||
|
|||
Мы создадим FastAPI-**зависимость** с помощью `yield`, которая будет создавать новую сессию (Session) для каждого запроса. Это как раз и обеспечит использование отдельной сессии на каждый отдельный запрос. 🤓 |
|||
|
|||
Затем мы создадим объявленную (`Annotated`) зависимость `SessionDep`. Мы сделаем это для того, чтобы упростить остальной код, который будет использовать эту зависимость. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} |
|||
|
|||
### Создание таблиц базы данных при запуске приложения |
|||
|
|||
Мы будем создавать таблицы базы данных при запуске приложения. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *} |
|||
|
|||
В данном примере мы создаем таблицы при наступлении события запуска приложения. |
|||
|
|||
В продуктовом приложении вы, скорее всего, будете использовать скрипт для миграции базы данных, который выполняется перед запуском приложения. 🤓 |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
В SQLModel будут включены утилиты миграции, входящие в состав Alembic, но на данный момент вы просто можете использовать |
|||
<a href="https://alembic.sqlalchemy.org/en/latest/" class="external-link" target="_blank">Alembic</a> напрямую. |
|||
|
|||
/// |
|||
|
|||
### Создание героя (Hero) |
|||
|
|||
Каждая модель в SQLModel является также моделью Pydantic, поэтому вы можете использовать её при **объявлении типов**, точно также, как и модели Pydantic. |
|||
|
|||
Например, при объявлении параметра типа `Hero`, она будет считана из **тела JSON**. |
|||
|
|||
Точно также, вы можете использовать её при объявлении типа значения, возвращаемого функцией, и тогда структурированные данные будут отображены через пользовательский интерфейс автоматически сгенерированной документации FastAPI. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} |
|||
|
|||
Мы используем зависимость `SessionDep` (сессию базы данных) для того, чтобы добавить нового героя `Hero` в объект сессии (`Session`), сохранить изменения в базе данных, обновить данные героя и затем вернуть их. |
|||
|
|||
### Чтение данных о героях |
|||
|
|||
Мы можем **читать** данные героев из базы данных с помощью `select()`. Мы можем включить `limit` и `offset` для постраничного считывания результатов. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} |
|||
|
|||
### Чтение данных отдельного героя |
|||
|
|||
Мы можем прочитать данные отдельного героя (`Hero`). |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} |
|||
|
|||
### Удаление данных героя |
|||
|
|||
Мы также можем удалить героя `Hero` из базы данных. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} |
|||
|
|||
### Запуск приложения |
|||
|
|||
Вы можете запустить приложение следующим образом: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Далее перейдите в пользовательский интерфейс API `/docs`. Вы увидите, что **FastAPI** использует модели для создания документации API. Эти же модели используются для сериализации и проверки данных. |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image01.png"> |
|||
</div> |
|||
|
|||
## Добавление в приложение дополнительных (вспомогательных) моделей |
|||
|
|||
Теперь давайте проведём **рефакторинг** нашего приложения, чтобы сделать его более безопасным и более универсальным. |
|||
|
|||
Обратите внимание, что на данном этапе наше приложение позволяет на уровне клиента определять `id` создаваемого героя (`Hero`). 😱 |
|||
|
|||
Мы не можем этого допустить, т.к. существует риск переписать уже присвоенные `id` в базе данных. Присвоение `id` должно происходить **на уровне бэкэнда (backend)** или **на уровне базы данных**, но никак **не на уровне клиента**. |
|||
|
|||
Кроме того, мы создаем секретное имя `secret_name` для героя, но пока что, мы возвращаем его повсеместно, и это слабо напоминает **секретность**... 😅 |
|||
|
|||
Мы поправим это с помощью нескольких дополнительных (вспомогательных) моделей. Вот где SQLModel по-настоящему покажет себя. ✨ |
|||
|
|||
### Создание дополнительных моделей |
|||
|
|||
В **SQLModel**, любая модель с параметром `table=True` является **моделью таблицы**. |
|||
|
|||
Любая модель, не содержащая `table=True` является **моделью данных**, это по сути обычные модели Pydantic (с несколько расширенным функционалом). 🤓 |
|||
|
|||
С помощью SQLModel мы можем использовать **наследование**, что поможет нам **избежать дублирования** всех полей. |
|||
|
|||
#### Базовый класс `HeroBase` |
|||
|
|||
Давайте начнём с модели `HeroBase`, которая содержит поля, общие для всех моделей: |
|||
|
|||
* `name` |
|||
* `age` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} |
|||
|
|||
#### Модель таблицы `Hero` |
|||
|
|||
Далее давайте создадим **модель таблицы** `Hero` с дополнительными полями, которых может не быть в других моделях: |
|||
|
|||
* `id` |
|||
* `secret_name` |
|||
|
|||
Модель `Hero` наследует от `HeroBase`, и поэтому включает также поля из `HeroBase`. Таким образом, все поля, содержащиеся в `Hero`, будут следующими: |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} |
|||
|
|||
#### Публичная модель данных `HeroPublic` |
|||
|
|||
Далее мы создадим модель `HeroPublic`. Мы будем возвращать её клиентам API. |
|||
|
|||
Она включает в себя те же поля, что и `HeroBase`, и, соответственно, поле `secret_name` в ней отсутствует. |
|||
|
|||
Наконец-то личность наших героев защищена! 🥷 |
|||
|
|||
В модели `HeroPublic` также объявляется поле `id: int`. Мы как бы заключаем договоренность с API клиентом, на то, что передаваемые данные всегда должны содержать поле `id`, и это поле должно содержать целое число (и никогда не содержать `None`). |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Модель ответа, гарантирующая наличие поля со значением типа `int` (не `None`), очень полезна при разработке API клиентов. Определенность в передаваемых данных может обеспечить написание более простого кода. |
|||
|
|||
Также **автоматически генерируемые клиенты** будут иметь более простой интерфейс. И в результате жизнь разработчиков, использующих ваш API, станет значительно легче. 😎 |
|||
|
|||
/// |
|||
|
|||
`HeroPublic` содержит все поля `HeroBase`, а также поле `id`, объявленное как `int` (не `None`): |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} |
|||
|
|||
#### Модель для создания героя `HeroCreate` |
|||
|
|||
Сейчас мы создадим модель `HeroCreate`. Эта модель будет использоваться для проверки данных, переданных клиентом. |
|||
|
|||
Она содержит те же поля, что и `HeroBase`, а также поле `secret_name`. |
|||
|
|||
Теперь, при создании нового героя, клиенты будут передавать секретное имя `secret_name`, которое будет сохранено в базе данных, но не будет возвращено в ответе API клиентам. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Вот как нужно работать с **паролями**: получайте их, но не возвращайте их через API. |
|||
|
|||
Также хэшируйте значения паролей перед тем, как их сохранить. Ни в коем случае не храните пароли в открытом виде, как обычный текст. |
|||
|
|||
/// |
|||
|
|||
Поля модели `HeroCreate`: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} |
|||
|
|||
#### Модель для обновления данных героя `HeroUpdate` |
|||
|
|||
В предыдущих версиях нашей программы мы не могли обновить данные героя, теперь, воспользовавшись дополнительными моделями, мы сможем это сделать. 🎉 |
|||
|
|||
Модель данных `HeroUpdate` в некотором смысле особенная. Она содержит все те же поля, что и модель создания героя, но все поля модели являются **необязательными**. (Все они имеют значение по умолчанию.) Таким образом, при обновлении данных героя, вам достаточно передать только те поля, которые требуют изменения. |
|||
|
|||
Поскольку **все поля по сути меняются** (теперь тип каждого поля допускает значение `None` и значение по умолчанию `None`), мы должны их **объявить заново**. |
|||
|
|||
Фактически, нам не нужно наследоваться от `HeroBase`, потому что мы будем заново объявлять все поля. Я оставлю наследование просто для поддержания общего стиля, но оно (наследование) здесь необязательно. 🤷 |
|||
|
|||
Поля `HeroUpdate`: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} |
|||
|
|||
### Создание героя с помощью `HeroCreate` и возвращение результатов с помощью `HeroPublic` |
|||
|
|||
Теперь, когда у нас есть дополнительные модели, мы можем обновить те части приложения, которые их используют. |
|||
|
|||
Вместе c запросом на создание героя мы получаем объект данных `HeroCreate`, и создаем на его основе объект модели таблицы `Hero`. |
|||
|
|||
Созданный объект *модели таблицы* `Hero` будет иметь все поля, переданные клиентом, а также поле `id`, сгенерированное базой данных. |
|||
|
|||
Далее функция вернёт объект *модели таблицы* `Hero`. Но поскольку, мы объявили `HeroPublic` как модель ответа, то **FastAPI** будет использовать именно её для проверки и сериализации данных. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Теперь мы используем модель ответа `response_model=HeroPublic`, вместо того, чтобы объявить тип возвращаемого значения как `-> HeroPublic`. Мы это делаем потому, что тип возвращаемого значения не относится к `HeroPublic`. |
|||
|
|||
Если бы мы объявили тип возвращаемого значения как `-> HeroPublic`, то редактор и линтер начали бы ругаться (и вполне справедливо), что возвращаемое значение принадлежит типу `Hero`, а совсем не `HeroPublic`. |
|||
|
|||
Объявляя модель ответа в `response_model`, мы как бы говорим **FastAPI**: делай свое дело, не вмешиваясь в аннотацию типов и не полагаясь на помощь редактора или других инструментов. |
|||
|
|||
/// |
|||
|
|||
### Чтение данных героев с помощью `HeroPublic` |
|||
|
|||
Мы можем проделать то же самое **для чтения данных** героев. Мы применим модель ответа `response_model=list[HeroPublic]`, и тем самым обеспечим правильную проверку и сериализацию данных. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} |
|||
|
|||
### Чтение данных отдельного героя с помощью `HeroPublic` |
|||
|
|||
Мы можем **прочитать** данные отдельного героя: |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} |
|||
|
|||
### Обновление данных героя с помощью `HeroUpdate` |
|||
|
|||
Мы можем **обновить данные героя**. Для этого мы воспользуемся HTTP методом `PATCH`. |
|||
|
|||
В коде мы получаем объект словаря `dict` с данными, переданными клиентом (т.е. **только c данными, переданными клиентом**, исключая любые значения, которые могли бы быть там только потому, что они являются значениями по умолчанию). Для того чтобы сделать это, мы воспользуемся опцией `exclude_unset=True`. В этом главная хитрость. 🪄 |
|||
|
|||
Затем мы применим `hero_db.sqlmodel_update(hero_data)`, и обновим `hero_db`, использовав данные `hero_data`. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} |
|||
|
|||
### Удалим героя ещё раз |
|||
|
|||
Операция **удаления** героя практически не меняется. |
|||
|
|||
В данном случае желание *`отрефакторить всё`* остаётся неудовлетворенным. 😅 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} |
|||
|
|||
### Снова запустим приложение |
|||
|
|||
Вы можете снова запустить приложение: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Если вы перейдете в пользовательский интерфейс API `/docs`, то вы увидите, что он был обновлен, и больше не принимает параметра `id` от клиента при создании нового героя, и т.д. |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image02.png"> |
|||
</div> |
|||
|
|||
## Резюме |
|||
|
|||
Вы можете использовать <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">**SQLModel**</a> для взаимодействия с реляционными базами данных, а также для упрощения работы с **моделями данных** и **моделями таблиц**. |
|||
|
|||
Вы можете узнать гораздо больше информации в документации по **SQLModel**. Там вы найдете более подробное <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">мини-руководство по использованию SQLModel с **FastAPI**</a>. 🚀 |
@ -0,0 +1,83 @@ |
|||
# FastAPI CLI |
|||
|
|||
**FastAPI CLI** це програма командного рядка, яку Ви можете використовувати, щоб обслуговувати Ваш додаток FastAPI, керувати Вашими FastApi проектами, тощо. |
|||
|
|||
Коли Ви встановлюєте FastApi (тобто виконуєте `pip install "fastapi[standard]"`), Ви також встановлюєте пакунок `fastapi-cli`, цей пакунок надає команду `fastapi` в терміналі. |
|||
|
|||
Для запуску Вашого FastAPI проекту для розробки, Ви можете скористатись командою `fastapi dev`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:single">main.py</u> |
|||
<font color="#3465A4">INFO </font> Using path <font color="#3465A4">main.py</font> |
|||
<font color="#3465A4">INFO </font> Resolved absolute path <font color="#75507B">/home/user/code/awesomeapp/</font><font color="#AD7FA8">main.py</font> |
|||
<font color="#3465A4">INFO </font> Searching for package file structure from directories with <font color="#3465A4">__init__.py</font> files |
|||
<font color="#3465A4">INFO </font> Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> |
|||
|
|||
╭─ <font color="#8AE234"><b>Python module file</b></font> ─╮ |
|||
│ │ |
|||
│ 🐍 main.py │ |
|||
│ │ |
|||
╰──────────────────────╯ |
|||
|
|||
<font color="#3465A4">INFO </font> Importing module <font color="#4E9A06">main</font> |
|||
<font color="#3465A4">INFO </font> Found importable FastAPI app |
|||
|
|||
╭─ <font color="#8AE234"><b>Importable FastAPI app</b></font> ─╮ |
|||
│ │ |
|||
│ <span style="background-color:#272822"><font color="#FF4689">from</font></span><span style="background-color:#272822"><font color="#F8F8F2"> main </font></span><span style="background-color:#272822"><font color="#FF4689">import</font></span><span style="background-color:#272822"><font color="#F8F8F2"> app</font></span><span style="background-color:#272822"> </span> │ |
|||
│ │ |
|||
╰──────────────────────────╯ |
|||
|
|||
<font color="#3465A4">INFO </font> Using import string <font color="#8AE234"><b>main:app</b></font> |
|||
|
|||
<span style="background-color:#C4A000"><font color="#2E3436">╭────────── FastAPI CLI - Development mode ───────────╮</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ Serving at: http://127.0.0.1:8000 │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ API docs: http://127.0.0.1:8000/docs │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ Running in development mode, for production use: │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ </font></span><span style="background-color:#C4A000"><font color="#555753"><b>fastapi run</b></font></span><span style="background-color:#C4A000"><font color="#2E3436"> │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">╰─────────────────────────────────────────────────────╯</font></span> |
|||
|
|||
<font color="#4E9A06">INFO</font>: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] |
|||
<font color="#4E9A06">INFO</font>: Uvicorn running on <b>http://127.0.0.1:8000</b> (Press CTRL+C to quit) |
|||
<font color="#4E9A06">INFO</font>: Started reloader process [<font color="#34E2E2"><b>2265862</b></font>] using <font color="#34E2E2"><b>WatchFiles</b></font> |
|||
<font color="#4E9A06">INFO</font>: Started server process [<font color="#06989A">2265873</font>] |
|||
<font color="#4E9A06">INFO</font>: Waiting for application startup. |
|||
<font color="#4E9A06">INFO</font>: Application startup complete. |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Програма командного рядка `fastapi` це **FastAPI CLI**. |
|||
|
|||
FastAPI CLI приймає шлях до Вашої Python програми (напр. `main.py`) і автоматично виявляє екземпляр `FastAPI` (зазвичай названий `app`), обирає коректний процес імпорту, а потім обслуговує його. |
|||
|
|||
Натомість, для запуску у продакшн використовуйте `fastapi run`. 🚀 |
|||
|
|||
Всередині **FastAPI CLI** використовує <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a>, високопродуктивний, production-ready, ASGI cервер. 😎 |
|||
|
|||
## `fastapi dev` |
|||
|
|||
Використання `fastapi dev` ініціює режим розробки. |
|||
|
|||
За замовчуванням, **автоматичне перезавантаження** увімкнене, автоматично перезавантажуючи сервер кожного разу, коли Ви змінюєте Ваш код. Це ресурсо-затратно, та може бути менш стабільним, ніж коли воно вимкнене. Ви повинні використовувати його тільки під час розробки. Воно також слухає IP-адресу `127.0.0.1`, що є IP Вашого девайсу для самостійної комунікації з самим собою (`localhost`). |
|||
|
|||
## `fastapi run` |
|||
|
|||
Виконання `fastapi run` запустить FastAPI у продакшн-режимі за замовчуванням. |
|||
|
|||
За замовчуванням, **автоматичне перезавантаження** вимкнене. Воно також прослуховує IP-адресу `0.0.0.0`, що означає всі доступні IP адреси, тим самим даючи змогу будь-кому комунікувати з девайсом. Так Ви зазвичай будете запускати його у продакшн, наприклад у контейнері. |
|||
|
|||
В більшості випадків Ви можете (і маєте) мати "termination proxy", який обробляє HTTPS для Вас, це залежить від способу розгортання вашого додатку, Ваш провайдер може зробити це для Вас, або Вам потрібно налаштувати його самостійно. |
|||
|
|||
/// tip |
|||
|
|||
Ви можете дізнатись більше про це у [документації про розгортування](deployment/index.md){.internal-link target=_blank}. |
|||
|
|||
/// |
@ -0,0 +1,99 @@ |
|||
# 异步测试 |
|||
|
|||
您已经了解了如何使用 `TestClient` 测试 **FastAPI** 应用程序。但是到目前为止,您只了解了如何编写同步测试,而没有使用 `async` 异步函数。 |
|||
|
|||
在测试中能够使用异步函数可能会很有用,比如当您需要异步查询数据库的时候。想象一下,您想要测试向 FastAPI 应用程序发送请求,然后验证您的后端是否成功在数据库中写入了正确的数据,与此同时您使用了异步的数据库的库。 |
|||
|
|||
让我们看看如何才能实现这一点。 |
|||
|
|||
## pytest.mark.anyio |
|||
|
|||
如果我们想在测试中调用异步函数,那么我们的测试函数必须是异步的。 AnyIO 为此提供了一个简洁的插件,它允许我们指定一些测试函数要异步调用。 |
|||
|
|||
## HTTPX |
|||
|
|||
即使您的 **FastAPI** 应用程序使用普通的 `def` 函数而不是 `async def` ,它本质上仍是一个 `async` 异步应用程序。 |
|||
|
|||
`TestClient` 在内部通过一些“魔法”操作,使得您可以在普通的 `def` 测试函数中调用异步的 FastAPI 应用程序,并使用标准的 pytest。但当我们在异步函数中使用它时,这种“魔法”就不再生效了。由于测试以异步方式运行,我们无法在测试函数中继续使用 `TestClient`。 |
|||
|
|||
`TestClient` 是基于 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a> 的。幸运的是,我们可以直接使用它来测试API。 |
|||
|
|||
## 示例 |
|||
|
|||
举个简单的例子,让我们来看一个[更大的应用](../tutorial/bigger-applications.md){.internal-link target=_blank}和[测试](../tutorial/testing.md){.internal-link target=_blank}中描述的类似文件结构: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
文件 `main.py` 将包含: |
|||
|
|||
{* ../../docs_src/async_tests/main.py *} |
|||
|
|||
文件 `test_main.py` 将包含针对 `main.py` 的测试,现在它可能看起来如下: |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py *} |
|||
|
|||
## 运行测试 |
|||
|
|||
您可以通过以下方式照常运行测试: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pytest |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 详细说明 |
|||
|
|||
这个标记 `@pytest.mark.anyio` 会告诉 pytest 该测试函数应该被异步调用: |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py hl[7] *} |
|||
|
|||
/// tip |
|||
|
|||
请注意,测试函数现在用的是 `async def`,而不是像以前使用 `TestClient` 时那样只是 `def` 。 |
|||
|
|||
/// |
|||
|
|||
我们现在可以使用应用程序创建一个 `AsyncClient` ,并使用 `await` 向其发送异步请求。 |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py hl[9:12] *} |
|||
|
|||
这相当于: |
|||
|
|||
```Python |
|||
response = client.get('/') |
|||
``` |
|||
|
|||
我们曾经通过它向 `TestClient` 发出请求。 |
|||
|
|||
/// tip |
|||
|
|||
请注意,我们正在将 async/await 与新的 `AsyncClient` 一起使用——请求是异步的。 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
如果您的应用程序依赖于生命周期事件, `AsyncClient` 将不会触发这些事件。为了确保它们被触发,请使用 <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a> 中的 `LifespanManager` 。 |
|||
|
|||
/// |
|||
|
|||
## 其他异步函数调用 |
|||
|
|||
由于测试函数现在是异步的,因此除了在测试中向 FastAPI 应用程序发送请求之外,您现在还可以调用(和使用 `await` 等待)其他 `async` 异步函数,就和您在代码中的其他任何地方调用它们的方法一样。 |
|||
|
|||
/// tip |
|||
|
|||
如果您在测试程序中集成异步函数调用的时候遇到一个 `RuntimeError: Task attached to a different loop` 的报错(例如,使用 <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB 的 MotorClient</a> 时),请记住,只能在异步函数中实例化需要事件循环的对象,例如通过 `'@app.on_event("startup")` 回调函数进行初始化。 |
|||
|
|||
/// |
@ -0,0 +1,55 @@ |
|||
# OpenAPI 网络钩子 |
|||
|
|||
有些情况下,您可能想告诉您的 API **用户**,您的应用程序可以携带一些数据调用*他们的*应用程序(给它们发送请求),通常是为了**通知**某种**事件**。 |
|||
|
|||
这意味着,除了您的用户向您的 API 发送请求的一般情况,**您的 API**(或您的应用)也可以向**他们的系统**(他们的 API、他们的应用)**发送请求**。 |
|||
|
|||
这通常被称为**网络钩子**(Webhook)。 |
|||
|
|||
## 使用网络钩子的步骤 |
|||
|
|||
通常的过程是**您**在代码中**定义**要发送的消息,即**请求的主体**。 |
|||
|
|||
您还需要以某种方式定义您的应用程序将在**何时**发送这些请求或事件。 |
|||
|
|||
**用户**会以某种方式(例如在某个网页仪表板上)定义您的应用程序发送这些请求应该使用的 **URL**。 |
|||
|
|||
所有关于注册网络钩子的 URL 的**逻辑**以及发送这些请求的实际代码都由您决定。您可以在**自己的代码**中以任何想要的方式来编写它。 |
|||
|
|||
## 使用 `FastAPI` 和 OpenAPI 文档化网络钩子 |
|||
|
|||
使用 **FastAPI**,您可以利用 OpenAPI 来自定义这些网络钩子的名称、您的应用可以发送的 HTTP 操作类型(例如 `POST`、`PUT` 等)以及您的应用将发送的**请求体**。 |
|||
|
|||
这能让您的用户更轻松地**实现他们的 API** 来接收您的**网络钩子**请求,他们甚至可能能够自动生成一些自己的 API 代码。 |
|||
|
|||
/// info |
|||
|
|||
网络钩子在 OpenAPI 3.1.0 及以上版本中可用,FastAPI `0.99.0` 及以上版本支持。 |
|||
|
|||
/// |
|||
|
|||
## 带有网络钩子的应用程序 |
|||
|
|||
当您创建一个 **FastAPI** 应用程序时,有一个 `webhooks` 属性可以用来定义网络钩子,方式与您定义*路径操作*的时候相同,例如使用 `@app.webhooks.post()` 。 |
|||
|
|||
{* ../../docs_src/openapi_webhooks/tutorial001.py hl[9:13,36:53] *} |
|||
|
|||
您定义的网络钩子将被包含在 `OpenAPI` 的架构中,并出现在自动生成的**文档 UI** 中。 |
|||
|
|||
/// info |
|||
|
|||
`app.webhooks` 对象实际上只是一个 `APIRouter` ,与您在使用多个文件来构建应用程序时所使用的类型相同。 |
|||
|
|||
/// |
|||
|
|||
请注意,使用网络钩子时,您实际上并没有声明一个*路径*(比如 `/items/` ),您传递的文本只是这个网络钩子的**标识符**(事件的名称)。例如在 `@app.webhooks.post("new-subscription")` 中,网络钩子的名称是 `new-subscription` 。 |
|||
|
|||
这是因为我们预计**您的用户**会以其他方式(例如通过网页仪表板)来定义他们希望接收网络钩子的请求的实际 **URL 路径**。 |
|||
|
|||
### 查看文档 |
|||
|
|||
现在您可以启动您的应用程序并访问 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
|||
|
|||
您会看到您的文档不仅有正常的*路径操作*显示,现在还多了一些**网络钩子**: |
|||
|
|||
<img src="/img/tutorial/openapi-webhooks/image01.png"> |
Loading…
Reference in new issue