Django: пример создания приложения — часть 2: создание и работа с database API

Автор: | 05/12/2015
 

django_logo_2

Начало — Django: пример создания приложения — часть 1: создание запуск проекта

Создание моделей

Теперь, когда ваш проект настроен и работает — вы можете приступать непосредственно к разработке.

Django поставляется с утилитой, которая автоматически генерирует базовую структуру директорий приложений, позволяя вам сфокусироваться на написании кода, вместо того того что бы заниматься созданием каталогов.

Проект vs приложение

Какая разница между проектом (project) и приложением (application)? Приложение — это некоторая доступная из интернета часть, которая выполняет какой-то функционал, например — веблог-система, база данных публичных записей, или простая система опросов. Проект — это совокупность настроек и приложений веб-сайта. Проект может содержать несколько приложений, а приложение — может работать в нескольких проектах.

Ваше приложение может располагаться в любом каталоге, который находится в вашем Python path. В этом руководстве — мы создадим его в  том же каталоге, где расположен файл manage.py, так что его можно будет импортировать как модуль верхнего уровня, а не как подмодуль проекта mysite.

Что бы создать приложение — убедитесь, что вы находитесь в одном каталоге с файлом manage.py, и выполните такую команду:

$ ls -l
total 8
-rwxrwxr-x 1 setevoy setevoy  249 May  6 16:41 manage.py
drwxrwxr-x 2 setevoy setevoy 4096 May  6 18:44 mysite
$ python manage.py startapp polls

Она создаст директорию polls, которая будет выглядеть так:

$ tree -L 2 polls/
polls/
├── admin.py
├── __init__.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

Каталог polls будет является корневой директорией вашего приложения polls.

Первый шаг в создании приложения с базой данных — это описать ваши модели (models). Модельэто по сути схема базы данных, с некоторым дополнительными данными.

Философия

Модель — это основной источник информации о ваших данных. Она содержит необходимые поля, и описание поведения данных, которые хранятся в базе. Django следует философии DRY Principle. Цель состоит в том, что бы хранить модель ваших данных в одном месте, и автоматически получать данные из неё.

В нашем просто приложении-опросе мы создадим две модели — Question и ChoiceQuestion будет содержать вопрос и дату публикации. Choice будет иметь два поля — текст вопроса, и счётчик голосов. Каждый Choice будет ассоциирован с Question.

Всё это будет представлено в простых классах Python. Отредактируйте файл polls/models.py, что бы он выглядел так:

from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

class Choice(models.Model):
    question = models.ForeignKey(Question)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

Тут всё очень просто. Каждая модель представляет собой подкласс класса django.db.models.Model. Каждая модель содержит несколько переменных, которые в модели представляют поле в базе данных.

Каждое поле является экземпляром класса Field, например — CharField для текстовых полей, и DateTimeField для полей даты-времени. Так Django указывается какой тип данных в каком поле будет храниться.

Имя каждого объекта Field (например — question_text или pub_date) — это имя поля. Вы будете использовать его в вашем коде Python, а ваша база данных будет его использовать как имя колонки.

Некоторые классы Field требуют обязательных аргументов. К примеру, CharField требует указания max_length.

Кроме того, Field может иметь несколько опциональных аргументов. В данном случае — для votes мы установили значение по умолчанию default=0.

Обратите внимание, что мы установили связи через ForeignKey. Это указывает Django, что каждый Choice относится к одному Question. Django поддерживает все распространённые связи баз данных: many-to-one, many-to-many и one-to-one.

Активация моделей

Этот небольшой объём кода даёт Djnago много информации. Используя его Django сможет:

  • создать схему базы данных (операторы CREATE TABLE) для приложения;
  • создать Python API для доступа к базе данных и объектам Question и Choice в ней.

Но для начала — мы должны указать нашему проекту, что приложение polls установлено.

Философия

Приложения Django — подключаемые: вы можете использовать одно приложение в нескольких проектах, и вы можете распространять ваши приложения, так как они не привязаны к конкретной установке Django.

Отредактируйте файл mysite/settings.py, и измените список INSTALLED_APPS, добавив в него 'polls', что бы получить такой результат:

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'polls',
)

Теперь — Django будет знать, что требуется включить приложение polls. Давайте выполним ещё одну команду:

$ python manage.py makemigrations polls
Migrations for 'polls':
  0001_initial.py:
    - Create model Choice
    - Create model Question
    - Add field question to choice

Выполнив makemigrations — вы указываете Django, что вы сделали какие-то изменения в ваших моделях (в данном случае — вы добавили новые), и вы хотите, что бы они были сохранены как миграции (migrations).

Migrations — это то, как Django сохраняет изменения в ваших моделях (и, соответственно, в базе данных), и являются просто файлами на диске. Вы можете прсомотреть файл миграции, который расположен в polls/migrations и называется 0001_initial.py — но вам не требуется делать это каждый раз.

Так же есть команда, которая запускает миграцию и настраивает вашу базу данных, она называется migrate, и мы её очень скоро используем. А пока — давайте посмотрим, какие именно SQL-запросы будут выполнены при миграции. Команда sqlmigrate принимает аргументом имя файла миграции, и возвращает её в виде SQL:

$ python manage.py sqlmigrate polls 0001
BEGIN;
CREATE TABLE `polls_choice` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `choice_text` varchar(200) NOT NULL, `votes` integer NOT NULL);
CREATE TABLE `polls_question` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `question_text` varchar(200) NOT NULL, `pub_date` datetime NOT NULL);
ALTER TABLE `polls_choice` ADD COLUMN `question_id` integer NOT NULL;
ALTER TABLE `polls_choice` ALTER COLUMN `question_id` DROP DEFAULT;
CREATE INDEX `polls_choice_7aa0f6ee` ON `polls_choice` (`question_id`);
ALTER TABLE `polls_choice` ADD CONSTRAINT `polls_choice_question_id_246c99a640fbbd72_fk_polls_question_id` FOREIGN KEY (`question_id`) REFERENCES `polls_question` (`id`);

COMMIT;

Обратите внимание на следующее:

  • вывод может отличаться в зависимости от типа базы данных, тут показан вывод для MySQL/MariaDB;
  • имена таблиц генерируются из комбинации имени приложения (polls) и имени модели в нижнем регистре — question и choice (это можно изменить);
  • Primary keys (IDs) добавляются автоматически (и это можно изменить);
  • по умолчанию — Django добавляет «_id» к полю foreign key (и это настраивается тоже);
  • foreign key отношения создаются явно с помощью FOREIGN KEY;
  • команда sqlmigrate не выполняет никаких действий в самой базе данных — она просто отображает то, что должно быть выполнено, и полезно для проверки перед внесением изменений в базу данных.

Вы так же можете выполните команду python manage.py check, которая проверит потенциальные проблемы без создания файла миграции и без изменений в базе данных:

$ python manage.py check
System check identified no issues (0 silenced).

Теперь — давайте выполним migrate, что бы создать таблицы ваших моделей в базе данных:

$ python manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: staticfiles, messages
  Apply all migrations: admin, contenttypes, polls, auth, sessions
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying polls.0001_initial... OK

Команда migrate берёт все миграции, которые не были выполнены (Django отслеживает их с помощью специальной таблицы django_migrations в ваше базе данных) и запускает их, синхронизируя изменения, которые вы сделали в моделях, с базой данных.

Миграции — очень мощный инструмент, позволяющий вам вносить изменения без необходимости удаления базы или таблиц в ней, и создавать в ней новые таблицы.

Мы рассмотрим миграции позже, а пока — запомните три шага для внесения изменений в модели:

  • измените модели в файле models.py;
  • запустите python manage.py makemigrations, что бы создать файлы миграций;
  • запустите python manage.py migrate, что бы применить эти изменения в базе.

Причина, по которой процесс разделён на разные команды — это то, что вы можете сохранить ваши миграции в вашей системе контроля версий, и распространять их с вашим приложением. Это не только делает процесс разработки проще — но и позволяет использовать их другим разработчикам.

Вы можете почитать django-admin documentation для получения полной информации об утилите manage.py.

Работа с API

Теперь — давайте переключимся в консоль Python и поработаем с Django API. Что бы вызывать консоль — выполните:

$ python manage.py shell

Мы используем эту команду вместо обычного вызова команды python, так как manage.py установит переменную окружения DJANGO_SETTINGS_MODULE, которая укажет Django путь к импорту файла mysite/settings.py и подключит все находящиеся в нём настройки.

Теперь, находясь в консоли, вы можете поработать с database API:

# импортируем классы моделей, которые мы только что написали
>>> from polls.models import Question, Choice

>>> # пока никаких вопросов в голосовании нет
>>> Question.objects.all()
[]

>>> # создаём новый вопрос
>>> # поддержка временных зон включена по умолчанию
>>> # поэтому Django ожидает datetime с tzinfo для pub_date
>>> # используйте timezone.now() вместо datetime.datetime.now()
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

>>> # сохраняем новый объект в базу данных
>>> q.save()

>>> # теперь у него есть ID; вы можете заметить, что он будет 1L, в не 1
>>> # это зависит от типа сервера баз данных
>>> q.id
1L

>>> # получаем доступ к полям через атрибуты Python
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2015, 5, 7, 8, 9, 10, 364248, tzinfo=<UTC>)

>>> # изменим значения атрибутов, и вызываем save()
>>> q.question_text = "What's up?"
>>> q.save()

>>> # objects.all() отображает все вопросы в базе
>>> Question.objects.all()
[<Question: Question object>]

Минуту… <Question: Question object> — уродливое и бесполезное отображение данных из объекта. Давайте исправим это, отредактировав модель Question в файле polls/models.py, и добавив метод __unicode__()  (__str__() в Python 3) для моделей Question и Choice:

from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __unicode__(self):              # __str__ в Python 3
        return self.question_text


class Choice(models.Model):
    question = models.ForeignKey(Question)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __unicode__(self):              # __str__ в Python 3
        return self.choice_text

def __unicode__ тут — это обычный метод Python.

Давайте добавим ещё один, просто для примера:

import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

Обратите внимание, что мы так же добавили импорт datetime из стандартной библиотеки Python, и timezone из django.utils.

Сохраните изменения, и запустите python manage.py shell ещё раз:

>>> from polls.models import Question, Choice
>>> Question.objects.all()
[<Question: What's up?>]

>>> # Django предоставляет так же API для поиска по базе
>>> # основанный на ключевых словах
>>> Question.objects.filter(id=1)
[<Question: What's up?>]
>>> Question.objects.filter(question_text__startswith='What')
[<Question: What's up?>]

>>> # получить вопросы, созданные в этом году
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

>>> # вызов ID которого нет - вызове исключение
>>> Question.objects.get(id=2)
...
DoesNotExist: Question matching query does not exist.

>>> # поиск по первичному ключу очень распространённое явление
>>> # поэтому Djnago предоставляет сокращение для него
>>> # следующий вызов аналогичен Question.objects.get(id=1)
>>> Question.objects.get(pk=1)
<Question: What's up?>

>>> # убедимся, что наше дополнительное поле так же работает
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True
>>> # добавим к вопросу несколько вариантов ответа
>>> # вызов create создаёт новый объект Choice
>>> # выполняет INSERT, добавляет новый choice к набору имеющихся choice
>>> # и возвращает новый объект Choice
>>> # Django создаёт набор для хранения ForeignKey связей с "другой стороной"
>>> # (например - варианты ответа для вопроса)
>>> # которые могут быть доступны через API
>>> q = Question.objects.get(pk=1)

>>> # пока никаких вариантов ответов нет
>>> q.choice_set.all()
[]

>>> # создаём три ответа (choices)
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)

>>> # объекты Choice имеют API доступ к связанным с ними объектам Questions
>>> c.question
<Question: What's up?>

>>> # и наоборот - объекты Questions имеют доступ к объектам Choice
>>> q.choice_set.all()
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]
>>> q.choice_set.count()
3

>>> # API автоматически отслеживает необходимые связи
>>> # используйте двойное подчёркивание, что бы разделить связи
>>> # это работает на любых уровнях вложенности без ограничений
>>> # найдём все вопросы, у которых pub_date - этот год
>>> # (переменную 'current_year' мы создали ранее)
>>> Choice.objects.filter(question__pub_date__year=current_year)
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]

>>> # удалим один из вариантов ответа с помощью delete()
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()

Для получения детальной информации по связях моделей — смотрите Accessing related objects. Информация об использовании двойных подчёркиваний для выполнения поиска с помощью API — доступна на странице Field lookups. Полное описание работы с API — есть на странице Database API reference.

Продолжение — Django: пример создания приложения – часть 3: панель управления