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

в 4:36, , рубрики: Программирование

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js