Я начал разработку 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 на ХэшКоде.
ссылка на оригинал статьи http://habrahabr.ru/post/178963/
Добавить комментарий