Beego. Часть 2
Перевод второй части небольшого введения в фреймворк 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колько угодно.