著者は、 Diversity in Tech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

多くのアプリケーションにとって、望ましい機能の1つは、プロファイル画像をアップロードするユーザーの機能です。 ただし、この機能の構築は、ファイルのアップロードのサポートが組み込まれていないGraphQLを初めて使用する開発者にとっては難しい場合があります。

このチュートリアルでは、バックエンドアプリケーションから直接サードパーティのストレージサービスに画像をアップロードする方法を学習します。 GoバックエンドアプリケーションからS3互換のAWSGO SDKを使用するGraphQLAPIを構築し、高度にスケーラブルなオブジェクトストレージサービスである DigitalOceanSpacesに画像をアップロードします。 GoバックエンドアプリケーションはGraphQLAPIを公開し、DigitalOceanの ManagedDatabasesサービスによって提供されるPotsgreSQLデータベースにユーザーデータを保存します。

このチュートリアルの終わりまでに、Golangを使用してGraphQL APIを構築し、マルチパートHTTPリクエストからメディアファイルを受信し、 DigitalOceanSpaces内のバケットにファイルをアップロードできるようになります。

前提条件

このチュートリアルに従うには、次のものが必要です。

ステップ1— GolangGraphQLAPIのブートストラップ

このステップでは、Gqlgenライブラリーを使用してGraphQLAPIをブートストラップします。 Gqlgenは、GraphQLAPIを構築するためのGoライブラリです。 Gqglenが提供する2つの重要な機能は、スキーマファーストアプローチとコード生成です。 スキーマファーストのアプローチでは、最初にGraphQL Schema Definition Language (SDL)を使用してAPIのデータモデルを定義します。 次に、定義されたスキーマからAPIのボイラープレートコードを生成します。 コード生成機能を使用すると、APIのクエリリゾルバーとミューテーションリゾルバーが自動的に生成されるため、これらを手動で作成する必要はありません。

開始するには、以下のコマンドを実行してgqlgenをインストールします。

  1. go install github.com/99designs/gqlgen@latest

次に、という名前のプロジェクトディレクトリを作成します digitalocean このプロジェクトのファイルを保存するには:

  1. mkdir digitalocean

に変更します digitalocean プロジェクトディレクトリ:

  1. cd digitalocean

プロジェクトディレクトリから、次のコマンドを実行して、 go.mod 内のモジュールを管理するファイル digitalocean 事業:

 go mod init digitalocean

次に、nanoまたはお気に入りのテキストエディタを使用して、という名前のファイルを作成します tools.go プロジェクトディレクトリ内:

  1. nano tools.go

次の行をに追加します tools.go プロジェクトのツールとしてのファイル:

// +build tools

 package tools

 import _ "github.com/99designs/gqlgen" 

次に、 tidy コマンドを実行して、内に導入されたgqlgen依存関係をインストールします。 tools.go ファイル:

  1. go mod tidy

最後に、インストールされたGqlgenライブラリを使用して、GraphQLAPIに必要な定型ファイルを生成します。

  1. gqlgen init

の実行 gqlgen 上記のコマンドは、 server.go GraphQLサーバーを実行するためのファイルと graph を含むディレクトリ schema.graphqls GraphQLAPIのスキーマ定義を含むファイル。

このステップでは、Gqlgenライブラリを使用してGraphQLAPIをブートストラップしました。 次に、GraphQLアプリケーションのスキーマを定義します。

ステップ2—GraphQLアプリケーションスキーマの定義

このステップでは、以下を変更してGraphQLアプリケーションのスキーマを定義します。 schema.graphqls を実行したときに自動的に生成されたファイル gqlgen init 指図。 このファイルでは、ユーザー、クエリ、およびミューテーションのタイプを定義します。

に移動します graph ディレクトリを開き、 schema.graphqls GraphQLアプリケーションのスキーマを定義するファイル。 ボイラープレートスキーマを次のコードブロックに置き換えます。このコードブロックは、 User でタイプする Query すべてのユーザーデータと Mutation データを挿入するには:

schema.graphqls

scalar Upload

type User {
  id: ID!
  fullName: String!
  email: String!
  img_uri: String!
  DateCreated: String!
}

type Query {
  users: [User]!
}

input NewUser {
  fullName: String!
  email: String!
  img_uri: String
  DateCreated: String
}

input ProfileImage {
  userId: String
  file: Upload
}

type Mutation {
  createUser(input: NewUser!): User!
  uploadProfileImage(input: ProfileImage!): Boolean!
}

コードブロックは2つを定義します Mutation タイプとシングル Query すべてのユーザーを取得するためのタイプ。 mutation は、GraphQLアプリケーションに既存のデータを挿入または変更するために使用され、 query は、データをフェッチするために使用されます。 GET RESTAPIのHTTP動詞。

上記のコードブロックのスキーマは、GraphQLスキーマ定義言語を使用して Mutation を含む CreateUser タイプ、これは NewUser パラメータとして入力し、単一を返します user. また、 uploadProfileImage タイプ、これは ProfileImage 成功したアップロード操作のステータスを示すブール値を返します。

注: Gqlgenは、Uploadスカラー型を自動的に定義し、ファイルのプロパティを定義します。 これを使用するには、上記のコードブロックで行われたように、スキーマファイルの先頭で宣言するだけで済みます。

この時点で、アプリケーションのデータモデルの構造を定義しました。 次のステップは、Gqlgenのコード生成機能を使用して、スキーマのクエリとミューテーションリゾルバー関数を生成することです。

ステップ3—アプリケーションリゾルバーの生成

このステップでは、Gqlgenのコード生成機能を使用して、前のステップで作成したスキーマに基づいてGraphQLリゾルバーを自動的に生成します。 resolver は、GraphQLフィールドの値を解決または返す関数です。 この値は、オブジェクト、または文字列、数値、さらにはブール値などのスカラー型である可能性があります。

Gqlgenパッケージは、スキーマファーストアプローチに基づいています。 Gqlgenの時間節約機能は、で定義されたスキーマに基づいてアプリケーションのリゾルバーを生成する機能です。 schema.graphqls ファイル。 この機能を使用すると、リゾルバーのボイラープレートコードを手動で記述する必要がありません。つまり、定義されたリゾルバーの実装に集中できます。

コード生成機能を使用するには、プロジェクトディレクトリで以下のコマンドを実行して、GraphQLAPIモデルファイルとリゾルバーを生成します。

  1. gqlgen generate

を実行した後、いくつかのことが起こります gqlgen 指図。 に関連する2つの検証エラー schema.resolvers.go ファイルが印刷され、いくつかの新しいファイルが生成され、プロジェクトは新しいフォルダー構造になります。

treeコマンドを実行して、プロジェクトに追加された新しいファイルを表示します。

tree *

現在のディレクトリ構造は次のようになります。

Output
go.mod go.sum gqlgen.yml graph ├── db.go ├── generated │   └── generated.go ├── model │   └── models_gen.go ├── resolver.go ├── schema.graphqls └── schema.resolvers.go server.go tmp ├── build-errors.log └── main tools.go 2 directories, 8 files

プロジェクトファイルの中で、重要なファイルの1つは schema.resolvers.go. を実装するメソッドが含まれています MutationQuery 以前にで定義されたタイプ schema.graphqls ファイル。

検証エラーを修正するには、 CreateTodoTodos 下部のメソッド schema.resolvers.go ファイル。 タイプ定義がで変更されたため、Gqlgenはメソッドをファイルの一番下に移動しました schema.graphqls ファイル。

schema.resolvers.go

package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
	"context"
	"digitalocean/graph/generated"
	"digitalocean/graph/model"
	"fmt"
)

func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) User(ctx context.Context) (*model.User, error) {
	panic(fmt.Errorf("not implemented"))
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
//  - When renaming or deleting a resolver the old code will be put in here. You can safely delete
//    it when you're done.
//  - You have helper methods in this file. Move them out to keep these resolver files clean.

func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
	panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
	panic(fmt.Errorf("not implemented"))
}

で定義されているように schema.graphqls ファイル、Gqlgenのコードジェネレーターは2つのミューテーションと1つのクエリリゾルバーメソッドを作成しました。 これらのリゾルバーは、次の目的を果たします。

  • CreateUser:このミューテーションリゾルバーは、接続されたPostgresデータベースに新しいユーザーレコードを挿入します。

  • UploadProfileImage:このミューテーションリゾルバーは、マルチパートHTTPリクエストから受信したメディアファイルをアップロードし、そのファイルをDigitalOceanSpaces内のバケットにアップロードします。 ファイルのアップロード後、アップロードされたファイルのURLがに挿入されます img_uri 以前に作成されたユーザーのフィールド。

  • Users:このクエリリゾルバーは、データベースに既存のすべてのユーザーを照会し、それらを照会結果として返します。

MutationタイプとQueryタイプから生成されたメソッドを調べると、パニックが発生することがわかります。 not implemented 実行時にエラーが発生します。 これは、それらがまだ自動生成されたボイラープレートコードであることを示しています。 このチュートリアルの後半で、に戻ります。 schema.resolver.go これらの生成されたメソッドを実装するためのファイル。

この時点で、コンテンツに基づいてこのアプリケーションのリゾルバーを生成しました schema.graphqls ファイル。 次に、Managed Databasesサービスを使用して、ミューテーションリゾルバーに渡されたデータを格納するデータベースを作成し、ユーザーを作成します。

ステップ4—DigitalOceanでのマネージドデータベースインスタンスのプロビジョニングと使用

このステップでは、DigitalOceanコンソールを使用してManaged Databasesサービスにアクセスし、このアプリケーションからのデータを格納するPostgreSQLデータベースを作成します。 データベースが作成されたら、詳細を安全に保存します。 .env ファイル。

アプリケーションは画像をデータベースに直接保存しませんが、それでも各ユーザーのレコードを挿入するためのデータベースが必要です。 保存されたレコードには、アップロードされたファイルへのリンクが含まれます。

ユーザーのレコードは、フルネームメール dateCreated、、および文字列データ型のimg_uriフィールドで構成されます。 img_uri フィールドには、ユーザーがこのGraphQL APIを介してアップロードし、DigitalOceanSpacesのバケット内に保存されている画像ファイルを指すURLが含まれています。

DigitalOceanダッシュボードを使用して、コンソールの[データベース]セクションに移動して新しいデータベースクラスターを作成し、提供されているデータベースのリストからPostgreSQLを選択します。 他のすべての設定はデフォルト値のままにし、下部のボタンを使用してこのクラスターを作成します。

データベースクラスターの作成プロセスは、完了するまでに数分かかります。

クラスタを作成したら、データベースクラスタページのはじめにの手順に従って、使用するクラスタを設定します。

はじめにガイドの2番目のステップで、続行をクリックします。これは後で実行しますテキストで続行します。 デフォルトでは、データベースクラスターはすべての接続に対して開かれています。

注:本番環境に対応したシナリオでは、2番目のステップの信頼できるソースの追加入力フィールドには、アプリケーションを実行しているDigitalOceanドロップレットのIPアドレスなどの信頼できるIPアドレスのみを含める必要があります。 開発中に、開発マシンのIPアドレスを信頼できるソースの追加入力フィールドに追加することもできます。

これらのインバウンドソースを許可するボタンをクリックして保存し、次の手順に進みます。

次のステップで、クラスターの接続の詳細が表示されます。 アクションドロップダウンをクリックし、接続の詳細オプションを選択して、クラスターの資格情報を見つけることもできます。

このスクリーンショットでは、右側の灰色のボックスは、作成されたデモクラスターの接続クレデンシャルを示しています。

これらのクラスタークレデンシャルを環境変数として安全に保存します。 の中に digitalocean プロジェクトディレクトリ、作成 .env 次の形式でクラスターのクレデンシャルをファイルして追加します。強調表示されたプレースホルダーのコンテンツを独自のクレデンシャルに置き換えてください。

.env

 DB_PASSWORD=YOUR_DB_PASSWORD
 DB_PORT=PORT
 DB_NAME=YOUR_DATABASE_NAME
 DB_ADDR=HOST
 DB_USER=USERNAME

接続の詳細が安全に保存されている .env ファイルの場合、次のステップは、これらの資格情報を取得し、データベースクラスターをプロジェクトに接続することです。

先に進む前に、Postgresデータベースに接続するときにGolangのネイティブSQLパッケージを操作するためのデータベースドライバーが必要になります。 go-pg は、ORM(オブジェクトリレーショナルマッピング)クエリをPostgresデータベースのSQLクエリに変換するためのGolangライブラリです。 godotenv は、環境クレデンシャルをからロードするためのGolangライブラリです。 .env アプリケーションにファイルします。 最後に、 go.uuid は、データベースに挿入される各ユーザーのレコードのUUID(ユニバーサル一意識別子)を生成します。

これらをインストールするには、次のコマンドを実行します。

  1. go get github.com/go-pg/pg/v10 github.com/joho/godotenv github.com/satori/go.uuid

次に、に移動します graph ディレクトリを作成し、 db.go ファイル。 ファイル内のコードを徐々にまとめて、管理対象データベースクラスターで作成されたPostgresデータベースに接続します。

まず、コードブロックのコンテンツをに追加します db.go ファイル。 この関数(createSchema)データベースへの接続が確立された直後に、Postgresデータベースにユーザーテーブルを作成します。

db.go
package graph

import (
	"github.com/go-pg/pg/v10"
	"github.com/go-pg/pg/v10/orm"
	"digitalocean/graph/model"
)

func createSchema(db *pg.DB) error {
	for _, models := range []interface{}{(*model.User)(nil)}{
		if err := db.Model(models).CreateTable(&orm.CreateTableOptions{
			IfNotExists: true,
		}); err != nil {
			panic(err)
		}
	}

	return nil
}

を使用して IfNotExists に渡されたオプション CreateTable go-pg からのメソッド、 createSchema 関数は、テーブルが存在しない場合にのみ新しいテーブルをデータベースに挿入します。 このプロセスは、新しく作成されたデータベースをシードする単純化された形式として理解できます。 psql クライアントまたはGUIを使用してテーブルを手動で作成するのではなく、 createSchema 関数がテーブルの作成を処理します。

次に、以下のコードブロックのコンテンツをに追加します db.go Postgresデータベースへの接続を確立し、 createSchema 接続が正常に確立された場合の上記の機能:

db.go

import (
	  // ...

		 "fmt" 
		 "os" 
	)

func Connect() *pg.DB {
	DB_PASSWORD := os.Getenv("DB_PASSWORD")
	DB_PORT := os.Getenv("DB_PORT")
	DB_NAME := os.Getenv("DB_NAME")
	DB_ADDR := os.Getenv("DB_ADDR")
	DB_USER := os.Getenv("DB_USER")

	connStr := fmt.Sprintf(
		"postgresql://%v:%v@%v:%v/%v?sslmode=require",
		DB_USER, DB_PASSWORD, DB_ADDR, DB_PORT, DB_NAME )

	opt, err := pg.ParseURL(connStr); if err != nil {
  	  panic(err)
      }

	db := pg.Connect(opt)

	if schemaErr := createSchema(db); schemaErr != nil {
		panic(schemaErr)
	}

	if _, DBStatus := db.Exec("SELECT 1"); DBStatus != nil {
		panic("PostgreSQL is down")
	}

	return db 
}

実行すると、エクスポートされます Connect 上記のコードブロックの関数は、go-pgを使用してPostgresデータベースへの接続を確立します。 これは、次の操作によって実行されます。

  • まず、ルートに保存したデータベースのクレデンシャル .env ファイルが取得されます。 次に、取得した資格情報でフォーマットされた文字列を格納する変数が作成されます。 この変数は、データベースに接続するときに接続URIとして使用されます。

  • 次に、作成された接続文字列が解析され、フォーマットされた資格情報が有効かどうかが確認されます。 有効な場合、接続文字列はに渡されます connect 接続を確立するための引数としてのメソッド。

エクスポートされたものを使用するには Connect 関数、あなたは関数をに追加する必要があります server.go ファイルなので、アプリケーションの起動時に実行されます。 次に、接続をに保存できます DB 内のフィールド Resolver 構造体。

以前に作成したものを使用するには Connect からの機能 graph アプリケーションが開始された直後にパッケージ化し、から資格情報をロードする .env アプリケーションにファイルを挿入し、 server.go お好みのコードエディタでファイルを作成し、以下で強調表示されている行を追加します。

注:必ず既存のものを交換してください srv の変数 server.go とファイル srv 以下で強調表示されている変数。

server.go
 package main

import (
  "log"
  "net/http"
  "os"
  "digitalocean/graph"
  "digitalocean/graph/generated"

  "github.com/99designs/gqlgen/graphql/handler"
  "github.com/99designs/gqlgen/graphql/playground"
 "github.com/joho/godotenv"
)

const defaultPort = "8080"

func main() {
     err := godotenv.Load(); if err != nil {
     log.Fatal("Error loading .env file")
    } 

  // ...

	 Database := graph.Connect()
	 srv := handler.NewDefaultServer(
	 		generated.NewExecutableSchema(
	 				generated.Config{
	 					Resolvers: &graph.Resolver{
	 						DB: Database,
	 					},
	 				}),
	 	)

  // ...
}

このコードスニペットでは、に保存されているクレデンシャルをロードしました .env Load()関数を使用します。 あなたは Connect からの機能 db パッケージと作成しました Resolver データベース接続が保存されているオブジェクト DB 分野。 (保存されたデータベース接続は、このチュートリアルの後半でリゾルバーによってアクセスされます。)

現在、定型文 Resolver 構造体 resolver.go ファイルにが含まれていません DB 上記のコードでデータベース接続を保存したフィールド。 DBフィールドを作成する必要があります。

の中に graph ディレクトリ、を開きます resolver.go ファイルを作成して変更します Resolver 構造体 DB フィールドと go-pg 以下に示すように、そのタイプとしてのポインタ:

resolver.go
package graph

import "github.com/go-pg/pg/v10"

// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.

type Resolver struct {
	DB *pg.DB
}

これで、エントリのたびにデータベース接続が確立されます server.go ファイルが実行され、 go-pg パッケージは、リゾルバー関数からデータベースに対して操作を実行するためのORMとして使用できます。

このステップでは、DigitalOceanのマネージドデータベースサービスを使用してPostgreSQLデータベースを作成しました。 また、 db.go とファイル Connect アプリケーションの起動時にPostgreSQLデータベースへの接続を確立する機能。 次に、生成されたリゾルバーを実装して、PostgreSQLデータベースにデータを保存します。

ステップ5—生成されたリゾルバーの実装

このステップでは、メソッドを実装します schema.resolvers.go ファイル。ミューテーションおよびクエリリゾルバとして機能します。 実装されたミューテーションリゾルバーはユーザーを作成し、ユーザーのプロファイルイメージをアップロードしますが、クエリリゾルバーは保存されているすべてのユーザーの詳細を取得します。

ミューテーションリゾルバーメソッドの実装

の中に schema.graphqls ファイル、2つのミューテーションリゾルバが生成されました。 1つはユーザーのレコードを挿入する目的で、もう1つはプロフィール画像のアップロードを処理します。 ただし、これらのミューテーションはボイラープレートコードであるため、まだ実装されていません。

を開きます schema.resolvers.go ファイル。 インポートを変更し、 CreateUser データベースへのユーザー詳細入力を含む新しい行を挿入するための強調表示された行での変更:

schema.resolvers.go
package graph

import (
  "context"
  "fmt"
   "time" 

  "digitalocean/graph/generated"
  "digitalocean/graph/model"
  "github.com/satori/go.uuid" 
)

func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
	 user := model.User{ 
	 	ID:          fmt.Sprintf("%v", uuid.NewV4()), 
	 	FullName:    input.FullName, 
	 	Email:       input.Email, 
	 	ImgURI:      "https://bit.ly/3mCSn2i", 
	 	DateCreated: time.Now().Format("01-02-2006"), 
	 } 
 
	 _, err := r.DB.Model(&user).Insert(); if err != nil { 
	 	return nil, fmt.Errorf("error inserting user: %v", err) 
	 } 
 
	 return &user, nil 
}

の中に CreateUser ミューテーションでは、挿入されたユーザー行について注意すべき点が2つあります。 まず、挿入される各行にUUIDが与えられます。 第二に、 ImgURI 各行のフィールドには、デフォルト値としてプレースホルダー画像のURLがあります。 これはすべてのレコードのデフォルト値であり、ユーザーが新しい画像をアップロードすると更新されます。

次に、この時点でビルドされたアプリケーションをテストします。 プロジェクトディレクトリから、 server.go 次のコマンドでファイルします。

  1. go run ./server.go

次に、に移動します http://localhost:8080 Webブラウザーを介して、GraphQLAPIに組み込まれているGraphQLプレイグラウンドにアクセスします。 以下のコードブロックのGraphQLMutationをプレイグラウンドエディターに貼り付けて、新しいユーザーレコードを挿入します。

graphql

mutation createUser {
  createUser(
    input: {
      email: "[email protected]"
      fullName: "John Doe"
    }
  ) {
    id
  }
}

右ペインの出力は次のようになります。

あなたは実行しました CreateUser John Doe という名前のテストユーザーを作成するためのミューテーション、および id ミューテーションの結果、新しく挿入されたユーザーレコードのが返されました。

注:をコピーします id 実行されたGraphQLクエリから返される値。 を使用します id 上記で作成したテストユーザーのプロフィール画像をアップロードする場合。

この時点で、2番目があります UploadProfileImage ミューテーションリゾルバ関数は実装する必要があります。 ただし、この関数を実装する前に、まずクエリリゾルバーを実装する必要があります。 これは、各アップロードが特定のユーザーにリンクされているためです。そのため、画像をアップロードする前に特定のユーザーのIDを取得しました。

クエリリゾルバメソッドの実装

で定義されているように schema.resolvers.graphqls ファイルでは、作成されたすべてのユーザーを取得するために1つのクエリリゾルバーが生成されました。 以前のミューテーションリゾルバーメソッドと同様に、クエリリゾルバーメソッドも実装する必要があります。

開ける scheme.resolvers.go 生成されたものを変更します Users 強調表示された行を使用してリゾルバーを照会します。 内の新しいコード Users 以下のメソッドは、Postgresデータベースにすべてのユーザー行を照会し、結果を返します。

schema.resolvers.go
package graph

func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
  var users []*model.User

  err := r.DB.Model(&users).Select()
	if err != nil {
		return nil, err
	} 

  return users, nil 
}

以内 Users 上記のリゾルバー関数、ユーザーテーブル内のすべてのレコードのフェッチはgo-pgを使用して可能になります select 上のメソッド User 合格せずにモデル WHERE また LIMIT クエリへの句。

注:クエリから多くのレコードが返される大規模なアプリケーションの場合、パフォーマンスを向上させるために、返されたデータのページ付けを検討することが重要です。

ブラウザからこのクエリリゾルバをテストするには、次の場所に移動します。 http://localhost:8080 GraphQLプレイグラウンドにアクセスします。 以下のGraphQLクエリをプレイグラウンドエディターに貼り付けて、作成されたすべてのユーザーレコードをフェッチします。

graphql

query fetchUsers {
  users {
      fullName
      id
      img_uri
  }
}

右ペインの出力は次のようになります。

返された結果では、次のことがわかります。 users 配列値を持つオブジェクトが返されました。 今のところ、以前に作成されたユーザーのみが users これは、テーブル内の唯一のレコードであるためです。 より多くのユーザーが users を実行する場合は配列 createUser 新しい詳細を持つ突然変異。 また、 img_uri 返されるデータのフィールドには、ハードコードされたフォールバック画像のURLが含まれています。

この時点で、両方を実装しました。 CreateUser 突然変異と User クエリ。 2番目から画像を受信するためのすべてが整っています UploadProfileImage リゾルバーを使用し、S3互換の AWS-GO SDKを使用して、受信した画像をDigitalOceanSpacesのバケットにアップロードします。

ステップ6—DigitalOceanSpacesへの画像のアップロード

このステップでは、2番目の間に強力なAPIを使用します UploadProfileImage スペースに画像をアップロードするためのミューテーション。

まず、DigitalOceanコンソールの[スペース]セクションに移動します。ここで、バックエンドアプリケーションからアップロードされたファイルを保存するための新しいバケットを作成します。

新しいスペースの作成ボタンをクリックします。 設定をデフォルト値のままにして、新しいスペースの一意の名前を指定します。

新しいスペースが作成されたら、[設定]タブに移動し、スペースのエンドポイント、名前、およびリージョンをコピーします。 これらをに追加します .env この形式のGraphQLプロジェクト内のファイル:

.env
SPACE_ENDPOINT=BUCKET_ENDPOINT
DO_SPACE_REGION=DO_SPACE_REGION
DO_SPACE_NAME=DO_SPACE_NAME

例として、次のスクリーンショットは設定タブを示し、デモスペースの名前、地域、およびエンドポイントの詳細を強調しています(Victory-space):

前提条件の一部として、スペースのスペースアクセスキーとシークレットキーを作成しました。 アクセスキーとシークレットキーをに貼り付けます .env 次の形式のGraphQLアプリケーション内のファイル:

.env
ACCESS_KEY=YOUR_SPACE_ACCESS_KEY
SECRET_KEY=YOUR_SPACE_SECRET_KEY

この時点で、を使用する必要があります CTRL + C key 組み合わせてGraphQLサーバーを停止し、以下のコマンドを実行して、アプリケーションにロードされた新しいクレデンシャルでGraphQLアプリケーションを再起動します。

  1. go run ./server.go

Spaceクレデンシャルがアプリケーションに読み込まれたので、アップロードロジックを作成します。 UploadProfileImage ミューテーションリゾルバ。 最初のステップは、 aws-sdk-go SDKを追加して構成し、DigitalOceanSpaceに接続することです。

Spaces 内でバケットに対してプログラムで操作を実行する1つの方法は、互換性のあるAWSSDKを使用することです。 AWS Go SDKは、Go開発者が使用する一連のライブラリを提供する開発キットです。 SDKによって提供されるライブラリは、S3バケットへのファイル転送などのAWSリソースを使用して操作を実行するときに、Goで記述されたアプリケーションで使用できます。

DigitalOcean Spaces documentation は、AWSSDKを使用してSpacesAPIで実行できる操作のリストを提供します。 aws-sdk-go SDKを使用して、DigitalOceanSpaceに接続します。

を実行します go get インストールするコマンド aws-sdk-go アプリケーションへのSDK:

  1. go get github.com/aws/aws-sdk-go

次のいくつかのコードブロックでは、アップロードロジックを徐々にまとめていきます。 UploadProfileImage ミューテーションリゾルバ。

まず、 schema.resolvers.go ファイル。 強調表示された行を追加して、保存されたクレデンシャルを使用してAWS SDKを設定し、DigitalOceanSpaceとの接続を確立します。

注:アップロードロジックを徐々にまとめているため、以下のコードブロック内のコードは不完全です。 後続のコードブロックでコードを完成させます。

schema.resolvers.go
package graph

import (
   ...

   "os"

   "github.com/aws/aws-sdk-go/aws"
   "github.com/aws/aws-sdk-go/aws/credentials"
   "github.com/aws/aws-sdk-go/aws/session"
   "github.com/aws/aws-sdk-go/service/s3"
)

func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {

	SpaceRegion := os.Getenv("DO_SPACE_REGION")
	accessKey := os.Getenv("ACCESS_KEY")
	secretKey := os.Getenv("SECRET_KEY")

	s3Config := &aws.Config{
		Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
		Endpoint:    aws.String(os.Getenv("SPACE_ENDPOINT")),
		Region:      aws.String(SpaceRegion),
	}

	newSession := session.New(s3Config)
	s3Client := s3.New(newSession)

}

SDKが構成されたので、次のステップはマルチパートHTTPリクエストで送信されたファイルをアップロードすることです。

送信されたファイルを処理する1つの方法は、マルチパートリクエストからコンテンツを読み取り、コンテンツをメモリ内の新しいファイルに一時的に保存し、を使用して一時ファイルをアップロードすることです。 aws-SDK-go ライブラリを作成し、アップロード後に削除します。 このアプローチを使用すると、このGraphQL APIを使用するWebアプリケーションなどのクライアントアプリケーションは、サードパーティのAPIを使用してファイルをアップロードするのではなく、同じGraphQLエンドポイントを使用してファイルのアップロードを実行します。

これを実現するには、強調表示された行を既存のコードに追加します。 UploadProfileImage の突然変異リゾルバ schema.resolvers.go ファイル:

schema.resolvers.go

package graph

import (
   ...
  
   "io/ioutil"
   "bytes"

)

func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
...

SpaceName := os.Getenv("DO_SPACE_NAME")

...


  userFileName := fmt.Sprintf("%v-%v", input.UserID, input.File.Filename)
  stream, readErr := ioutil.ReadAll(input.File.File)
	if readErr != nil {
		fmt.Printf("error from file %v", readErr)
	}

	fileErr := ioutil.WriteFile(userFileName, stream, 0644); if fileErr != nil {
		fmt.Printf("file err %v", fileErr)
	}

	file, openErr := os.Open(userFileName); if openErr != nil {
		fmt.Printf("Error opening file: %v", openErr)
	}

	defer file.Close()

	buffer := make([]byte, input.File.Size)

_, _ = file.Read(buffer)

	fileBytes := bytes.NewReader(buffer)

	object := s3.PutObjectInput{
		Bucket: aws.String(SpaceName),
		Key:    aws.String(userFileName),
		Body:   fileBytes,
		ACL:    aws.String("public-read"),
	}

	if _, uploadErr := s3Client.PutObject(&object); uploadErr != nil {
		return false, fmt.Errorf("error uploading file: %v", uploadErr)
	}

	_ = os.Remove(userFileName)

 
return true, nil
}

上記のコードブロックのioパッケージのReadAllメソッドを使用して、最初にGraphQL APIに送信されるマルチパートリクエストに追加されたファイルのコンテンツを読み取り、次に一時ファイルを読み取りますこのコンテンツをにダンプするために作成されます。

次に、 PutObjectInput 構造体を使用して、アップロードするファイルの構造を作成しました。 Bucket, Key, ACL、 と Body 一時的に保存されたファイルの内容となるフィールド。

注: アクセス制御リストACL)のフィールド PutObjectInput 構造体には public-read アップロードされたすべてのファイルをインターネットで表示できるようにする価値。 アプリケーションでアップロードされたデータを非公開にする必要がある場合は、このフィールドを削除できます。

作成後 PutObjectInput 構造体、 PutObject メソッドは、 PUT 操作、の値を送信します PutObjectInput バケットに構造体。 エラーが発生した場合は、偽のブール値とエラーメッセージが返され、リゾルバー関数の実行と一般的なミューテーションが終了します。

アップロードミューテーションリゾルバーをテストするには、DigitalOceanのマスコットであるSammytheSharkの画像を使用できます。 使用 wget サミーの画像をダウンロードするコマンド:

wget https://html.sammy-codes.com/images/small-profile.jpeg

次に、以下のcURLコマンドを実行して、GraphQL APIにHTTPリクエストを送信し、リクエストフォームの本文に追加されたSammyの画像をアップロードします。

注: Windowsオペレーティングシステムを使用している場合は、バックスラッシュエスケープがあるため、 GitBashシェルを使用してcURLコマンドを実行することをお勧めします。

curl localhost:8080/query  -F operations='{ "query": "mutation uploadProfileImage($image: Upload! $userId : String!) { uploadProfileImage(input: { file: $image  userId : $userId}) }", "variables": { "image": null, "userId" : "12345" } }' -F map='{ "0": ["variables.image"] }'  -F [email protected]

注:ランダムを使用しています userId ユーザーのレコードを更新するプロセスがまだ実装されていないため、上記のリクエストの値。

出力は次のようになり、ファイルのアップロードが成功したことを示します。

Output
{"data": { "uploadProfileImage": true }}

DigitalOceanコンソールの[スペース]セクションに、ターミナルからアップロードされた画像があります。

この時点で、アプリケーション内のファイルのアップロードは機能しています。 ただし、ファイルはアップロードを実行したユーザーにリンクされています。 各ファイルのアップロードの目的は、ファイルをストレージバケットにアップロードしてから、更新することでユーザーにリンクすることです。 img_uri ユーザーのフィールド。

を開きます resolver.go のファイル graph ディレクトリを作成し、以下のコードブロックを追加します。 これには2つのメソッドが含まれています。1つは指定されたフィールドによってデータベースからユーザーを取得する関数で、もう1つはユーザーのレコードを更新する関数です。

resolver.go

import (
...

  "digitalocean/graph/model"
  "fmt"
)

...

func (r *mutationResolver) GetUserByField(field, value string) (*model.User, error) {
	user := model.User{}

	err := r.DB.Model(&user).Where(fmt.Sprintf("%v = ?", field), value).First()

	return &user, err
}


func (r *mutationResolver) UpdateUser(user *model.User) (*model.User, error) {
	_, err := r.DB.Model(user).Where("id = ?", user.ID).Update()
	return user, err
}

最初 GetUserByField 上記の関数は fieldvalue 引数、両方とも文字列型。 go-pgのORMを使用して、データベースでクエリを実行し、ユーザーテーブルからデータをフェッチします。 WHERE 句。

二番目 UpdateUser コードブロックの関数はgo-pgを使用して実行します UPDATE ユーザーテーブルのレコードを更新するステートメント。 where メソッドを使用して、 WHERE 条件付きの句がに追加されます UPDATE 同じ行のみを更新するステートメント ID 関数に渡されます。

これで、次の2つの方法を使用できます。 UploadProfileImage 突然変異。 以下の強調表示されたコードブロックのコンテンツをに追加します UploadProfileImage 内の突然変異 schema.resolvers.go ファイル。 これにより、ユーザーテーブルから特定の行が取得され、 img_uri ファイルがアップロードされた後のユーザーのレコードのフィールド。

注:強調表示されたコードを、 UploadProfileImage 突然変異。

schema.resolvers.go

package graph

 
func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
  _ = os.Remove(userFileName)

 
    user, userErr := r.GetUserByField("ID", *input.UserID)
  
  	if userErr != nil {
  		return false, fmt.Errorf("error getting user: %v", userErr)
  	}
  
   fileUrl := fmt.Sprintf("https://%v.%v.digitaloceanspaces.com/%v", SpaceName, SpaceRegion, userFileName)
  
  	user.ImgURI = fileUrl
  
   	if _, err := r.UpdateUser(user); err != nil {
  		return false, fmt.Errorf("err updating user: %v", err)
  	}
  

  return true, nil
}

に追加された新しいコードから schema.resolvers.go ファイル、 ID 文字列とユーザーのIDがに渡されます GetUserByField ミューテーションを実行しているユーザーのレコードを取得するヘルパー関数。

次に、新しい変数が作成され、最近アップロードされたファイルのリンクが次の形式になるようにフォーマットされた文字列の値が指定されます。 https://BUCKET_NAME.SPACE_REGION.digitaloceanspaces.com/USER_ID-FILE_NAME. The ImgURI 取得したユーザーモデルのフィールドに、アップロードされたファイルへのリンクとして、フォーマットされた文字列の値が再割り当てされました。

以下のcurlコマンドをターミナルに貼り付け、コマンドで強調表示されているUSER_IDプレースホルダーを userId 前のステップでGraphQLプレイグラウンドを介して作成されたユーザーの 確認してください userId 端末が値を適切にエンコードできるように、引用符で囲まれています。

  1. curl localhost:8080/query -F operations='{ "query": "mutation uploadProfileImage($image: Upload! $userId : String!) { uploadProfileImage(input: { file: $image userId : $userId}) }", "variables": { "image": null, "userId" : "USER_ID" } }' -F map='{ "0": ["variables.image"] }' -F 0=@small-profile.jpeg

出力は次のようになります。

Output
{"data": { "uploadProfileImage": true }}

ユーザーの img_uri 更新されました、あなたは使用することができます fetchUsers ブラウザのGraphQLプレイグラウンドからクエリを実行して、ユーザーの詳細を取得します。 更新が成功した場合、デフォルトのプレースホルダーURLが https://bit.ly/3mCSn2i の中に img_uri フィールドがアップロードされた画像の値に更新されました。

右ペインの出力は次のようになります。

返された結果では、 img_uri クエリから返された最初のユーザーオブジェクトには、DigitalOceanSpaces内のバケットへのファイルのアップロードに対応する値があります。 のリンク img_uri フィールドは、バケットエンドポイント、ユーザーのID、最後にファイル名で構成されます。

ACLオプションを介してアップロードされたファイルセットの権限をテストするには、 img_uri ブラウザのリンク。 アップロードされた画像のデフォルトのメタデータにより、画像ファイルとして自動的にコンピュータにダウンロードされます。 ファイルを開いて画像を表示できます。

での画像 img_uri リンクはコマンドラインからアップロードされたものと同じ画像になり、 resolver.go ファイルが正しく実行され、ファイルアップロードロジック全体が UploadProfileImage 突然変異は期待どおりに機能します。

このステップでは、AWS SDK for Goを使用して、DigitalOceanSpaceに画像をアップロードしました。 UploadProfileImage ミューテーションリゾルバ。

結論

このチュートリアルでは、GraphQLアプリケーションのミューテーションリゾルバーからAWS SDK for Golangを使用して、DigitalOceanSpaceで作成されたバケットにファイルをアップロードしました。

次のステップとして、このチュートリアル内で構築されたアプリケーションをデプロイできます。 Go Dev Guide は、GolangアプリケーションをDigitalOceanの App Platform にデプロイする方法に関する初心者向けのガイドを提供します。これは、アプリケーションを構築、デプロイ、および管理するためのフルマネージドソリューションです。さまざまなプログラミング言語。