Хорошие новости! Официальный драйвер go для mongoDB вышел в бета-версию. Немного поразмыслив я решил перевести статью с официального сайта mongoDB вместо того, чтобы писать материал самостоятельно. Вот что будет в этом туториале:


  • Установка mongo-go-driver
  • Соединение с mongoDB с помощью mongo-go-driver
  • Использование BSON объектов
  • Использование CRUD методов
    image
    Исходный код этого руководства находится в [этом GitHub репозитории](https://github.com/tfogo/mongodb-go-tutorial). Для работы с этим руководством требуется установленная MongoDB версии не ниже 3.2 и Golang 1.10 или старше.

Установка mongo-go-driver


Драйвер состоит из нескольких пакетов.
Вы можете установить драйвер с помощью go get:


go get github.com/mongodb/mongo-go-driver

Вывод может содержать в себе следующее предупреждение:


package github.com/mongodb/mongo-go-driver: no Go files in (...)


Это ожидаемый результат.


прим. переводчика: лично я использую модули и там все прекрасно.


Если вы используете в качестве менеджера для управления зависимостсями dep, то вам придется установить основной пакет mongo, также пакет bson и mongo/options:


dep ensure --add github.com/mongodb/mongo-go-driver/mongo github.com/mongodb/mongo-go-driver/bson github.com/mongodb/mongo-go-driver/mongo/options

Создание основы


Создайте проект и добавьте в него файл main.go со следующим содержимым:


package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mongodb/mongo-go-driver/bson"
    "github.com/mongodb/mongo-go-driver/mongo"
    "github.com/mongodb/mongo-go-driver/mongo/options"
)

// You will be using this Trainer type later in the program
type Trainer struct {
    Name string
    Age  int
    City string
}

func main() {
    // Rest of the code will go here
}

В этом коде мы импортируем mongo-go-driver, некоторые стандартные библиотеки и определяем тип Trainer.


Соединение с mongoDB с помощью mongo-go-driver


После того как мы импортировали mongo-go-driver, у нас появилась возможность создать соединение с mongoDB использую функцию mongo.Connect(). У нас есть возможность передать в эту функцию в качестве аргуменов context и стороку содержащую адрес подключения к mongodb. При желании мы также можем передать в качестве третьего аргумента объект options.ClientOptions для тонкой настройки параметров драйвера. В документации подробно описаны доступные парпметры.


Давайте добавим следующий код в тело функции main:


client, err := mongo.Connect(context.TODO(), "mongodb://localhost:27017")

if err != nil {
    log.Fatal(err)
}

// Check the connection
err = client.Ping(context.TODO(), nil)

if err != nil {
    log.Fatal(err)
}

fmt.Println("Connected to MongoDB!")

После успешного соединения с mongoDB мы можем обратиться к коллекции trainers, которая находится в базе с именем test добавив следующий код в конец функции main:


collection := client.Database("test").Collection("trainers")

Рекомендуется держать соединение с mongoDB открытым, чтобы не приходилось открывать и закрывать соединение для каждого запроса. Однако, если приложение больше не требует подключения, оно может быть закрыто с помощю функции client.Disconnect() следующим образом:


err = client.Disconnect(context.TODO())

if err != nil {
    log.Fatal(err)
}
fmt.Println("Connection to MongoDB closed.")

Закомментируйте неиспользуемые пакеты bson и mongo/options.


Запустите наш код (go run main.go) чтобы проверить подключение к серверу mongoDB.


Использовение BSON объектов


JSON документы в mongoDB хранятся в двоичном формате, называемом BSON. В отличие от других баз данных в которых JSON данные хранятся в виде строк и чисел, кодировка BSON добавляет новые типы, такие как int, long, date, float и decimal128.


Это значительно упрощает обработку, сортировку и сравнение данных приложениями. Драйвер Go имеет два семейства типов для представления данных BSON: Типы D и типы RAW.


Семейство D состоит из четырех типов:


  • D: документ BSON. Этот тип следует использовать в ситуациях, когда порядок имеет значение, например, команды MongoDB.
  • M: Неупорядоченный словарь(ассоциативный массив, map). Он такой же, как D, за исключением того, что он не сохраняет порядок.
  • A: массив BSON.
  • E: одиночный элемент внутри D.

Вот пример фильтра, построенного с использованием D-типов, который производит поиск документов, в которых поле name соответствует значениям Alice или Bob:


Семейство типов Raw используется для проверки среза байтов. Вы можете извлечь одиночные элементы из типов Raw, используя Lookup().


Это может быть полезно когда необходимо избавиться от лишней нагрузки при конвертировании BSON в иной тип. В этом туториале будет использоваться только семейство типов D.


Использование CRUD методов


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


Вставка (создание) документов


Для начала необходимо создать несколько новых структур Trainer для вставки в базу данных:


ash := Trainer{"Ash", 10, "Pallet Town"}
misty := Trainer{"Misty", 10, "Cerulean City"}
brock := Trainer{"Brock", 15, "Pewter City"}

Для того чтобы всавить одиночный документ следует использовать метод collection.InsertOne():


insertResult, err := collection.InsertOne(context.TODO(), ash)
if err != nil {
    log.Fatal(err)
}

fmt.Println("Inserted a single document: ", insertResult.InsertedID)

Для вставки нескольких документов одновременно существут метод collection.InsertMany():


trainers := []interface{}{misty, brock}

insertManyResult, err := collection.InsertMany(context.TODO(), trainers)
if err != nil {
    log.Fatal(err)
}

fmt.Println("Inserted multiple documents: ", insertManyResult.InsertedIDs)

Обновление документов


Метод collection.UpdateOne() позволяет обновить один документ. Требуется создать фильтр для поиска документа в базе данных и документ для операции обновления. Вы можете создать их, используя типы bson.D:


filter := bson.D{{"name", "Ash"}}

update := bson.D{
    {"$inc", bson.D{
        {"age", 1},
    }},
}

Следующий код найдет документ, в котором поле name совпадает со значением Ash и увеличит значение age на 1.


updateResult, err := collection.UpdateOne(context.TODO(), filter, update)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Matched %v documents and updated %v documents.\n", updateResult.MatchedCount, updateResult.ModifiedCount)

Поиск документов


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


Чтобы найти один документ, используйте collection.FindOne(). Этот метод возвращает одно значение, которое можно декодировать в переменную.


Мы будем использовать ту же переменную фильтра, которую использовали в запросе на обновление.


// create a value into which the result can be decoded
var result Trainer

err = collection.FindOne(context.TODO(), filter).Decode(&result)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Found a single document: %+v\n", result)

Чтобы найти несколько документов, используйте collection.Find().


Этот метод возвращает Cursor. Cursor предоставляет поток документов, с помощью которого можно перебирать и декодировать по одному документу за раз.


Когда документы в Cursor исчерпываются, следует закрыть Cursor. Также Cursor можно тонко настроить использую пакет options.


В нашем примере, мы устанавливаем лимит на выдачу двух документов.


// Pass these options to the Find method
options := options.Find()
options.SetLimit(2)

// Here's an array in which you can store the decoded documents
var results []*Trainer

// Passing nil as the filter matches all documents in the collection
cur, err := collection.Find(context.TODO(), nil, options)
if err != nil {
    log.Fatal(err)
}

// Finding multiple documents returns a cursor
// Iterating through the cursor allows us to decode documents one at a time
for cur.Next(context.TODO()) {

    // create a value into which the single document can be decoded
    var elem Trainer
    err := cur.Decode(&elem)
    if err != nil {
        log.Fatal(err)
    }

    results = append(results, &elem)
}

if err := cur.Err(); err != nil {
    log.Fatal(err)
}

// Close the cursor once finished
cur.Close(context.TODO())

fmt.Printf("Found multiple documents (array of pointers): %+v\n", results)

Удаление документов


Вы можете удалить документы, используя collection.DeleteOne() или collection.DeleteMany().
Мы передаем nil в качестве аргумента фильтра, который будет соответствовать всем документам в коллекции. Также можно использовать collection.Drop() для удаления всей коллекции.


deleteResult, err := collection.DeleteMany(context.TODO(), nil)
if err := cur.Err(); err != nil {
    log.Fatal(err)
}
fmt.Printf("Deleted %v documents in the trainers collection\n", deleteResult.DeletedCount)

Далнейшие шаги


> Финальный код этого туториала находится в репозитории GitHub
> Документация к драйверу достпна в GoDoc


Если у вас есть какие-либо вопросы, пожалуйста, свяжитесь с нами в группе Google mongo-go-driver.
Пожалуйста, отправляйте отчеты об ошибках в MongoDB JIRA.
Мы будем рады получить ваши отзывы о Go Driver.

Комментарии (12)


  1. PYXRU
    20.12.2018 15:42

    Писал, недавно свой такой, getOne мне кажется в моем случае более оптимизированный

    type Record struct {
    	Hello string `bson:"hello"`
    }
    
    record := new(Record)
    
    err = mgoConnector.GetOne(TEST_COLLECTION, bson.M{
        "hello": "test",
    }, record)
    
    fmt.Println((*record).Hello)


    Если интересно могу выложить в opensource, правда операций поменьше:

    type MongoConnector struct {
    	Connect        func(mongoUrl string, databaseName *string) error
    	InsertOne      func(collectionName string, entity interface{}) error
    	UpdateOne      func(collectionName string, findPredicate bson.M, updatePredicate bson.M) error
    	UpdateAll      func(collectionName string, findPredicate bson.M, updatePredicate bson.M) (*mgo.ChangeInfo, error)
    	GetOne         func(collectionName string, findPredicate bson.M, structToDeserialize interface{}) error
    	GetAll         func(collectionName string, findPredicate bson.M, structToDeserialize interface{}) error
    	DropCollection func(collectionName string) error
    	Disconnect     func()
    }


    1. pocoZ Автор
      20.12.2018 15:45

      Вы можете поделиться своими идеями с разработчиками здесь и здесь


    1. pocoZ Автор
      20.12.2018 15:51

      А GetAll у вас сразу декодирует результат в массив или приходится перебирать Cursor?
      mgo декодирует сразу в массив и такой вариант лично мне нравится больше. Пока я не осознал пользы Cursor.


      1. PYXRU
        20.12.2018 15:53

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


        1. pocoZ Автор
          20.12.2018 15:55

          Может стоит отправить это в open source и написать здесь мануал?


  1. Ddnn
    20.12.2018 21:18

    Хм, как, однако, много изменилось с альфы. Буквально пару месяцев назад(0.0.17) работа с bson вообще по-другому выглядела — использовалось куча

    бойлерплейтных билдеров
    .UpdateOne(nil,
    		bson.NewDocument(bson.EC.String("_id", access.UserID)),
    		bson.NewDocument(
    			bson.EC.SubDocumentFromElements("$addToSet",
    				bson.EC.SubDocumentFromElements("documents",
    					bson.EC.Array("$each", documents),
    				),
    			),
    			bson.EC.SubDocumentFromElements("$setOnInsert",
    				bson.EC.String("_id", access.UserID),
    			),
    		),
    		updateopt.Upsert(true),
    )


    1. pocoZ Автор
      20.12.2018 22:23

      Сейчас посмотрел. Нет, не появилось. Но вы можете свои предложения отправить сюда и сюда


  1. taliban
    21.12.2018 14:37

    Не понимаю зачем все время совать контекст везде? Какой смысл в это вкладывают создатели драйвера? Есть ли возможность не использовать его? Задать дефолтный?


    1. pocoZ Автор
      21.12.2018 14:41

      Это сделано для возможности создания так называемого Graceful Shutdown. Не использовать его нельзя, но вы можете туда отправлять что-то вроде context.TODO(). Но лично мне проброс контекста очень нравится, потому что ошибки при окончании работы приложения сведутся к минимуму.


      1. taliban
        21.12.2018 16:27

        Я если честно все еще не понимаю как он связан с Graceful Shutdown. А проброс контекста требует в бизнес логике помимо коллекции принимать еще контекст, и так везде. Тот же mgo не требует контекста, в нем невозможен Graceful Shutdown? И где можно почитать про него? Беглое гугление не дало результатов.


        1. pocoZ Автор
          21.12.2018 16:34

          Да, mgo частично лишен этого функционала. В первую очередь почитайте документацию по context.


          1. taliban
            21.12.2018 16:41

            Благодарю, просто я руками закрываю соединение в скриптах, думал этого достаточно.
            Да, статья все разьяснила, благодарю