Базовые операции с базой данных в веб-приложении

Последнее обновление: 07.09.2022

В прошлой теме были рассмотрены базовые операции с базой данных. Рассмотрим, как это будет выглядеть в рамках веб-приложения на простейшем примере.

В итоге получится следующий проект:

Базовые операции с базой данных SQLite через SqlAlchemy в веб-приложении на FastAPI и Python

Определение подключения к базе данных и моделей

Для хранения всей инфраструктуры для работы с базой данных в папке проекта определим файл 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, которая загрузит список объектов:

взаимодействие javascript с Web API и базой данных SQLite через SqlAlchemy на FastAPI и Python

После этого мы сможем выполнять все базовые операции с пользователями - получение, добавление, изменение, удаление. Например, добавим нового пользователя:

взаимодействие с Web API REST и базой данных SQLite через SqlAlchemy в веб-приложении на FastAPI и Python
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850