Beego. Часть 2

40 minute read

Перевод второй части небольшого введения в фреймворк Beego от Matthew Setter. Оригинал тут. Перевод первой части.

Приветствую читалелей 2-й части серии, в которой мы продолжаем изучение веб-фрейморка Beego написанного на Go. Если вы пропустили первую часть, то я рекомендую вернуться к ней, так ка там изложены фундаментальный принципы, которые мы будем использовать.

В первой части у нас получился неплохой старт. Мы начали понимать принципы работы с Beego, установили этот фреймворк и тулзу Bee для работы с ним, создали базовый проект, добавили экшены к контроллеру, создали шаблон представления, добавили кастомный роут и закончили на работе с параметрами запроса

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

Вложенные представления

Наверняка, вы обратили внимание на код в функциях контроллера:


manage.Layout = "basic-layout.tpl"
manage.LayoutSections = make(map[string]string)
manage.LayoutSections["Header"] = "header.tpl"
manage.LayoutSections["Footer"] = "footer.tpl"

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

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

Используя параметры Layout и LayoutSections мы можем указать базовое представление(layout) basic-layout.tpl и сабтемплейты, в нашем случае это заголовок(header.tpl) и подвал(footer.tpl).

После того, как мы это сделаем, сгенерированный экшеном контент вставляется в шаблон представления с помощью {{.LayoutContent}}, а заголовок и подвал доступны в переменных {{.Header}} и {{.Footer}} соответственно.

Модели

Чтобы добавить возможность работы с базой данных, мы должны сделать несколько вещей. Для начала нам нужно создать несколько моделей. Модели - это просто обычные структуры с некоторой дополнительной информацией. Ниже показан файл models/models.go с моделями, которые можно будет использовать по всему сайту


package models

type Article struct {
    Id     int    `form:"-"`
    Name   string `form:"name,text,name:" valid:"MinSize(5);MaxSize(20)"`
    Client string `form:"client,text,client:"`
    Url    string `form:"url,text,url:"`
}

func (a *Article) TableName() string {
    return "articles"
}

Как вы можете видеть, тут представлена одна модель - Article. Это простое описание статьи, которое содержит четыре параметра: Id, Name, Client и Url. Заметьте, для каждого поля отдельно указана метаинформация, которая будет использоваться в формах и при валидаци.

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


Id int `form:"-"`

В нашей базе данных id - это автоинкрементное поле. Это прекрасно работает, когда нужно добавить новую запись в табличку, нам не нужно руками указывать id, только при удалении, обновлении или поиске. И так, указываем тег для этого поля как form:"-", что обозначает необязательность поля Id.


Name   string `form:"name,text,name:" valid:"MinSize(5);MaxSize(20)"`

Это немного более комплексный пример. Давайте начнем с "name,text,name:". Вот что это будет обозначать, когда будет строиться форма.

  • Значения поля с именем name будет инициализированное значением из параметра Name
  • Это будет текстовым полем
  • Лейбл для этого поля будет name

Теперь посмотрим на valid:"MinSize(5);MaxSize(20)". Это специальные правила валидации: MinSize и MaxSize. Это означает, что значение поля должно быть не короче 5 символов, но и не длиннее 20.

Тут можно посмотреть все правила валидации, которыми можно пользоваться, включая Range, Email, IP, Mobile, Base64 и Phone.


Client string `form:"client,text,client:"`
Url    string `form:"url,text,url:"`

Последние две записи означают, что в параметр Client попадает значение из поля формы client, которое является текстовым инпутом с лейблом "client:". В параметр Url попадают значения из поля url, которое так же является текстовым инпутом с лейблом url:. Теперь посмотрим на функцию TableName

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

Я специально внес такое различие, что бы показать что необходимо сделать в случае различных имен таблицы и структуры. И раз уж мы начали говорить про структуру таблицы, давайте посмотрим на нее внимательней.


CREATE TABLE "articles" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "name" varchar(200) NOT NULL,
    "client" varchar(100),
    "url" varchar(400) DEFAULT NULL,
    "notes" text,
    UNIQUE (name)
);

Интеграция моделей внутри приложения

Мы создали и настроили наши модели, добавили информацию для валидации и создания форм. Теперь нам нужно сделать модели доступными в приложении. Мы должны добавить импорт моделей и дополнительных пакетов в main.go.


"github.com/astaxie/beego/orm"
_ "github.com/mattn/go-sqlite3"
models "sitepointgoapp/models"

Первый импорт - это подключение ORM библиотеки, второй импорт подключает драйвер для SQLite3, который обязателен при использовании этой базы. Третий импорт - это подключение наших моделей.


func init() {
    orm.RegisterDriver("sqlite", orm.DR_Sqlite)
    orm.RegisterDataBase("default", "sqlite3", "database/orm_test.db")
    orm.RegisterModel(new(models.Article))
}

На финальном шаге мы должны зарегистрировать драйвер, базу данных и наши модели, которые будем использовать в приложении. Мы делаем это в три этапа, показанные выше. Указываем, что мы используем SQLite и устанавливаем его для дефолтного соединения с нашей базой данных, которая находится в файле database/orm_test.db.

И в конце мы регистрируем только одну модель, которую будем использовать models.Article.

CRUD Операции

После всех этих манипуляций у нас есть настроенное соединение с базой данных и ее интеграция с нашим приложением. Давайте начнем с двух простых CRUD операций: удаление(D) и обновление(U). Ни одна из этих операций не содержит форму, потому что я максимально упрощаю экшен и концентрируюсь на ORM коде без учена построения формы или валидации. Мы работаем в Add экшене.

Удаление записи

Создадим экшен удаления, в котором будем пытаться удалить некоторую статью из нашей базы данных по параметру id. В файле routers/routers.go добавим еще один роут в init функции.


beego.Router("/manage/delete/:id([0-9]+)", &controllers.ManageController{}, "*:Delete")

Теперь добавим новый экшен Delete в файл controllers/manage.go`.


func (manage *ManageController) Delete() {
    // convert the string value to an int
    articleId, _ := strconv.Atoi(manage.Ctx.Input.Param(":id"))

Здесь мы получаем параметр id и конвертируем его в тип int из типа string используя для этого функцию Atoi из пакета strconv. Это очень простой пример, в котором пропущена обработка ошибок и сразу сохраняем значение параметра в переменной articleId.


o := orm.NewOrm()
o.Using("default")
article := models.Article{}

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


    // Check if the article exists first
    if exist := o.QueryTable(article.TableName()).Filter("Id", articleId).Exist(); exist {
        if num, err := o.Delete(&models.Article{Id: articleId}); err == nil {
            beego.Info("Record Deleted. ", num)
        } else {
            beego.Error("Record couldn't be deleted. Reason: ", err)
        }
    } else {
        beego.Info("Record Doesn't exist.")
    }
}

Это сердце функции. Для начала, делаем запрос, проверяем существует ли статья, с указанным Id. Если статья существует, мы вызываем метод Delete, передаем ему объект нашей статьи только с одним указанным полем Id.

Если никаких ошибок не произошло, то статья удаляется и вызывается функция beego.Info, которая записывает в лог, что статья удалена. Если мы не можем удалить статью, то вызываем функцию Error, которая, кроме всего прочего, принимает в качестве параметра объет ошибки err, в котором указано что именно пошло не так и почему статья не удалена.

Обновление записи

Мы научились удалять записи, давайте теперь попробуем что ни будь обновить и заодно научимся использовать флеш-сообщения.


func (manage *ManageController) Update() {
    o := orm.NewOrm()
    o.Using("default")
    flash := beego.NewFlash()

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


// convert the string value to an int
if articleId, err := strconv.Atoi(manage.Ctx.Input.Param(":id")); err == nil {
    article := models.Article{Id: articleId}

Здесь мы пытаемся получить id параметр и инициализировать новую модель Article если это возможно.


if o.Read(&article) == nil {
    article.Client = "Sitepoint"
    article.Url = "http://www.google.com"
    if num, err := o.Update(&article); err == nil {
        flash.Notice("Record Was Updated.")
        flash.Store(&manage.Controller)
        beego.Info("Record Was Updated. ", num)
    }

Потом мы вызываем Read метод и передаем в него объект Article и этот метод пытается загрузить все остальные параметры статьи из базы данных из записи в котрой есть id равный указанному нами в Article.Id.

Как только это возможно, мы устанавливаем значения Client и Url и передаем этот объект в метод Update, который обновляет запись в базе данных.

Если после этого у нас нет никаких ошибок, мы вызываем метод Notice у объекта Flash, указываем просто сообщение м вызываем метод Store для сохранения информации.


} else {
    flash.Notice("Record Was NOT Updated.")
    flash.Store(&manage.Controller)
    beego.Error("Couldn't find article matching id: ", articleId)
} else {
    flash.Notice("Record Was NOT Updated.")
    flash.Store(&manage.Controller)
    beego.Error("Couldn't convert id from a string to a number. ", err)
}

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


    // redirect afterwards
    manage.Redirect("/manage/view", 302)
}

В самом конце мы вызываем метод Redirect, передаем ему url на который мы должны перейти и HTTP код статуса. Редирект на /manage/view произойдет независимо от того обновилась модель или нет.

Просмотр всех записей

View функция состоит из двух блоков. Во-первых отображаем все статьи из таблицы, во вторых показываем все накопившиеся флеш сообщения. По флеш сообщениям сразу будет понятно как прошла операция.


func (manage *ManageController) View() {
    flash := beego.ReadFromRequest(&manage.Controller)

    if ok := flash.Data["error"]; ok != "" {
        // Display error messages
        manage.Data["errors"] = ok
    }

    if ok := flash.Data["notice"]; ok != "" {
        // Display error messages
        manage.Data["notices"] = ok
    }

Первым делом, инициализируем переменнуюflash и пытаемся получить два значения error и notice. Это результаты вызовов функций flash.Error и flash.Notice соответственно. Если эти значения установленны, мы сохраняем эту информацию в Data и в дальнейшем можем получить к ней доступ.


    o := orm.NewOrm()
    o.Using("default")

    var articles []*models.Article
    num, err := o.QueryTable("articles").All(&articles)

    if err != orm.ErrNoRows && num > 0 {
        manage.Data["records"] = articles
    }
}

Как и в предыдущих примерах, сначала мы указываем какое соединение нужно использовать, после этого инициализируем слайс моделей Article в переменной articles. Затем, вызываем метод QueryTable указывая имя таблицы. И вызываем метод All, которому передаем указатель на слайс для наших статей, в который будет загружен весь результат.

Если все прошло хорошо, то мы получим список всех статей, который сможем передать в шаблон представления.

Вставка записи

Теперь посмотрим как добавлять новые записи используя метод Add, который получает данные из формы и валидирует их.


func (manage *ManageController) Add() {
    o := orm.NewOrm()
    o.Using("default")
    article := models.Article{}

Это я пропущу, мы подробно рассматривало это ранее.


if err := manage.ParseForm(&article); err != nil {
    beego.Error("Couldn't parse the form. Reason: ", err)
} else {
    manage.Data["Articles"] = article

Здесь вызывается метод ParseForm передавая ему article объект. Если никаких ошибок не произошло мы передаем переменную article в шаблон для более простого рендеринга формы.


if manage.Ctx.Input.Method() == "POST" {
valid := validation.Validation{}
isValid, _ := valid.Valid(article)
if !isValid {
    manage.Data["Errors"] = valid.ErrorsMap
    beego.Error("Form didn't validate.")
} else {

В этом месте проверяется тип запроса. Нам нужно обрабатывать только POST. Если это так, то мы инициализируем новый объект валидации и передаем в метод Valid объект artcile для валидации данных из POST согласно правилам в моделе.

Если данные не проходят валидацию, то все ошибки сохраняются и будут доступны в параметре valid.ErrorsMap. Все ошибки будут доступны в шаблоне в переменной Errors. В противном случае, мы сохраняем новую статью в базе. Если при этом произошла ошибка, мы логируем ее.


id, err := o.Insert(&article)
if err == nil {
    msg := fmt.Sprintf("Article inserted with id:", id)
    beego.Debug(msg)
} else {
    msg := fmt.Sprintf("Couldn't insert new article. Reason: ", err)
    beego.Debug(msg)
}

Итоги

Мы закончили обзор фреймворка Beego. Еще очень много фич осталось нерассмотренными, но они бы не поместились в эти две статьи.

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

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