Чтобы максимизировать ваши преимущества с помощью контейнеров

Это третья часть серии статей о написании веб-приложений. В этой серии мы будем писать веб-приложение на Rust, и я объясню вам, как написать его самостоятельно.

Однако, если вы не хотите писать код самостоятельно, я сделал репозиторий со всем кодом, написанным на протяжении всей этой серии, здесь. Я делал коммит в репозиторий в конце каждой части серии. «В предыдущей части мы рассмотрели использование баз данных, чтобы сделать операции CRUD еще проще. В этой части мы обсудим использование Object-Relational Mapper или сокращенно ORM, чтобы сделать работу с нашей базой данных еще проще.

ORM

Object-Relational Mapping — это метод преобразования данных между системами типов в объектно-ориентированных языках программирования. Для упрощения это метод преобразования классов и их данных в данные, которые могут использоваться другими программами. Однако, как правило, когда некоторые упоминают ORM, они обсуждают библиотеку, использующую этот метод.

ORM почти всегда используются с базами данных. ORM возьмет объект в классе или экземпляр структуры и создаст для него запись в базе данных. Таким образом, мы могли бы просто передать ORM экземпляр нашей структуры Task, и он создаст для нас запись в таблице tasks, вместо того, чтобы нам приходилось писать sql самостоятельно.

Таким образом, ORM — это, по сути, способ сократить шаблонный код. Вместо того, чтобы писать кучу повторяющегося кода sql, мы предоставляем ORM генерировать код sql на основе наших структур и кода, который мы оставляем внутри этих структур. Помимо этого, ORM часто реализуют способы управления созданием таблиц, доступных в вашей базе данных. Они называются «миграциями».

По сути, наш драйвер базы данных дает нам возможность запускать sql команд в нашей базе данных. Он просто позволяет взаимодействовать с нашей базой данных. ORM позволяет нам писать на выбранном нами языке, а не запускать sql команд для взаимодействия с базой данных.

Учитывая то, как мы настроили Rocket, предполагается использовать асинхронный ORM. Самая популярная асинхронная ORM в Rust на данный момент — это SeaQL/sea-orm, так что это то, что мы будем реализовывать в нашей кодовой базе.

Во-первых, измените свой Cargo.toml, чтобы он выглядел следующим образом:

При этом мы установили sea-orm и некоторые другие библиотеки, которые нам понадобятся, и удалили библиотеки, которые теперь являются избыточными. Теперь первое, что мы собираемся сделать, это создать те миграции, о которых я упоминал ранее. Мы будем использовать sea-orm для создания таблиц в нашей базе данных, поэтому нам не нужно делать это вручную.

Миграции

В терминале выполните следующую команду:

cargo install sea-orm-cli

Это установит интерфейс командной строки (CLI), чтобы мы могли запускать определенные команды на нашем терминале. Как вы понимаете, эти команды упрощают выполнение определенных действий с помощью sea-orm. Используя наш недавно установленный интерфейс командной строки, мы собираемся создать каталог для хранения наших миграций, выполнив следующую команду:

sea-orm-cli migrate init

В моей текущей версии sea-orm это создает немного неправильный Cargo.toml. Убедитесь, что для Cargo.toml в вашем каталоге migration включены функции sqlx-postgres и runtime-tokio-native-tls для sea-orm-migration, а async-std и Rocket импортированы следующим образом (импорт для async-std — это фрагмент кода, начинающийся с [dependencies.async-std]).

Теперь мы создадим новую миграцию, выполнив следующую команду:

sea-orm-cli migrate generate create_tasks_table

Теперь, если вы перейдете к migration/src, вы увидите файл, который начинается с m, имеет несколько номеров и create_tasks_table в конце. Для меня получилось m20220623_084419_create_tasks_table, но число зависит от времени, поэтому для вас оно будет другим. В любом случае, откройте этот файл. Теперь поместите следующий код в этот файл.

Примечание: под impl MigrationName... оставьте строку такой же, как имя файла. Это не должно быть имя, которое у меня есть.

Удалите другой файл с номерами впереди и измените lib.rs, чтобы он использовал только вашу миграцию следующим образом:

Затем убедитесь, что main.rs в migration/src выглядит следующим образом:

Теперь в терминале запускаем

cargo new entity

Измените Cargo.toml entity, чтобы он выглядел следующим образом:

Обратите внимание, что значения name и path под [lib] являются значениями по умолчанию. Но они просто помещаются туда на случай, если значения по умолчанию когда-либо изменятся. Все, что нам действительно нужно, это [lib], чтобы объявить, что у Cargo есть цель библиотеки.

Переименуйте entity/src/main.rs в lib.rs. Мы позаботимся о том, чтобы это было заменено некоторым кодом, который можно импортировать через некоторое время.

В нашем исходном src sea-orm заменит драйвер базы данных, который мы использовали в нашем примере до сих пор. По большей части это тот же драйвер базы данных, но он имеет некоторые дополнительные функции и другие особенности, позволяющие работать с sea-orm.

На данный момент мы будем рассматривать код, который позволяет нам подключаться к нашей базе данных, как шаблон, который не нужно понимать. Мы поместим этот шаблон в отдельный файл, а затем импортируем необходимые части в main.rs. Итак, в src создайте новый файл с именем pool.rs со следующим кодом:

Теперь мы собираемся изменить нашу основную функцию, чтобы подключиться к нашей новой, более привлекательной базе данных и запустить миграцию, которую мы создаем. Таким образом, перейдите в main.rs и измените функцию rocket, чтобы она выглядела следующим образом:

Сейчас это больно делать, но ни один из этих кодов в main.rs больше не нужен, поэтому мы его удалим. Удалите структуру Task, структуру TaskItem, структуру TaskId, структуру TodoDatabase, структуру DatabaseError, две реализации для Database Error, add_task, read_tasks, edit_task и delete_task. Когда все сказано и сделано, наш main.rs снова стал почти новым.

Теперь в Postgres войдите в базу данных todo и удалите нашу уже существующую таблицу tasks с помощью следующей команды:

DROP TABLE tasks;

Со всем этим вы можете еще раз запустить cargo run в терминале, который будет использовать нашу миграцию для создания таблицы tasks. Он также создаст таблицу с именем seaql_migrations, в которой будет отслеживаться информация о наших миграциях.

Сущности

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

Главным образом потому, что мы будем редактировать эти объекты, чтобы они лучше работали в нашем проекте, и запуск команды создания объектов перезаписывает сделанные нами изменения. Таким образом, я просто дам вам код для созданных нами сущностей, но имейте в виду, что вы можете создавать сущности из существующей базы данных, если когда-нибудь найдете в ней потребность или хорошее применение.

Итак, в entity/src создайте файл с именем tasks.rs и введите следующий код:

Кстати, вы можете заметить, что к нашему полю id применен атрибут #[field(default = 0)]. Это делает так, что при обработке наших данных id по умолчанию будет равно 0, если значение не указано, поэтому Tasks можно ввести без id.

После этого создайте файл с именем lib.rs в entity/src и введите в него следующий код:

pub mod tasks;

Это позволит использовать структуру Model в tasks.rs в любом пакете, который импортирует библиотечный ящик entity (создание папки с именем entity с помощью этого кода позволило нам локально создать библиотеку с именем entity. Если вы хотите узнать больше о том, как Rust имеет дело с совместным использованием кода между файлами и проектами, ознакомьтесь с главой 7 книги Rust, где это рассматривается довольно подробно).

Итак, с точки зрения нашего кода, теперь мы можем использовать нашу экспортированную модель tasks в качестве структуры для получения введенных данных. Обратите внимание, что модель реализует FromForm, поэтому теперь мы будем вводить наши данные как x-www-form-urlencoded, а не JSON. Учитывая наше небольшое количество основных параметров, этот переключатель имеет большой смысл.

Кроме того, в Rocket синтаксический анализ формы по умолчанию снисходителен, поэтому, если есть отсутствующие, дублирующиеся или дополнительные поля, он все равно будет анализировать. Учитывая, что у нас есть места, где мы можем получить id, но не предмет, элемент, но не идентификатор, или и то, и другое, это уменьшит дополнительный код, который нам приходилось писать раньше.

В любом случае, давайте заново реализуем все наши CRUD-операции через ORM. Вы заметите, что я в основном использую модель для поиска элементов в базе данных и для добавления элементов, которые я собрал, используя структуру модели, в базу данных. В этом сила ORM. Мне не нужно писать sql сейчас; Я могу просто писать функции на Rust.

Итак, приступим! Первый — переписывание операции создания. Вот код для этого:

Как видите, мы снова использовали DatabaseError в качестве оболочки для фактической ошибки из нашей библиотеки базы данных, чтобы мы могли возвращать эти ошибки. В add_task функции into_inner извлекают нужные нам данные из имеющихся у нас переменных.

Наконец, в SeaORM, чтобы использовать модель для обновления или создания элемента, она должна быть ActiveModel, поэтому мы должны использовать данные, которые у нас уже есть, для создания версии ActiveModel. Наконец, последняя строка — это, конечно же, вставка элемента в базу данных.

Далее идет операция чтения, которая еще проще. Я включил импорт для одной из переменных, используемых в этой функции, потому что это именованный импорт, и иначе вы не сможете правильно импортировать его.

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

Далее edit_task немного сложнее, но все равно довольно просто. Мы находим задачу, которую пытаемся обновить, task_to_update, изменяем в ней поля, которые мы хотели бы изменить, затем вызываем update для обновления базы данных.

Наконец, delete_task — единственный, у которого немного изменился API. Из-за того, что я получаю только DeleteResult от SeaORM, я теперь просто возвращаю, сколько задач было удалено, а не удаленную задачу.

Кроме того, поскольку я не могу гарантировать, что id передается вместе с формой задачи, и единственное значение, которое я принимаю, — это id, я делаю id параметром URL-адреса, что прекрасно работает в данном случае. Таким образом, это выглядит по-другому, но в основном делает то же самое.

Таким образом, общий main.rs выглядит так:

И с этим мы еще раз реализовали наши операции CRUD, на этот раз с использованием ORM. Однако мы добавили много кода в кодовую базу и сохранили только около трех строк в main.rs. Давайте поговорим об этом.

Чрезмерная разработка проблемы

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

Как правило, мы хотим, чтобы наш код был несколько гибким. Поскольку программирование является итеративным, почти гарантировано, что кто-то посмотрит на то, что вы написали, и решит изменить его части или добавить к нему новые функции. Если ваш код является гибким, что бы разработчик ни делал, это будет намного проще.

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

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

Таким образом, с любым раствором нужно быть осторожным. Очень гибкое решение также будет очень сложным, и работать с ним будет кошмаром, даже несмотря на то, что первоначальное намерение состояло в том, чтобы упростить работу с ним. С другой стороны, очень простое решение не будет гибким, поэтому его, вероятно, придется переписывать с нуля всякий раз, когда необходимо добавить новую функцию. Ни одна крайность не идеальна.

Когда вы «переусложняете» проблему, это означает, что вы сделали свой код слишком гибким и в процессе сделали его гораздо более сложным, чем он должен быть. Конечно, является ли решение на самом деле перепроектированным или нет, это вопрос субъективного мнения. Это зависит от вкусов и чувствительности разработчика.

Но, как и любая субъективная вещь, есть определенные случаи, когда большинство людей согласны. Я полагаю, что большинство людей согласятся с тем, что реализация ORM для проекта, который мы делаем, требует чрезмерной инженерии.

ORM позволяет очень легко создавать большое количество таблиц с множеством взаимосвязей и работать с данными внутри этой большой базы данных. Это инструмент, когда у вас десятки или сотни столов. В нашем случае у нас будет только две таблицы к тому времени, когда мы закончим с этим приложением. Хотя поддерживать их напрямую через sql сложнее, это также требует гораздо меньшей сложности кода и управляемо для небольшого количества таблиц.

Итак, зачем я вообще реализовал ORM? Как я уже говорил в начале этой части, моя цель — продемонстрировать все виды библиотек и концепций, которые используют современные веб-приложения и фреймворки, чтобы вы могли лучше понять их. Хотя ORM не лучшая идея для этого приложения, я хотел продемонстрировать эту концепцию, потому что ORM практически вездесущи в серверных фреймворках.

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

Контейнеризация

Возможно, вы заметили интересную проблему с нашей текущей базой данных. Дело в том, что нам нужно установить и настроить приложение на нашем компьютере, чтобы наш код работал. Когда над проектом работает только один разработчик, это нормально. Однако это может создать множество проблем в условиях, когда над этим проектом могут работать несколько разработчиков.

Первая проблема заключается в том, что установка программного обеспечения утомительна, никогда не доставляет удовольствия и часто подвержена ошибкам. Во-вторых, не все приложения доступны во всех операционных системах. Хотя postgres может быть доступен в Windows, Mac и Linux, такая кросс-совместимость не является данностью для большого количества программного обеспечения.

Далее, допустим, разработчик, работающий над этим проектом, захотел использовать postgres дома. Использование одной и той же установки postgres для нескольких проектов возможно, но грязно. Кроме того, трудно выяснить, где распространять конфигурации, необходимые для работы программного обеспечения.

Это сделано в ридми? Отправлено как сообщение в Slack? Поместить в текстовый файл? И что произойдет, если эти конфигурации обновятся или изменятся? Как вы уведомляете людей? Наконец, локальные установки могут быть повреждены или испорчены другими вещами, работающими на машине разработчика.

Было бы неплохо, если бы был способ установить postgres таким образом, чтобы избежать всех этих проблем. И, поскольку я поднимаю эту тему, вы знаете, что она есть. Ответом на эти проблемы является контейнеризация. Как работает контейнеризация? Вместо того, чтобы запускать приложение непосредственно в нашей ОС, мы запускаем другую ОС и запускаем приложение в ней.

Делая ОС как можно меньше, а также выполняя некоторые другие оптимизации, это может быть достаточно быстро и хорошо работать в среде разработки. Более того, большинство контейнеров (ОС и приложение внутри них) можно настроить с помощью определенных файлов. Таким образом, мы можем управлять версиями того, как настроено приложение, такое как postgres.

Поскольку контейнер, который мы хотим установить, и настройки приложения контролируются файлами конфигурации, разработчику не нужно беспокоиться об установке программного обеспечения вручную. Большинство приложений контейнеризации также являются кроссплатформенными, что означает, что вы можете запускать приложения, которые обычно не являются кроссплатформенными, внутри контейнера.

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

Наконец, поскольку наше приложение работает в совершенно отдельной ОС, нам не нужно беспокоиться о том, что оно будет испорчено или повреждено другими приложениями на нашем компьютере.

Имея все это в виду, давайте использовать контейнеры, чтобы упростить работу с нашей установкой postgres. Существует множество приложений, обеспечивающих контейнеризацию, но мы будем использовать Docker. В последние годы многие люди начали не любить Docker за его модель ценообразования, поэтому, если вы собираетесь использовать контейнеризацию в будущем, вы можете рассмотреть другие варианты. Но для нашего игрушечного примера Docker вполне подойдет. Итак, перейдите на сайт, создайте учетную запись и установите Docker в качестве настольного приложения.

Теперь отсюда мы будем использовать Postgres — официальное изображение для создания нашего контейнера. Что такое образ? Это просто ОС с установленным в ней приложением. Контейнер — это экземпляр, на котором запущен образ. Итак, мы будем использовать образ для запуска контейнера с postgres.

Как мы будем запускать этот образ? Мы будем использовать инструмент командной строки docker/compose, который устанавливается вместе с настольной установкой Docker. docker-compose берет файл с именем docker-compose.yml и использует его, чтобы определить, какие образы запускать и с какими конфигурациями.

Кстати, в качестве примечания, Dockerfiles можно использовать для изменения существующих образов, что позволяет создавать собственные образы, которые лучше подходят для ваших целей. К счастью, мы можем просто использовать базовый образ postgres, но Dockerfiles очень часто используются в средах разработки, поэтому я подумал, что это следует упомянуть.

В любом случае мы создадим файл с именем docker-compose.yml в том же каталоге, что и Cargo.toml. Внутри него будет идти следующий код:

Теперь, если вы перейдете в каталог todo-app и запустите docker-compose up, ваша база данных запустится. Когда база данных запущена, вы можете открыть другой терминал и запустить свое приложение. Однако есть еще одна проблема.

Поскольку на вашем компьютере установлен postgres, на нем постоянно работает служба, использующая port 5432. Таким образом, когда мы обращаемся к port 5432, мы будем обращаться к нашему локальному приложению, а не к приложению в контейнере.

Отключите или удалите установку postgres, чтобы разрешить подключение к контейнерной базе данных. Как только вы это сделаете, вы заметите, что ничего не изменилось, и база данных работает так же. Просто теперь проще настроить и работать.

Нам нужно docker-compose?

Нет. Мы могли бы запустить образ с правильными настройками через командную строку или настольное приложение. Но использование docker-compose позволяет нам оставить полезный файл конфигурации, в котором подробно описаны все конфигурации, используемые для работы нашего приложения. И, если мы хотим, чтобы для нашего приложения запускалось несколько контейнеров, мы можем перечислить конфигурации для всех из них в docker-compose.yml.

Я никогда не делал этого сам, но вы также можете использовать функциональность Dockerfile для настройки вашего изображения. Итак, если вы хотите обойти использование docker-compose по какой-то странной причине, это может быть вариантом. В любом случае, существует множество способов работы с Docker, но я считаю, что этот способ действительно максимизирует преимущества, которые вы получаете от контейнеризации приложений, работающих с вашим программным обеспечением, поэтому мы используем его в этой серии статей.

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

Спасибо, что прочитали эту статью. Я надеюсь, что эта серия поможет вам улучшить свои навыки веб-разработки.

Ресурсы