В прошлой теме были рассмотрены базовые операции с базой данных. Рассмотрим, как это будет выглядеть в рамках веб-приложения на простейшем примере.
В итоге получится следующий проект:
Для хранения всей инфраструктуры для работы с базой данных в папке проекта определим файл database.py со следующим кодом:
from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import sessionmaker from sqlalchemy import Column, Integer, String from fastapi import FastAPI SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) class Base(DeclarativeBase): pass class Person(Base): __tablename__ = "people" id = Column(Integer, primary_key=True, index=True) name = Column(String) age = Column(Integer,) SessionLocal = sessionmaker(autoflush=False, bind=engine)
Итак, в данном случае мы будем подключаться к базе данных SQLite, которая будет создаваться в папке проекта.
Хранимые в бд данные будут представлены классом Person с тремя атрибутами: id, name и age.
В файле main.py определим следующий код:
from database import * from sqlalchemy.orm import Session from fastapi import Depends, FastAPI, Body from fastapi.responses import JSONResponse, FileResponse # создаем таблицы Base.metadata.create_all(bind=engine) app = FastAPI() # определяем зависимость def get_db(): db = SessionLocal() try: yield db finally: db.close() @app.get("/") def main(): return FileResponse("public/index.html") @app.get("/api/users") def get_people(db: Session = Depends(get_db)): return db.query(Person).all() @app.get("/api/users/{id}") def get_person(id, db: Session = Depends(get_db)): # получаем пользователя по id person = db.query(Person).filter(Person.id == id).first() # если не найден, отправляем статусный код и сообщение об ошибке if person==None: return JSONResponse(status_code=404, content={ "message": "Пользователь не найден"}) #если пользователь найден, отправляем его return person @app.post("/api/users") def create_person(data = Body(), db: Session = Depends(get_db)): person = Person(name=data["name"], age=data["age"]) db.add(person) db.commit() db.refresh(person) return person @app.put("/api/users") def edit_person(data = Body(), db: Session = Depends(get_db)): # получаем пользователя по id person = db.query(Person).filter(Person.id == data["id"]).first() # если не найден, отправляем статусный код и сообщение об ошибке if person == None: return JSONResponse(status_code=404, content={ "message": "Пользователь не найден"}) # если пользователь найден, изменяем его данные и отправляем обратно клиенту person.age = data["age"] person.name = data["name"] db.commit() # сохраняем изменения db.refresh(person) return person @app.delete("/api/users/{id}") def delete_person(id, db: Session = Depends(get_db)): # получаем пользователя по id person = db.query(Person).filter(Person.id == id).first() # если не найден, отправляем статусный код и сообщение об ошибке if person == None: return JSONResponse( status_code=404, content={ "message": "Пользователь не найден"}) # если пользователь найден, удаляем его db.delete(person) # удаляем объект db.commit() # сохраняем изменения return person
Разберем в общих чертах этот код. Прежде всего вначале создаем все необходимые таблицы в базе данных (если они отстуствуют)
Base.metadata.create_all(bind=engine)
Предпочтительным способом передачи сессии базы данных в функции обработки запроса представляет внедрение сессии бд в качестве зависимости.
Поэтому далее создаем функцию get_db()
, через которую объект сессии базы данных будет передаваться в функцию обработки:
def get_db(): db = SessionLocal() try: yield db finally: db.close()
Здесь сначала создаем объект сессии базы данных. Затем в конструкции try..finally с помощью оператора yield возвращаем созданный объект. Таким образом, данный объект будет внедрен в функцию обработки запроса. Выражение yield будет выполняться при получении каждого нового запроса.
Помещение выражение yield в блок try позволяет получить и обработать любую ошибку, возникшую в процессе взаимодействия с базой данных.
После завершения операций с базой данных выполняется блок finally, в котором закрывается подключение к базе данных с помощью метода close()
При обращении к корню веб-приложения, то есть по пути "/", оно будет отправлять в ответ файл index.html
, то есть веб-страницу, посредством которой
пользователь сможет взаимодействовать с сервером:
@app.get("/") def main(): return FileResponse("public/index.html")
Далее определяются функции, которые собственно и представляют API. Вначале определяется функция, которая обрабатывает запрос типа GET по пути "api/users":
@app.get("/api/users") def get_people(db: Session = Depends(get_db)): return db.query(Person).all()
C помощью класса Depends() в функцию передается результат функции get_db, то есть сессия базы данных, который передается параметру db
. И через этот параметр
мы сможем взаимодействовать с базой данных. Подобным образом сессия базы данных внедряется во все остальные функции-обработчики запросов.
Запрос GET предполагает получение объектов, и в данном случае с помощью метода db.query(Person).all()
получаем список объектов Person, который отправляется клиенту.
Когда клиент обращается к приложению для получения одного объекта по id в запрос типа GET по адресу "api/users/{id}", то срабатывает другая функция:
@app.get("/api/users/{id}") def get_person(id, db: Session = Depends(get_db)): person = db.query(Person).filter(Person.id == id).first() if person==None: return JSONResponse(status_code=404, content={ "message": "Пользователь не найден"}) return person
Здесь через параметр id получаем из пути запроса идентификатор объекта Person и по этому идентификатору ищем нужный объект в базе данных.
Если объект по id не был найден, то возвращаем с помощью класса JSONResponse
статусный код 404 с некоторым сообщением в формате JSON.
Если объект найден, то отправляем найденный объект клиенту.
При получении запроса типа DELETE по маршруту "/api/users/{id}" срабатывает другая функция:
@app.delete("/api/users/{id}") def delete_person(id, db: Session = Depends(get_db)): person = db.query(Person).filter(Person.id == id).first() if person == None: return JSONResponse( status_code=404, content={ "message": "Пользователь не найден"}) db.delete(person) db.commit() return person
Здесь действует аналогичная логика - если объект по id не найден, отправляет статусный код 404. Если же объект найден, то удаляем его из базы данных и посылаем клиенту.
При получении запроса с методом POST по адресу "/api/users" срабатывает следующая функция:
@app.post("/api/users") def create_person(data = Body(), db: Session = Depends(get_db)): person = Person(name=data["name"], age=data["age"]) db.add(person) db.commit() db.refresh(person) return person
Запрос типа POST предполагает передачу приложению отправляемых данных. Причем мы ожидаем, что клиент отправит данные, которые содержат значения name и age. Для краткости опустим
валидацию входных данных. И для получения данных из тела запроса с помощью класса Body получаем данные в параметр data и затем используем данные из этого параметра для создания объекта Person.
Затем созданный объект добавляем в базу данных, с помощью метода db.refresh()
обновляем его состояние и отправляем назад клиенту.
Если приложению приходит PUT-запрос по адресу "/api/users", то аналогичным образом получаем отправленные клиентом данные в виде объекта Person и пытаемся найти подобный объект в базе данных. Если объект не найден, отправляем статусный код 404. Если объект найден, то изменяем его данные, обновляем его состояние и отправляем обратно клиенту:
@app.put("/api/users") def edit_person(data = Body(), db: Session = Depends(get_db)): person = db.query(Person).filter(Person.id == data["id"]).first() if person == None: return JSONResponse(status_code=404, content={ "message": "Пользователь не найден"}) person.age = data["age"] person.name = data["name"] db.commit() # сохраняем изменения db.refresh(person) return person
Таким образом, мы определили простейший API. Теперь добавим код клиента.
Теперь в проекте определим папку public, в которую добавим новый файл index.html
Определим в файле index.html следующим код для взаимодействия с сервером FastAPI:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> <style> td {padding:5px;} button{margin: 5px;} </style> </head> <body> <h2>Список пользователей</h2> <div> <input type="hidden" id="userId" /> <p> Имя:<br/> <input id="userName" /> </p> <p> Возраст:<br /> <input id="userAge" type="number" /> </p> <p> <button id="saveBtn">Сохранить</button> <button id="resetBtn">Сбросить</button> </p> </div> <table> <thead><tr><th>Имя</th><th>Возраст</th><th></th></tr></thead> <tbody> </tbody> </table> <script> // Получение всех пользователей async function getUsers() { // отправляет запрос и получаем ответ const response = await fetch("/api/users", { method: "GET", headers: { "Accept": "application/json" } }); // если запрос прошел нормально if (response.ok === true) { // получаем данные const users = await response.json(); const rows = document.querySelector("tbody"); // добавляем полученные элементы в таблицу users.forEach(user => rows.append(row(user))); } } // Получение одного пользователя async function getUser(id) { const response = await fetch(`/api/users/${id}`, { method: "GET", headers: { "Accept": "application/json" } }); if (response.ok === true) { const user = await response.json(); document.getElementById("userId").value = user.id; document.getElementById("userName").value = user.name; document.getElementById("userAge").value = user.age; } else { // если произошла ошибка, получаем сообщение об ошибке const error = await response.json(); console.log(error.message); // и выводим его на консоль } } // Добавление пользователя async function createUser(userName, userAge) { const response = await fetch("api/users", { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); document.querySelector("tbody").append(row(user)); } else { const error = await response.json(); console.log(error.message); } } // Изменение пользователя async function editUser(userId, userName, userAge) { const response = await fetch("api/users", { method: "PUT", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); document.querySelector(`tr[data-rowid='${user.id}']`).replaceWith(row(user)); } else { const error = await response.json(); console.log(error.message); } } // Удаление пользователя async function deleteUser(id) { const response = await fetch(`/api/users/${id}`, { method: "DELETE", headers: { "Accept": "application/json" } }); if (response.ok === true) { const user = await response.json(); document.querySelector(`tr[data-rowid='${user.id}']`).remove(); } else { const error = await response.json(); console.log(error.message); } } // сброс данных формы после отправки function reset() { document.getElementById("userId").value = document.getElementById("userName").value = document.getElementById("userAge").value = ""; } // создание строки для таблицы function row(user) { const tr = document.createElement("tr"); tr.setAttribute("data-rowid", user.id); const nameTd = document.createElement("td"); nameTd.append(user.name); tr.append(nameTd); const ageTd = document.createElement("td"); ageTd.append(user.age); tr.append(ageTd); const linksTd = document.createElement("td"); const editLink = document.createElement("button"); editLink.append("Изменить"); editLink.addEventListener("click", async() => await getUser(user.id)); linksTd.append(editLink); const removeLink = document.createElement("button"); removeLink.append("Удалить"); removeLink.addEventListener("click", async () => await deleteUser(user.id)); linksTd.append(removeLink); tr.appendChild(linksTd); return tr; } // сброс значений формы document.getElementById("resetBtn").addEventListener("click", () => reset()); // отправка формы document.getElementById("saveBtn").addEventListener("click", async () => { const id = document.getElementById("userId").value; const name = document.getElementById("userName").value; const age = document.getElementById("userAge").value; if (id === "") await createUser(name, age); else await editUser(id, name, age); reset(); }); // загрузка пользователей getUsers(); </script> </body> </html>
Основная логика здесь заключена в коде javascript. При загрузке страницы в браузере получаем все объекты из БД с помощью функции getUsers()
:
async function getUsers() { // отправляет запрос и получаем ответ const response = await fetch("/api/users", { method: "GET", headers: { "Accept": "application/json" } }); // если запрос прошел нормально if (response.ok === true) { // получаем данные const users = await response.json(); const rows = document.querySelector("tbody"); // добавляем полученные элементы в таблицу users.forEach(user => rows.append(row(user))); } }
Для добавления строк в таблицу используется функция row()
, которая возвращает строку. В этой строке будут определены ссылки для изменения и удаления пользователя.
Ссылка для изменения пользователя с помощью функции getUser()
получает с сервера выделенного пользователя:
async function getUser(id) { const response = await fetch(`/api/users/${id}`, { method: "GET", headers: { "Accept": "application/json" } }); if (response.ok === true) { const user = await response.json(); document.getElementById("userId").value = user.id; document.getElementById("userName").value = user.name; document.getElementById("userAge").value = user.age; } else { // если произошла ошибка, получаем сообщение об ошибке const error = await response.json(); console.log(error.message); // и выводим его на консоль } }
И выделенный пользователь добавляется в форму над таблицей. Эта же форма применяется и для добавления объекта. С помощью скрытого поля, которое хранит id пользователя, мы можем узнать, какое действие выполняется - добавление или редактирование. Если id не установлен (равен пустой строке), то выполняется функция createUser, которая отправляет данные в POST-запросе:
async function createUser(userName, userAge) { const response = await fetch("api/users", { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); document.querySelector("tbody").append(row(user)); } else { const error = await response.json(); console.log(error.message); } }
Если же ранее пользователь был загружен на форму, и в скрытом поле сохранился его id, то выполняется функция editUser, которая отправляет PUT-запрос:
async function editUser(userId, userName, userAge) { const response = await fetch("api/users", { method: "PUT", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ id: userId, name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); document.querySelector(`tr[data-rowid='${user.id}']`).replaceWith(row(user)); } else { const error = await response.json(); console.log(error.message); } }
И функция deleteUser()
посылает на сервер запрос типа DELETE на удаление пользователя, и при успешном удалении на сервере
удаляет объект по id из списка объектов Person.
Теперь запустим проект, и по умолчанию приложение отправит браузеру веб-страницу index.html, которая загрузит список объектов:
После этого мы сможем выполнять все базовые операции с пользователями - получение, добавление, изменение, удаление. Например, добавим нового пользователя: