Для работы с базой данных в Django существует понятие ORM. Само понятие ORM обозначает возможность преобразования и взаимодействия прикладного языка (в нашем случае Python) и базы данных (обычно это SQL). В любой реализации ORM есть 4 ключевых понятия, которые складываются в аббревиатуру CRUD:
- Create - создание или добавление данных в базу;
- Read - получение и чтение данных из базы;
- Update - обновление данных в базе;
- Delete - удаление данных.
Если совсем упрощать, то ORM - это программа, которая снимает с нас обязанность знать язык запросов (например SQL). В этой статье, мы рассмотрим как делается CRUD в Django и создадим простое приложение.
Подготовка проекта
У вас уже должен быть установлен Django 2-ой или 3-ей версии. Создадим тестовый проект:
django-admin startproject crud_project
cd crud_project/
Создадим приложение на котором будут показаны примеры с CRUD:
python manage.py startapp crud_app
Добавим в файл конфигурации 'settings.py', в массив 'INSTALLED_APPS', следующую строку:
vim crud_project/settings.py
'crud_app.apps.CrudAppConfig'
Работа с моделью
Работа ORM, в Django, частично реализована в моделях. Модель - это общее название классов созданных вами и наследуемых от django.db.models.Model. Класс 'django.db.models.Model' имеет некоторые методы работы с базой и их вы наследуете. Каждая модель, обычно, соответствует одной таблице в базе данных. Модели прописываются в файле 'models.py' у каждого из приложений отдельно. Откроем этот файл в нашем приложении:
vim crud_app/models.py
В этом файле мы создадим два класса-модели, которые опишут таблицу со студентами их группами:
class StudentModel(models.Model):
name = models.CharField(max_length=80)
sex = models.CharField(max_length=15)
age = models.IntegerField()
group = models.ForeignKey(to='GroupModel', on_delete=models.CASCADE)
def __str__(self):
return self.name
class GroupModel(models.Model):
name = models.CharField(max_length=10)
def __str__(self):
return self.name
В модели есть понятие полей (Field), которое определяется в переменной (например name). Каждое такое поле соответствует колонке в базе данных. Поле определяет какой тип данных в нем может находится, в нашем случае:
- CharField - строка ограниченной длины;
- IntegerField - целое число;
- ForeignKey - колонка для связи двух таблиц.
Кроме этого, в SQL базах данных вы обязаны определять дополнительные параметры для разных типов данных. Это может быть максимальная длина строки (max_length) или что случится с данными, если родительский объект будет удален (on_delete). Таких типов данных множество и какие-то автоматических заполняются ORM.
Что бы созданная нами модель появилась в базе - мы должны выполнить 2 действия:
- Создать SQL код;
- Применить SQL код на базе.
Это делается самим ORM при выполнении следующих команд:
python manage.py makemigrations
python manage.py migrate
Попробуем добавить данные в базу через ORM. Один из способов сделать это - использовать скрипт, который идет вместе с Django, под названием shell. Он работает как обычный IDE Python, но дополнительно подтягивает зависимости Django. Что бы его использовать выполните следующую команду:
python manage.py shell
Импортируйте созданные модели:
from crud_app.models import StudentModel, GroupModel
Create
Есть два способа добавить данные в базу. Первый - это использовать метод .save(). В примере ниже мы создадим группу для студентов:
# создаем объект (новую строку в базе)
group = GroupModel()
# присваиваем значение полю (колонке)
group.name = "A101"
# формируем и выполняем SQL запрос
group.save()
Если вы не выполните метод save() в примере выше, то данные не будут добавлены в базу. Из-за этого я чаще использую метод create(), который делает все сам:
group = GroupModel.objects.create(name="A102")
Так как переменная group, в обоих случаях, хранит ссылку на какой-то объект - мы можем продолжать изменять у него значения:
group.name = "A103"
Мы должны вызвать метод .save() еще раз, что бы это новое значение сохранилось:
group.save()
Read
Получение данных выполняется через метод get(). В нем мы должны указать минимум одно обязательное поле, по которому будем выполнять поиск. Пример получения группы по имени:
GroupModel.objects.get(name="A102")
Метод get сработает, если будет найден только один экземпляр строки по соответствующему значению. Если таких результатов будет найдено больше одного - вы увидите ошибку:
- crud_app.models.GroupModel.MultipleObjectsReturned: get() returned more than one GroupModel -- it returned 2!
Кроме колонок, которые вы создавали собственноручно Django автоматически создает колонку с идентификаторами (ID). Эта колонка, если не указано иное, всегда является первичным ключом (PK) (уникально идентифицирует строку) и по ней создается кластерный индекс. Получить значение этой строки (если вы не меняли первичный ключ сами) можно двумя способами:
group = GroupModel.objects.get(name="A102")
# Получаем идентификатор строки базы данных
group.id
# Получаем первичный ключ строки
group.pk
В Django можно использовать дополнительные операторы, которые будут использоваться для поиска и получения данных. Среди таких операторов:
- gt - значение слева больше чем справа;
- lt - значение слева меньше чем справа;
- gte - значение слева больше или равно правому;
- lte - значение слева меньше или равно правому;
- in - поиск в массиве или другом итерируемом объекте;
- startswith - строка начинается с указанных символов.
Это не все операторы, которые вы можете использовать. Все операторы вы можете посмотреть в документации Django. Каждый такой оператор сопровождается двумя нижними подчеркиваниями "__". Пример такого поиска:
# Ищем значения где PK меньше чем 2
GroupModel.objects.get(pk__lt=2)
# Ищем значения, которые начинаются со строки A102
GroupModel.objects.get(name__startswith="A102")
Можно искать строку по нескольким полям сразу:
GroupModel.objects.get(pk__lt=2, name="A101")
Кроме поиска значения в единичном экземпляре - мы так же можем искать группу значений из базы используя метод .filter():
GroupModel.objects.filter(pk__gt=0, pk__lt=5)
Возвращаемы тип данных называется QuerySet. Этот итерируемый объект и мы можем использовать индексы для вывода отдельных записей:
for el in GroupModel.objects.filter(pk__gt=0, pk__lt=5):
print(el.name)
Вывод всех значений в таблице делается через метод all():
GroupModel.objects.all()
Update
Мы можем обновить колонку у одной строки используя раннее описанные методы:
group = GroupModel.objects.get(name="A102")
# Выводим текущее значение
group.name
# Присваиваем новое значение
group.name = "A103"
# Сохраняем в базу
group.save()
# Выводим новое значение
group.name
Можно обновить значение у множества объектов используя update(). В следующем примере мы поменяем name у всех найденных объектов:
groups = GroupModel.objects.filter(id__gte=2)
groups.update(name="103A")
Еще одна разница между save() и update() в том, что в первом случае будут обновлены все поля (не важно меняли ли вы их). Для метода save() существует дополнительный параметр update_fields, который помогает избежать этого.
Существуют и другие методы обновления и создания записей, таких как update_or_create (обновляем запись, если она существует или создаем новую) или get_or_create (если записи нет, то она будет создана).
Delete
Метод delete() удаляет запись в базе данных. Он работает как для одного объекта так и для множества:
GroupModel.objects.get(name="A101").delete()
GroupModel.objects.filter(name__startswith="A").delete()
Создание и получение записи со связью ForeignKey
Наша таблица 'StudentModel' связана с таблицей 'GroupModel'. В качестве связующей колонки выступает 'group'. Что бы добавить студента и связать его с группой мы должны выполнить несколько действий:
- Получить нужный объект из GroupModel;
- Добавить студента, где поле 'group' будет ссылаться на найденный объект.
Предыдущие шаги на примере:
# Получили объект группы
group = GroupModel.objects.get(name="A101")
# создали студента
student = StudentModel.objects.create(
name="Alexa Pirojkova",
sex="Female",
age="19",
# указали в какой группе он находится
group=group
)
Что бы найти студентов, которые находятся в определенной группе так же используется два нижних подчеркивания. В примере ниже мы указываем 'group' как часть 'StudentModel' и 'name', как поле в таблице 'GroupModel':
StudentModel.objects.get(group__name="A101")
StudentModel.objects.filter(group__name="A101")
StudentModel.objects.get(group_id__lt=10, id__gt=0)
Всех студентов, принадлежащих определенной группе, можно получить и обратным путем. Имея объект принадлежащий модели GroupModel, мы можем увидеть метод "_set" возвращающий студентов:
group = GroupModel.objects.get(id=1)
dir(group)
group.studentmodel_set.all()
Вывод SQL запросов
При необходимости вы можете вывести SQL запрос применяемый на базе данных. Это можно сделать использовав .query и только для типов данных QuerySet (массивов с объектами):
print(GroupModel.objects.all().query)
print(GroupModel.objects.filter(id__gt=0).query)
Следующий способ, соответственно, вернет ошибку т.к. возвращает не QuerySet:
group = GroupModel.objects.get(id=1)
group.query
- AttributeError: 'GroupModel' object has no attribute 'query'
Что бы выводить SQL запрос для всех случаев можно использовать логирование, debug toolbar и другие способы.
Создание CRUD приложения
Наше приложение будет состоять нескольких частей:
- Формы, с помощью которой можно добавлять студентов и обновлять данные уже у существующих;
- Страницы для создания студентов, на основе формы;
- Страницы для просмотра полной информации об одном студенте;
- Страницы с выводом всех студентов
- Страницы с возможностью обновления данных об одном студенте на основе формы;
- Страницы удаления.
Реализуем эти задачи за счет простых функций ( function based views). В Django так же существуют "class based views", которые еще больше сокращают шаги описанные ниже, но они рассматриваться не будут.
Форма
В Django есть несколько способов создать форму. Если вам нужна форма, которая будет соответствовать полям модели, то можно использовать класс ModelForm. В этом случае мы должны указать только название модели и поля, которые хотим выводить конечным пользователям.
Для создания форм принято создавать новый файл под названием 'forms.py' в папке того приложения, над которым вы работаете. В нашем случае это будет путь:
crud_app\forms.py
В этом файле мы импортируем класс модели и формы и напишем код:
from .models import StudentModel
from django.forms import ModelForm
class StudentForm(ModelForm):
class Meta:
# Названием модели на основе которой
# нужно создать форму.
model = StudentModel
# Поля модели, которые нужно вывести
fields = ('name', 'sex', 'age', 'group')
Страница создания студентов
Страницы сайта реализуются за счет функции или класса (возвращает данные пользователю), адреса (по которому можно обратится пользователю) и шаблона (разметка HTML). Функции и классы определяются в views.py, адреса в urls.py, а шаблоны в папке templates.
Функцию, которая будет создавать студентов, назовем create_view. Эта функция будет использовать следующие возможности:
- request - объект, который возвращается от пользователя. В этом объекте хранится введенные пользователем данные, тип запроса и другая информация;
- form - хранит в себе объект формы или распарсенные данные из request;
- is_valid() - функция, которая проверяет что все данные, введенными пользователями, соответствуют тому, что находится в модели (базе);
- .save() - сохранение нового студента.
Сама функция будет выглядеть так:
from .models import StudentModel
from .forms import StudentForm
from django.shortcuts import render, redirect
def create_view(request):
# Проверяем, что запрос на
# добавление студента (POST)
# или просто на получение формы
if request.method == 'POST':
# Получаем из запроса только те данные
# которые использует форма
form = StudentForm(request.POST)
# Проверяем правильность введенных данных
if form.is_valid():
# сохраняем в базу
form.save()
# переадресуем на главную страницу
return redirect('/')
else:
form =StudentForm()
context = {
'form': form
}
return render(request, 'create.html', context)
Создадим шаблон, который будет отображать данные из функции по следующему пути:
vim crud_app/templates/create.html
В этот файл поместите следующую разметку:
<form method = "post">
{% csrf_token %}
{{ form.as_p }}
<input type = "submit" value = "submit">
</form>
Определить адрес, по которому пользователь сможет открыть эту форму, можно в urls.py:
vim crud_project/urls.py
В этот файл добавьте следующий код:
from django.contrib import admin
from django.urls import path
from crud_app.views import *
urlpatterns = [
path('admin/', admin.site.urls),
path('create/', create_view),
]
Теперь можно запустить сервер и открыть страницу для проверки работы формы:
python manage.py runserver
Если заполнить формы и нажать 'submit' студент будет создан, но появится ошибка т.к. перенаправление выполняется на несуществующий адрес. Мы его создадим далее.
Страницы с выводом студентов
Мы создадим 2 страницы, которые будут выводить студентов. Первая страница, с названием student_detail_view, вернет информацию об одном студенте. Функция student_view- вернет список студентов.
Метод Http404, который используется ниже, вернет ошибку если указанный id не будет найден:
from django.http import Http404
# ...
def student_view(request):
# Получаем всех студентов
dataset = StudentModel.objects.all()
return render(request, 'listview.html', {'dataset': dataset})
def student_detail_view(request, id):
try:
# Получаем студента по-определенному id
data = StudentModel.objects.get(id=id)
except StudentModel.DoesNotExist:
raise Http404('Такого студента не существует')
return render(request, 'detailview.html', {'data': data})
Создадим файл шаблона, для функции student_detail_view, по следующему пути:
vim crud_app/templates/detailview.html
С содержимым:
<h3>Name: {{ data.name }}</h3><br>
<h3>Sex: {{ data.sex}}</h3><br>
<h3>Age: {{ data.age }}</h3><br>
<h3>Group: {{ data.group.name }}</h3><br>
<hr/>
И еще один файл, для функции student_view, по пути:
vim crud_app/templates/listview.html
С содержимым:
{% for data in dataset %}
{{ data }}
<hr>
{% endfor %}
Добавим 2 ссылки в следующий файл:
vim crud_project/urls.py
path('', student_view),
path('<int:id>/', student_detail_view),
Теперь, открывая главную страницу, вы увидите всех студентов:
Если добавить к странице идентификатор, откроем конкретного студента:
Страница обновления студента
Функцию, с помощью которой сможем обновить данные студента назовем update_view. Если от пользователя приходит запрос GET, то значит ему нужно вернуть текущую информацию о студенте. Если запрос типа POST, то пользователь прислал уже обновленные данные и их нужно сохранить:
from django.shortcuts import render, redirect, get_object_or_404
...
def update_view(request, id):
try:
old_data = get_object_or_404(StudentModel, id=id)
except Exception:
raise Http404('Такого студента не существует')
# Если метод POST, то это обновленные данные студента
# Остальные методы - возврат данных для изменения
if request.method =='POST':
form = StudentForm(request.POST, instance=old_data)
if form.is_valid():
form.save()
return redirect(f'/{id}')
else:
form = StudentForm(instance = old_data)
context ={
'form':form
}
return render(request, 'update.html', context)
Мы так же должны создать файл шаблона по следующему пути:
vim crud_app/templates/update.html
Со следующим содержимым:
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="update">
</form>
В файл urls.py поместим:
vim crud_project/urls.py
path('update/<int:id>/', update_view),
Запустив сервер и открыв страницу обновления можно будет увидеть заполненную форму с возможностью обновления данных:
Страница удаления
Функция удаления пользователя будет выглядеть следующим образом:
def delete_view(request, id):
try:
data = get_object_or_404(StudentModel, id=id)
except Exception:
raise Http404('Такого студента не существует')
if request.method == 'POST':
data.delete()
return redirect('/')
else:
return render(request, 'delete.html')
Создадим файл шаблона:
vim crud_app/templates/delete.html
С содержимым:
<form method="post">
{% csrf_token %}
Подтвердите удаление студента
<input type = "submit" value="Да">
<a href='/'>Нет</a>
</form>
В файл urls добавьте:
path('delete/<int:id>/', delete_view),
При запуске сервера эта страница будет выглядеть так:
...