Опыт использования Gorm и некоторых распространенных расширений (2)

Go

существуетПредыдущийВ этой статье я поделился некоторым опытом и расширениями своего использования gorm в новых и обновленных сценариях. В этой статье я поделюсь своим опытом создания запросов.

Во-первых, я разделил запрос на количество задействованных таблиц:

  • Запрос одной таблицы
  • Многотабличный запрос

По объему запроса его можно разделить на:

  • запрос один
  • запрос диапазона
    • запросить группу
    • упорядоченный запрос
    • запросить первые несколько
    • Пейджинговый запрос

При ежедневном использовании однотабличный запрос занимает большую часть сцены. Инкапсуляция этой части кода в соответствии с областью запроса может значительно уменьшить избыточный код.

Запрос одной таблицы

Итак, я следовал стилю API gorm и сделал следующую инкапсуляцию:

ps: в следующих примерах предполагается, что пользовательский объект был определен

запрос один

func (dw *DBExtension) GetOne(result interface{}, query interface{}, args ...interface{}) (found bool, err error) {
	var (
		tableNameAble TableNameAble
		ok            bool
	)

	if tableNameAble, ok = query.(TableNameAble); !ok {
		if tableNameAble, ok = result.(TableNameAble); !ok {
			return false, errors.New("neither the query nor result implement TableNameAble")
		}
	}

	err = dw.Table(tableNameAble.TableName()).Where(query, args...).First(result).Error

	if err == gorm.ErrRecordNotFound {
		dw.logger.LogInfoc("mysql", fmt.Sprintf("record not found for query %s, the query is %+v, args are %+v", tableNameAble.TableName(), query, args))
		return false, nil
	}

	if err != nil {
		dw.logger.LogErrorc("mysql", err, fmt.Sprintf("failed to query %s, the query is %+v, args are %+v", tableNameAble.TableName(), query, args))
		return false, err
	}

	return true, nil
}

Этот абзац стоит пояснить об обработке, когда данные не могут быть запрошены, сообщает горм.gorm.ErrRecordNotFoundОшибка, я сделал специальную обработку этой ошибки и выразил это особое состояние с помощью логического значения found.

Код вызова следующий:

condition := User{Id:1}
result := User{}

if  found, err := dw.GetOne(&result, condition); !found {
	//not found
    if err != nil {
    	// has error
        return err
    }
    
}

Его также можно написать так, чтобы более гибко указывать условия запроса:

result := User{}

if  found, err := dw.GetOne(&result, "id = ?", 1); !found {
	//not found
    if err != nil {
    	// has error
        return err
    }
    
}

Операторы, выполняемые в обоих способах написания:

select * from test.user where id = 1

запрос диапазона

Для четырех типов запросов страны я сделал следующий пакет:


func (dw *DBExtension) GetList(result interface{}, query interface{}, args ...interface{}) error {
	return dw.getListCore(result, "", 0, 0, query, args)
}

func (dw *DBExtension) GetOrderedList(result interface{}, order string, query interface{}, args ...interface{}) error {
	return dw.getListCore(result, order, 0, 0, query, args)
}

func (dw *DBExtension) GetFirstNRecords(result interface{}, order string, limit int, query interface{}, args ...interface{}) error {
	return dw.getListCore(result, order, limit, 0, query, args)
}

func (dw *DBExtension) GetPageRangeList(result interface{}, order string, limit, offset int, query interface{}, args ...interface{}) error {
	return dw.getListCore(result, order, limit, offset, query, args)
}

func (dw *DBExtension) getListCore(result interface{}, order string, limit, offset int, query interface{}, args []interface{}) error {
	var (
		tableNameAble TableNameAble
		ok            bool
	)

	if tableNameAble, ok = query.(TableNameAble); !ok {
		// type Result []*Item{}
		// result := &Result{}
		resultType := reflect.TypeOf(result)
		if resultType.Kind() != reflect.Ptr {
			return errors.New("result is not a pointer")
		}

		sliceType := resultType.Elem()
		if sliceType.Kind() != reflect.Slice {
			return errors.New("result doesn't point to a slice")
		}
		// *Item
		itemPtrType := sliceType.Elem()
		// Item
		itemType := itemPtrType.Elem()

		elemValue := reflect.New(itemType)
		elemValueType := reflect.TypeOf(elemValue)
		tableNameAbleType := reflect.TypeOf((*TableNameAble)(nil)).Elem()

		if elemValueType.Implements(tableNameAbleType) {
			return errors.New("neither the query nor result implement TableNameAble")
		}

		tableNameAble = elemValue.Interface().(TableNameAble)
	}

	db := dw.Table(tableNameAble.TableName()).Where(query, args...)
	if len(order) != 0 {
		db = db.Order(order)
	}

	if offset > 0 {
		db = db.Offset(offset)
	}

	if limit > 0 {
		db = db.Limit(limit)
	}

	if err := db.Find(result).Error; err != nil {
		dw.logger.LogErrorc("mysql", err, fmt.Sprintf("failed to query %s, query is %+v, args are %+v, order is %s, limit is %d", tableNameAble.TableName(), query, args, order, limit))
		return err
	}

	return nil
}

Чтобы уменьшить избыточный код, общая логика написана наgetListCoreВ функции используются некоторые знания об отражении голанга.

Но просто помните, что самая большая разница между отражением golang и отражением других языков заключается в том, чтоотражение golang для примитивных значений вместо типов, все легко понять.

Один из приемов заключается в том, как определить, реализует ли тип интерфейс, используя указатель на nil.

	elemValue := reflect.New(itemType)
	elemValueType := reflect.TypeOf(elemValue)
	tableNameAbleType := reflect.TypeOf((*TableNameAble)(nil)).Elem()

	if elemValueType.Implements(tableNameAbleType) {
		return errors.New("neither the query nor result implement TableNameAble")
	}

Что касается конкретного использования, то я не буду приводить примеры по одному, студенты, знакомые с gorm api, смогут увидеть это с первого взгляда.

Многотабличный запрос

Что касается многотабличных запросов, потому что сложно извлечь разные сценарии в разных сценариях, нет инкапсуляции, но мой опыт таков.Отдайте приоритет использованию метода gorm вместо самостоятельного написания sql. Вы можете делать все, что хотите.

Здесь я ленюсь и публикую самый сложный кусок кода, который я написал в проекте, для вашего развлечения.

сложный пример

Этот код взят из промежуточной таблицы данных скрытых точек. Чтобы использовать общий код для реализации запросов в различных сценариях отображения, дизайн кода стал более гибким, что включает запросы к нескольким связанным таблицам, динамическую фильтрацию и агрегирование в соответствии с условиями запроса, а также Логика запроса пагинации.

func buildCommonStatisticQuery(tableName, startDate, endDate string) *gorm.DB {
	query := models.DB().Table(tableName)

	if startDate == endDate || endDate == "" {
		query = query.Where("date = ?", startDate)
	} else {
		query = query.Where("date >= ? and date <= ?", startDate, endDate)
	}

	return query
}

func buildElementsStatisticQuery(startDate, endDate,  elemId string,  elemType int32) *gorm.DB {
	query := buildCommonStatisticQuery("spotanalysis.element_statistics", startDate, endDate)

	if elemId != "" && elemType != 0 {
		query = query.Where("element_id = ? and element_type = ?", elemId, elemType)
	}

	return query
}

func CountElementsStatistics(count *int32, startDate, endDate, instId, appId, elemId string, elemType int32, groupFields []string ) error {
	query := buildElementsStatisticQuery(startDate, endDate,  elemId, elemType)

	query = whereInstAndApp(query, instId, appId)

	if len(groupFields) != 0 {
		query = query.Select(fmt.Sprintf("count(distinct(concat(%s)))", strings.Join(groupFields, ",")))
	} else {
		query = query.Select("count(id)")
	}

	query = query.Count(count)
	return query.Error
}


func GetElementsStatistics(result interface{}, startDate, endDate, instId, appId, elemId string, elemType int32, groupFields []string, orderBy string, ascOrder bool, limit, offset int32) error {
	query := buildElementsStatisticQuery(startDate, endDate, elemId, elemType)
	if len(groupFields) != 0 {
		groupBy := strings.Join(groupFields, "`,`")
		groupBy = "`" + groupBy + "`"
		query = query.Group(groupBy)
		query = havingInstAndApp(query, instId, appId)

		sumFields := strings.Join([]string{
			"SUM(`element_statistics`.`mp_count`) AS `mp_count`",
			"SUM(`element_statistics`.`h5_count`) AS `h5_count`",
			"SUM(`element_statistics`.`total_count`) AS `total_count`",
			"SUM(`element_statistics`.`collection_count`) AS `collection_count`",
			"SUM(`element_statistics`.`mp_share_count`) AS `mp_share_count`",
			"SUM(`element_statistics`.`h5_share_count`) AS `h5_share_count`",
			"SUM(`element_statistics`.`poster_share_count`) AS `poster_share_count`",
			"SUM(`element_statistics`.`total_share_count`) AS `total_share_count`",
		}, ",")

		query = query.Select(groupBy + "," + sumFields)
	} else {
		query = whereInstAndApp(query, instId, appId)
	}

	query = getPagedList(query, orderBy, ascOrder, limit, offset)

	return query.Find(result).Error
}

func getPagedList(query *gorm.DB, orderBy string, ascOrder bool, limit , offset int32) *gorm.DB {
	if orderBy != "" {
		if ascOrder {
			orderBy += " asc"
		} else {
			orderBy += " desc"
		}
		query = query.Order(orderBy)
	}

	if offset != 0 {
		query = query.Offset(offset)
	}
	if limit != 0 {
		query = query.Limit(limit)
	}
	return query
}

func whereInstAndApp(query *gorm.DB, instId string, appId string) *gorm.DB {
	query = query.Where("inst_id = ?", instId)
	if appId != "" {
		query = query.Where("app_id = ?", appId)
	}
	return query
}

func havingInstAndApp(query *gorm.DB, instId string, appId string) *gorm.DB {
	query = query.Having("inst_id = ?", instId)
	if appId != "" {
		query = query.Having("app_id = ?", appId)
	}
	return query
}

Спасибо за ваше терпение, и если эта статья была вам полезна, пожалуйста, поставьте лайк~~~

Если вы можете перейти в репозиторий кода:Github:Ksloveyuan/gorm-exСтавьте ✩звезду✩, хозяин будет еще благодарен!