Найти в Дзене
Я, Golang-инженер

#63. Миграции схемы БД в SQLite на Go. Бекапы и отмена изменений. Пример приложения для сбора инфо по IP. Работа с утилитой sqlite3

Оглавление

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.

Хой, джедаи и амазонки!

Ранее познакомился с СУБД SQLite и PostgreSQL: о практике в SQLite и о разнице между БД и СУБД можно почитать в предыдущей публикации, а о практике с PostgreSQL в этой - в то время я проходил акселерацию в Яндекс Практикуме для трудоустройства. Сейчас приостановил поиск работы и участие в акселерации по личным причинам, но программировать стараюсь продолжать, и через пару месяцев намерен продолжить поиск работы.

Сейчас улучшаю навык работы с базами данных, и начал с основы основ - практиковаться с SQLite. В публикации мы разберём теорию: что такое миграции, какая нужна библиотека для этих целей, посмотрим примеры кода. А далее закрепим навык: напишем приложение, которое будет по IP или домену искать дополнительную информацию об IP и сохранять данные в БД через СУБД SQLite, созданную миграциями. Публикация получилась длинная.

https://ru.freepik.com/free-photo/flock-canadian-geese-flying-around-great-salt-lake-utah-us_17248672.htm
https://ru.freepik.com/free-photo/flock-canadian-geese-flying-around-great-salt-lake-utah-us_17248672.htm

1. Что такое миграции в СУБД

1.1. Введение

С термином миграции познакомился в тестовом задании, звучало оно примерно так:

Создать БД с помощью миграций.

Миграции баз данных бывают двух видов:

  • Перенос данных: с одного диска на другой, с локального хранилища в облако, между СУБД;
  • Изменение структуры хранения данных в текущей БД.

Речь в публикации пойдёт именно об изменениях структуры БД с помощью миграций.

Миграции - по сути, это папка с файлами формата .sql, которые можно использовать для обновления схемы БД или отката изменений. Пример содержимого файла миграций ниже - sql-запрос на создание таблицы и индекса на столбец в БД:

Файл для создания таблицы в БД
Файл для создания таблицы в БД

1.2. Миграции - это Git в мире СУБД?

По ходу изучения миграция возникла такая ассоциация, что миграции - это система контроля версий (Git) для СУБД. Для себя я выделил три пункта, обосновывающих такую связь, которые заодно объясняют, для чего нужны миграции:

  1. Как и в Git, миграции позволяют фиксировать каждое изменение в схеме БД: есть история изменений.
  2. Проще синхронизировать работу над одной БД для нескольких разработчиков.
  3. В случае ошибки, легко откатить внесённые изменения.

Я выделил для себя два способа работать с БД:

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

1.3. Миграции vs константный код

Занимаясь на курсах, и когда практиковал работу с БД сам, код по созданию или изменению схемы БД был константным, а это более примитивный способ работы с СУБД.

Что значит константный код? Да вот он, в переменной request:

Создание БД константным кодом
Создание БД константным кодом

Это простенький код по созданию таблицы в БД и индекса. Через терминал откроем таблицу в БД и посмотрим на схему:

Работа в терминале
Работа в терминале

Мы запустили команду .schema и получили структуру БД - в данном случае у нас одна таблица и выдаётся её код. Всё в порядке, программа отработала как мы и ожидали.

Однако константный код по созданию не позволяет воспользоваться преимуществами, описанными в параграфе 1.2, см. выше. Идём разбираться с миграциями детальнее

2. Практика кода с миграциями

2.1. Создаём БД миграциями

Первое, что нужно понять - для работы с БД через СУБД SQLite (или любую другую СУБД), нужен драйвер; популярный драйвер для SQLIte загружается с github.com/mattn/go-sqlite3 - это сторонняя библиотека для Go. А для работы с миграциями в SQLite нужна другая сторонняя библиотека. Например, для Go есть популярная библиотека миграций множества СУБД: github.com/golang-migrate/migrate

Перечень СУБД, для которых библиотека golang-migrate поддерживает миграции
Перечень СУБД, для которых библиотека golang-migrate поддерживает миграции

В ReadMe на SQLite3 указано, что в библиотеке миграций под капотом библиотека mattn в виде драйвера СУБД, о которой писал выше:

Фрагмент ReadMe с GitHub
Фрагмент ReadMe с GitHub

Далее в импорты пакета main, или где мы создаём БД, нужно прописать следующие строки:

"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"

Выглядит это примерно так:

Фрагмент с GitHub
Фрагмент с GitHub

Далее открываем БД и создаём миграции. Я пока не пишу код для проверки подключения к БД в виде пинга, или, например, таймаута соединений для простоты и наглядности.

Создаём миграции
Создаём миграции

Что здесь нужно добавить: код создаёт в корне проекта БД с именем mydatabase.db - это вся база данных в одном файле. Также в корне проекта должны быть созданы вручную файлы миграций по примеру выше в отдельном каталоге, в данном случае в корне проекта есть папка migrations. Наименования файлов миграций, как я понял, выполняют двумя способами:

  1. Просто порядковый номер впереди до нижнего слеша, например: 001_create_users_table.up.sql, 001_create_users_table.down.sql, 002_add_email_to_users_table.up.sql и 002_add_email_to_users_table.down.sql;
  2. Текущей временной меткой файла, например: 20240929_122930_create_users_table.up.sql, 20240929_122930_create_users_table.down.sql, 20240930_125055_add_email_to_users_table.up.sql и 20240930_125055_add_email_to_users_table.down.sql.

Во втором примере 2024-09-30 и 12:50:55 - дата и время создания миграции. В обоих примерах после номера миграции через слеш идёт описание миграции в наименовании файла и суффикс up или down, обозначающий файл для создания или отката миграции.

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

2.2. Создание и удаление таблицы миграциями

Файл создания таблицы может выглядеть так, как я указывал выше:

Файл миграции создания таблицы в БД
Файл миграции создания таблицы в БД

Это абстрактная таблица, в которой создаются три поля: id, name и price - например, для хранения товаров и их цен.

Файл миграции для отката этих изменений может выглядеть так:

Файл миграций отката изменений
Файл миграций отката изменений

2.3. Добавление столбца таблицы миграциями

Изменения схемы таблицы выполняется по алгоритму:

Алгоритм изменения таблицы
Алгоритм изменения таблицы

Иллюстрация взята с официального сайта sqlite, можно почитать там подробнее.

Файл миграции для создания нового столбца в таблице может выглядеть так:

Файл миграций создания столбца в таблице БД
Файл миграций создания столбца в таблице БД

Разберём код выше:

  • ALTER TABLE: указывает, что мы хотим изменить таблицу. В данном случае, это таблица с именем items.
  • ADD COLUMN: этот оператор указывает, что мы хотим добавить новый столбец в таблицу.
  • note TEXT: здесь мы определили имя нового столбца note и его тип данных TEXT. Это означает, что в новом столбце можно будет хранить текстовые данные.

Теперь посмотрим на код отката этой миграции:

Откат миграции создания столбца
Откат миграции создания столбца

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

Ниже пример, как может выглядеть sql-запрос в файле миграции для удаление колонки этим способом. Говорят, в ранних версиях sqlite так и нужно было делать, т.к. не было прямой команды удалить столбец. А сейчас нам не нужен этот громоздкий код для удаления колонки, мы просто посмотрим, что бы мы примерно писали, если бы потребовалось, скажем, изменить тип хранимых в столбце данных, или изменить имя файла базы данных, или какую-то другую неподдерживаемую sqlte команду:

Код миграции отката при создании столбца
Код миграции отката при создании столбца

Команды, напрямую поддерживаемые SQLite для изменения схемы БД, следующие:

  1. Переименовать таблицу;
  2. Переименовать столбец;
  3. Добавить столбец;
  4. Удалить столбец.

2.4. Таблица миграций

При создании миграции создаётся таблица миграций в БД, т.е. в единственном файле из которого состоит вся БД. Запустим консольный интерфейс для работы с нашей БД в SQLite командой $ sqlite3 mydatabase.db (приложение sqlite3 должно быть установлено):

Запускаем интерактивный режим приложения SQLite3
Запускаем интерактивный режим приложения SQLite3

Командой $ .tables посмотрим все таблицы в БД:

Таблицы в БД
Таблицы в БД

Мы увидели, что в БД есть 2 таблицы, созданные СУБД SQLite: items и schema_migrations. Наименование второй таблицы было в ReadMe из библиотеки миграций SQLite:

Фрагмент ReadMe
Фрагмент ReadMe

Это были два способа узнать имя таблицы, в которой хранится информация о миграциях. Сейчас мы можем посмотреть, что в нашей таблице миграций, зная её имя через команду $ SELECT * FROM schema_migrations:

Смотрим содержимое файла миграций
Смотрим содержимое файла миграций

Запись 2|0, которую мы видим - это версия миграции и состояние. Расшифровка этой строки такова:

  • 2: Это актуальная версия миграций, которая была применена к базе данных. Это указывает на то, что были применены две миграции. Собственно, в параграфах выше были показаны примеры с двумя миграциями: в одной мы создали таблицу items, а в другой миграции добавили колонку note.
  • 0: Это "грязное" состояние базы данных. Если это число равно 0, это означает, что база данных находится в чистом состоянии (то есть не было ошибок в ходе миграций). Если был бы откат (или не завершенная миграция), это значение могло бы быть равно 1, указывая на то, что базу данных нужно "очистить" или возвратить к корректному состоянию. И с этим состоянием нужно работать особым образом, который не рассматривается в публикации.

Примеры содержимого таблицы миграций:

  • 2|0: У меня применены две миграции, и не было ошибок или незавершенных миграций. То есть как на моей иллюстрации выше.
  • 2|1: Были применены две миграции, но есть нефиксированное состояние или ошибка в одной из миграций.

2.5. Резервная копия БД

Перед применением новой миграции к БД, хорошей практикой будет создать резервную копию БД. Когда у нас вся БД - это один файл, сам собой напрашивается способ просто скопировать файл. Вот варианты действий:

1. Прямое копирование Ctrl+C, Ctrl+V.

2. Копирование через интерактивный режим SQLite3 в терминале:

Копирование файла БД
Копирование файла БД

3. Копирование файла БД через терминал:

Копирование через команду cp в терминале
Копирование через команду cp в терминале

4. Кодом в приложении.

2.6. Откат миграции

Я так и не разобрался, как откатить миграцию через терминал. Если знаете как - напишите в комментарий. Один из вариантов результатов такой:

Вывод терминала после попытки отката миграции
Вывод терминала после попытки отката миграции

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

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

Откат миграции кодом
Откат миграции кодом

Сюда же можно добавить код бэкапа БД, если нам важны данные, которые могут быть удалены (например, при откате созданной таблицы или созданного столбца).

Собственно, это вся основная информация по созданию и использованию миграций в SQLite, с которой я разобрался на данный момент. Ещё хочу добавить пару слов о самой СУБД SQLite, раз я с ней начал разбираться лучше.

2.7. Ещё немного об SQLite

SQLite - это не просто учебная база данных, а профессиональный инструмент, используемый во множестве сфер деятельности: от пассажирских перевозок на авиалайнерах до приложений на телефонах. Взглянем на страницу официального сайта SQLite об известных пользователях СУБД.

Скриншот https://www.sqlite.org/famous.html
Скриншот https://www.sqlite.org/famous.html

SQLite встроен во все смартфоны и большинство компьютеров, эту СУБД применяют обычные люди каждый день, не задумываясь об этом. Разработчики обещают поддержку до 2050 года. А в активном использовании находится более 1 000 000 000 000 баз данных SQLite(!).

В общем, СУБД распространённая, можно осваивать и применять. Далее переходим к практике - пишем наше приложение.

3. Пишем приложение с миграциями БД

Хотел больше попрактиковаться с пакетом net/http, поэтому выбрал такое приложение, чтобы закрепить навык работы с SQLite и миграциями, где бизнес-логика связана с интернет-событиями.

В первой версии приложения мы по IP будем искать хосты и сохранять информацию в базу данных: ip, хост и время создания записи в БД.

Во второй версии приложения мы добавим функционал поиска как по ip, так и по домену, а также будем собирать дополнительную информацию об IP: страну, регион и город, с которым связан IP-адрес, провайдера, и другие данные. Также обновим схему базы данных через миграции: добавим новые столбцы для хранения соответствующей информации.

3.1. V1 приложения

Идею взял с сайта готовых рецептов Go и модифицировал код.

3.1.1. Код без БД

Первым делом создадим приложение без БД. Информация выводится в терминал. IP будем передавать через флаг.

Основа приложения
Основа приложения

Запустим программу с флагом -ip ip-адрес, например известных сайтов. Введём 129.222.0.0 или 8.8.8.8:

Результат выполнения программы
Результат выполнения программы

Пару слов о том, как работает код:

  1. В строках 11-14 мы прописали код ошибок, которые будет затем удобно подставлять в приложение.
  2. В строках 17-19 мы проверяем, введён ли аргумент командной строки при запуске программы. Команда go run main.go имеет 1 аргумент - main.go6 и он считается нулевым аргументом. Мы проверяем, имеется ли хотя бы два аргумента: нулевой и первый. И если аргументов меньше двух, то программа завершается.
  3. В строке 20 мы определяем имя флага командной строки и пишем подсказку, которая будет видна если скомпилировать приложение через go build и запросить подсказку --help:
Запрос подсказки скомпилированного приложения
Запрос подсказки скомпилированного приложения

В подсказке сказано, какой нужен флаг. Либо мы можем без компиляции программы, запустить её с некорректными аргументами - тоже получим ошибку:

Завершение программы
Завершение программы

4. В строке 21 мы разбираем флаги и их значения, переданные в командной строке и инициализируем связанные с флагами переменные.

4. В строках 23-25 проверяем, не пустой ли аргумент. Флаги парсятся в указатели, а значит мы проверяем значение переменной на которую ссылается указатель, а не сам указатель, т.к. сам указатель никогда не будет пустым.

5. В строках 27-30 вызываем функцию net.LookupAddr, которая выполняет обратный DNS-запрос, чтобы получить имя хоста на основе переданного IP-адреса. Результаты сохраняются в слайс строк. Обрабатываем ошибку - например, если передан несуществующий IP-адрес.

6. В строке 31 печатаем найденные хост/хосты и, для порядка, исходный IP-адрес.

3.1.2. Подключаем БД

Первым делом в функцию main добавим код:

Создание БД
Создание БД

Мы хотим, чтобы функция initDatabase возвращала указатель на объект, предоставляющий интерфейс для работы с БД типа *sql.DB, а также возвращаем ошибку.

В строке 26 мы закрываем объект db не привычной многим строкой defer db.Close(), а строкой более сложной, с анонимной функцией. Дело в том, что метод Close возвращает ошибку, и хотя мы её здесь не обрабатываем - будет хорошим тоном напомнить - что объект может быть не закрыт.

Далее напишем функцию initDatabase:

Создание БД и таблицы миграциями
Создание БД и таблицы миграциями

Я сразу решил добавить обработчик - нужна ли отмена последней миграции через функцию cancelMigrations, вот её код:

Отмена последней миграции
Отмена последней миграции

Затем создадим каталог migrations и создадим там два файла. Первый - для создания таблицы:

Файл миграции создания таблицы
Файл миграции создания таблицы

И второй файл - для отката изменений в БД по созданию таблицы, если это потребуется:

Файл отката миграции создания таблицы
Файл отката миграции создания таблицы

Таблицу я решил назвать hosts, т.к. это простое и понятное название места, где будут хранится данные о хостах.

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

Содержимое каталога миграций сейчас выглядит так:

Содержимое каталога файлов миграций
Содержимое каталога файлов миграций

Хорошо, код и миграции написали, пробуем запустить приложение: пока оно не будет сохранять информацию о хостах и ip в БД, а создаст схему БД и получит данные об ip:

Запрос и вывод терминала
Запрос и вывод терминала

Итак, БД создалась успешно и программа спрашивает, нужно ли откатить миграцию. Мы введём n и получим:

Результат выполнения программы
Результат выполнения программы

Теперь посмотрим что там внутри БД:

Работа в консольной утилите sqlite3
Работа в консольной утилите sqlite3

Здесь я сперва перешёл в утилиту sqlite3, затем сделал два запроса:

.schema - посмотреть структуру БД

select * from hosts; select * from schema_migrations; - посмотреть что лежит внутри таблиц. Мне показали содержимое таблицы миграций - видим, что применена одна миграция; а содержимое таблицы hosts пока пустое, поэтом данных и нет.

Вот я посмотрел содержание таблицы и - о ужас! для столбца host определён тип данных FLOAT, а мы ожидали хранить там строки, т.е. TEXT по типу данных SQLite. Вариантов тут два:

1. Удалить таблицу и создать всё снова.

2. Создать миграцию изменения таблицы.

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

3.1.3. Пишем миграцию изменения БД

В каталоге migrations добавляем файл 002...

Файл создания новой миграции
Файл создания новой миграции

Отмена миграции тоже будет объёмной - уже нельзя просто воспользоваться командой drop:

Откат миграции
Откат миграции

Содержимое каталога файлов миграций сейчас выглядит так:

Каталог миграций
Каталог миграций

Запускаем, проверяем:

Сообщение в терминале
Сообщение в терминале

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

Результат работы в терминале
Результат работы в терминале

Отлично, тип данных столба host изменился с FLOAT на TEXT.

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

3.1.4. Пинг к БД

Начнём с простого улучшения - выполним пинг к БД. Для этого дополним функцию initDatabase:

Код пинга
Код пинга

Результат выполнения программы с пингом:

Работа в терминале
Работа в терминале

Теперь - зачем это нужно.

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

3.1.5. Резервное копирование БД перед миграцией

Особо выдумывать не буду, и добавлю в главную функцию запрос - хотим ли мы сделать резервную копию БД. Если мы знаем, что у нас будут новые миграции - будет нужно самому подтвердить необходимость выполнить резервную копию.

Сперва доработаем функцию initDatabase:

Добавляем код создания резервной копии
Добавляем код создания резервной копии

Затем напишем саму функцию reserveDatabase. Функция делает:

  1. Спрашивает, нужен ли бэкап БД.
  2. Если нет - завершает функцию.
  3. Если да - создаёт каталог для бэкапов, если его нет.
  4. Открывает текущую БД.
  5. Создаёт файл бэкапа с присвоением в имени текущей даты и времени до секунды.
  6. Копирует информацию из существующей БД в бэкап.
  7. Печатает лог в терминал
Код резервного копирования базы данных
Код резервного копирования базы данных

Результат выполнения кода в терминале будет таким:

-46

А так будет выглядеть содержимое каталога с бэкапами - я сделал три бэкапа:

Бэкапы базы данных
Бэкапы базы данных

Формат начала наименования файла следующий: 2024-10-02 22:32:31 - год-месяц-день час:минута:секунда.

3.1.5. Создание таймаута соединений

Меняем начало функции с такого:

Текущий код
Текущий код

На такой:

Добавление параметра
Добавление параметра

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

Поработали над созданием БД, теперь можно переходить к её наполнению.

3.1.6. Заполняем БД

Сперва дополним функцию main:

Добавленный код выделен
Добавленный код выделен

Обратите внимание, что передаём мы первый элемент среза host. Просто для удобства будем считать, что нам достаточно одного хоста (их может быть по ip много). И вот ещё - по идее переменную host нужно назвать hosts, т.к. это срез.

Далее добавим в константы несколько сообщений об ошибках, которые будут повторяться в коде:

Новые сообщения об ошибках выделены
Новые сообщения об ошибках выделены

Далее напишем код добавления информации в базу данных

Код добавления информации в БД. * в строке 156 вместо price должен быть host
Код добавления информации в БД. * в строке 156 вместо price должен быть host

Что здесь интересного. В строках 154-157 использовал именованные параметры функции. Часто можно встретить в sql-запросе не что-то вроде :ip, а знак вопроса , т.е. ?. Это не наглядно, а так - наглядно.

Далее сделал несколько обработок ошибок: по тексту ошибок в константах понятно, что именно проверяется.

Результат запуска программы в терминал будет таким:

Работа кода в терминале
Работа кода в терминале

Проверим содержимое БД через консольную утилиту sqlite3:

Проверка содержимого БД
Проверка содержимого БД

Ну вот, всё на месте. Запустим программу ещё раз и добавим другой ip. Кстати, выход из sqlite3 можно выполнить сочетанием клавиш Ctrl+D.

Выполнение программы с новым ip
Выполнение программы с новым ip

Смотрим, что хранится в БД:

Обновление БД
Обновление БД

Кстати, для удобства отображения информации, рекомендую перейти в табличный режим командой .mode table:

Табличное отображение информации
Табличное отображение информации

Готово, теперь чтобы нам постоянно не обращаться за помощью к sqlite3, добавим в приложение вывод 10 (или меньше) последних добавленных позиций из БД.

И вот ещё что - глядя на отображение информации в табличном режиме, я увидел что нет информации в столбце timeAdd. Здесь можно пойти двумя путями: создать миграцию, где в тип данных для столбца указать поле автоматического заполнения даты. Либо доработать функцию вставки.

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

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

3.1.7. Выгрузка информации из БД

Сперва дорабатываем функцию main:

Обновлённый код выделен
Обновлённый код выделен

Далее пишем функцию печати. Первая часть кода:

Код печати данных из таблицы
Код печати данных из таблицы

Вторая часть кода:

-60

Запускаем приложение:

Вывод в терминале
Вывод в терминале

Получили вывод содержимого БД в терминал. Переходим к следующей части.

3.2. V2 приложение

Поскольку мы изучаем миграции, нужно придумать что-то такое, что потребует изменить схему БД. Пусть это будет получение гео-данных по ip. Также я решил, что мало интересного искать данные просто по IP - интереснее ввести домен и получить информацию о нём, включая IP. Вот этим и будем заниматься в расширении функционала приложения.

3.2.1. Источник гео-информации

Чтобы получить гео-информацию по ip, мы воспользуемся сайтом (точнее, API), на котором хранится гео-информация по ip:

https://ip-api.com/
https://ip-api.com/

Вот как выглядит полученная информация из браузера, если напрямую ввести в поисковую строку запрос: http://ip-api.com/json/129.222.0.0

Отображение информации
Отображение информации

У меня установлено приложение для форматирование json-объектов в браузере. Без расширения будет скорее всего что-то такое, менее читаемое:

Отображение json-объектов в бра
Отображение json-объектов в бра

Итак, с источником информации разобрались, переходим к реализации бизнес-логики.

3.2.2. Обработка флагов ip и domain

Начнём с реализации поиска хоста по ip и ip по домену.

Выведем парсинг флагов в отдельную функцию:

Парсинг данных
Парсинг данных

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

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

Код разбора флагов
Код разбора флагов

Из интересного здесь - преобразование типа string в тип []net.IP в строке кода 77 и преобразование типа []net.IP обратно в тип string в строке кода 89.

Также по названию функции - назвал IPAndHost подразумевая, что получаю IP и хост в этой функции. Логичнее было бы написать getIPAndHost, однако слышал, что в среде разработчиков Go есть негласное правило не приписывать get к наименованию функции.

Доработаем функцию main, вызывая созданные функции:

Новый код выделен
Новый код выделен

Запустим, посмотрим что получили при передаче аргументом домена через флаг -domain vk.com:

Вывод терминала
Вывод терминала

Так, код работает. Ради интереса возьмём любой выданный ip сайта vk.com и запустим с флагом -ip 87.240.132.67:

Вывод терминала
Вывод терминала

Как видим, приложение также работает. Переходим к получению расширенных данных по ip.

3.2.3. Гео-информация

Создадим структуру данных, которые хотим собирать. Я решил собрать такие данные:

Структура гео-данных по IP
Структура гео-данных по IP

Я не прописал джейсон-тег для поля Host, т.к. источник гео-информации не предоставляет его, и мы заполним это поле ранее найденной информацией.

Далее соберём гео-информацию функцией geoInfo:

Код поиска гео-информации по ip
Код поиска гео-информации по ip

Здесь мы подключаемся к интернет-API. Создаём экземпляр структуры, описанной выше, и кладём в неё данные из запроса за счёт json-тегов в описании структуры.

В константы я добавил домен нашего источника данных для создания к нему Get-запроса:

домен сайта
домен сайта

Далее я написал функцию печати структуры в терминал:

Печать данных
Печать данных

А затем доработали функцию main:

Добавленный код выделен
Добавленный код выделен

Запускаем приложение:

Вывод в терминал
Вывод в терминал

Всё отлично работает. Пишем миграции для обновления схемы БД.

3.2.4. Пишем миграции к схеме БД

Задачи у нас две:

  1. Изменить тип данных в столбце БД, чтобы временная метка генерировалась внутри БД, а не передавалась извне.
  2. Добавить новые колонки.

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

Файл создания миграции выглядит так:

Создание миграции
Создание миграции

Файл отмены миграции выглядит так:

Отмена миграции. *в комментариях перепутаны старые-новые таблицы
Отмена миграции. *в комментариях перепутаны старые-новые таблицы

Проверим схему БД в sqlite3:

Схема БД
Схема БД

Схема БД та, что мы ожидали. Переходим к заполнению БД новыми данными.

3.2.5. Дорабатываем операции вставки

Доработаем функцию insertInfo. Прежде всего, сделаем её методом:

Доработка функции
Доработка функции

Остальной код функции без изменений. Функция main дорабатывается, думаю - понятно как - убрали передачу ip и хоста, добавили метку экземпляра структуры.

Проверяем содержимое БД:

Содержимое БД
Содержимое БД

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

Можно на этом заканчивать, т.к. публикация итак получилась длинной. Из домашней работы, что ещё здесь напрашивается сделать:

  1. Распределить код на слои с применением интерфейсов. Сейчас код идёт единым полотном на 321 строку кода.
  2. Добавить вывод всей информации в терминал из БД.
  3. Добавить тесты.

Ссылка на код в GitHub.

4. Выводы

Мы познакомились с миграцией схем базы данных и попрактиковали их разработку. Это большое подспорье в дальнейшей работе с базами данных не важно в какой СУБД - SQLite, PostrgeSQL или других. Также потренировались в работе с пакетом http/net и сопутствующим ему пакетах net/url, encoding/json и других.

Предстоит ещё немало работы, чтобы начать разбираться в БД хотя бы на начальном уровне; например, в ходе работы возникало "состояние грязной схемы БД" - я пока не разобрался, как восстанавливать схему; а также как применять и откатывать миграции через терминал, а не кодом в приложении.

Также будет полезно научиться применять миграции по правилам 12 шагов, обозначенных выше: с транзакциями.

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

Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨‍💻👩‍💻👨‍💻