Работа с базой данных в Golang

от автора

В данном посте хочу рассказать об одном из способов работы с базой данных. Ни в коей мере не утверждаю, что он лучше чем другие возможные. Более того, с нетерпением жду вменяемой реализации 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 на ХэшКоде.

ссылка на оригинал статьи http://habrahabr.ru/post/178963/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *