В данном посте хочу рассказать об одном из способов работы с базой данных. Ни в коей мере не утверждаю, что он лучше чем другие возможные. Более того, с нетерпением жду вменяемой реализации ORM, чтобы отказаться от ручного управления сериализацией данных. По сути, в данной статье рассматривается один из подходов применяемый в наших веб-приложениях: http://sezn.ru/, http://hashcode.ru/ и http://careers.hashcode.ru/.
Я начал разработку http://careers.hashcode.ru/ более полутора лет назад. Первая версия сайта была завершена еще до выхода Go RC1. Проект является самым большим из всех, что я знаю, который разрабатывается вне Google.
Для начала определим структуру, которую мы будем запрашивать из базы. Поскольку ХэшКод посвящен вопросам и ответам, нашей структурой будет вопрос.
type Question struct {
Id int
Title int
Body string
TagsStr string
Score int
ViewCount int
Tags []Tag
}
Как вы могли заметить метки вопроса упоминаются дважды. Это необходимо, чтобы лишний раз не делать запрос по нескольким табличкам.
Структура метки достаточно проста.
type Tag struct {
Id int
UsedCount int
Name string
}
Вся модель сериализации построена на предположении, что большинство запросов идет по одной таблице базы, а если нам необходимо выбрать данные из нескольких таблиц, то мы делаем это в нескольких запросах. (Тесты не показали снижения производительности на большом количестве запросов.)
Предположим, что у нас есть вьюшка, для отображения списка вопросов (упрощена для наглядности).
func questionsListHandler(user UserInterface, w http.ResponseWriter, r *http.Request) * BasePage {
perPage, pageNum, orderBy := pageParams(r)
ctx := make(utils.Context)
ctx["User"] = user
ctx[“Questions”] = questionLoader.Load(perPage, (pageNum-1)*perPage, orderBy)
return &BasePage{
Title: QuestionsListPageTitle,
Body: Body{
Template: QuestionsListTmpl,
Data: ctx,
},
}
}
Нас интересует метод Load
. Он получает три параметра: количество объектов для выборки, смещение и сортировку.
func (self QuestionLoader) Load(offset, limit int, orderBy string) []*Question {
deps, names := self.defaultDependencies()
qp := makeQueryParam (
self.selectString(),
self.fromString(),
self.were(),
orderBy, limit, offset
)
return self.toQuestions(LoadDBModelWithCache(qp, self.extractor(), self.cacheBuilder(), deps, names))
}
Метод defaultDependencies
возвращает зависимости для данной модели (объекта базы). Структура Question
зависит от структуры Tag
.
func (self QuestionLoader) defaultDependencies() (map[string]DepFetcher, []string) {
dps := make(map[string]DepFetcher)
dps[tagLoader.tableName()] = TagByQuestionDependenceFetcher
return dps, []string{ tagLoader.tableName() }
}
DepFetcher
— это функция, которая будет вызвана для извлечения зависимости из базы.
func TagByQuestionDependenceFetcher(value interface{}) {
var question *Question
var ok bool
if question, ok = value.(*Question); !ok {
return
}
result := LoadDBModelWithCache(/* Запрос аналогичен запросу модели Question*/)
question.Tags = tagLoader.toTags(result)
}
Методы selectString
и fromString
возвращают строки, необходимые для запроса. В них нет ничего интересного. Метод were
возвращает структуру WhereParam
.
type WhereParam struct {
Query string
WhereAttr []WhereAttr
}
type WhereAttr struct {
Name string
Value interface{}
}
В поле Query
хранится строка, представляющая запрос. В WhereAttr
параметры. Ниже приведен упрощенный пример выборки вопроса по id.
func (self QuestionLoader) wereById (tblaliase, id int) *WhereParam {
wp := new(WhereParam)
wp.Query = "WHERE " + self.tblaliase() + ".id = @id"
wp.WhereAttr = []WhereAttr{
WhereAttr{
Name: "@id",
Value: id,
},
}
return wp
}
Функция makeQueryParam
возвращает сформированный запрос к базе в виде структуры QueryParam
.
type QueryParam struct {
Select string
From string
Where WhereParam
OrderBy string
Offset string
Limit string
Ext string
}
Метод извлечения данных из базы:
func LoadDBModelWithCache(qp *QueryParam,
resultMaker ResultMaker,
cacheBuilder CacheBuilder,
dependences map[string]DepFetcher,
dependencesNames []string) []interface{} {
if qp == nil {
return nil
}
var results []interface{} = nil
query := qp.String()
params := qp.Where.WhereAttr
sqlQuery := ReplaceParameters(query, params)
key := generateMd5Sum(sqlQuery)
ms := GetCacheSystem().GetStore()
if cacheBuilder != nil {
cachedBytes := cacheBuilder.Try(sqlQuery, key, params)
if cachedBytes != nil && len(cachedBytes) > 0 {
results = cacheBuilder.Extract(cachedBytes)
if results == nil {
ms.Delete(key)
}
}
}
if results == nil {
db, err := sql.Open(postgresDriverName, connection)
HandleDataBaseError(err)
defer db.Close()
prepQuery := PrepereQuery(query, params)
prepParams := PrepereQueryParams(params)
rows, err := db.Query(prepQuery, prepParams...)
HandleDataBaseError(err)
if rows != nil {
defer rows.Close()
results = resultMakerDecorator(rows, resultMaker)
if cacheBuilder != nil {
ms.EasySaveArray(key, results)
cacheBuilder.Descripe(results, key)
}
} else {
Logger.Println("Db query error:" + sqlQuery)
}
}
for i, _ := range results {
for _, depname := range dependencesNames {
fatcher := dependences[depname]
if fatcher != nil {
fatcher(results[i])
}
}
}
return results
}
Метод достаточно объемный, пойдем по порядку. На входе мы имеем: структуру c данными о запросе, метод, для формирования результата, объект менеджера кэша, список зависимостей и их имена.
Для начала мы преобразуем строку запроса и параметры, заменяя псевдонимы реальными значениями. Полученная строка используется как ключ к кэшу (за одно, можно выводить данное значения в лог отладки).
Получив ключ, мы пытаемся получить данные из memcached
. В случае успеха, формируем результат.
Если по данному ключу ничего нет, мы делаем запрос к базе данных. Получив данные из базы, заносим их в кэш.
На следующем шаге мы извлекаем все зависимости отдельными запросами. Здесь следует отметить две особенности. Первое — данные полученные из базы ничем не отличаются от данных из кэша, и представлены массивом сериализованных структур. Второе — существует аналогичная функция, которая извлекает данные за одно соединение с базой, но в разных запросах.
Функция извлечения данных из ячеек базы (ResultMaker
):
func tagFromResultSet (rows * sql.Rows) interface{} {
t := Tag{}
err := rows.Scan(&t.Id, &t.Name)
HandleDataBaseError(err)
return t
}
Последним нюансом является приведение типа.
func (self * QuestionLoader) toQuestions(queryResult []interface{}) []*Questions{
if queryResult == nil { return nil }
qrLen := len(queryResult)
if qrLen <= 0 { return nil }
resultSet := make([]*Questions, qrLen)
for index, result := range queryResult {
if val, ok := result.(*Questions); ok {
resultSet[index] = val
} else {
resultSet[index] = &Questions{}
}
}
return resultSet
}
На этом все. В следующем посте постараюсь кратко показать как сохранять, удалять и изменять данные. Буду рад ответить на ваши вопросы в комментариях к посту или в ветке по Go на ХэшКоде.
Автор: DevExpert