go

【データ更新APIの実装】Go言語でのAPI開発 | 実践的な学習から実装まで④

たけのこ

こんにちは!たけのこです。

今回も引き続き、Go言語を使用したAPIの実装方法について解説していきます。

前回は、クリーンアーキテクチャに沿ってフォルダ整理を行なっていきました。

https://takenoko-blog.net/2024/05/07/clean-architecture-explanation/

今回は、その記事の続きで整理し終わったプロジェクトで同様に、クリーンアーキテクチャ設計に沿ったユーザー情報を更新するAPIの実装について解説していきます。

では、早速行きましょう!

スポンサーリンク

はじめに

これまでのAPIでは、URLの後続にキーを指定するクエリパラメータを使ってユーザーデータの取得を行なうような実装をしてきました。

しかし、データの追加や更新などの大量のデータ、複雑なデータを扱う際にはリクエストボディに値を指定してAPIを呼び出します。

また、リクエストボディに指定できる形式は、htmlやxmlなど色々とありますが、今回はパッと見て分かりやすいJSON形式でデータのやり取りを行なう設計にしていきます。

リクエストボディに値を指定する方法

リクエストボディに値をセットしてAPIを呼び出す方法は色々とありますが、一番簡単な方法がAPI管理ツールを使用する方法です。

僕の場合は、以前も紹介した「Postman」というツールを使用しています。

Postmanを使用してリクエストボディでデータをAPI側に送る方法については、ザックリとこんな感じです~

APIを呼び出すフェーズで再度、セットするJSONについても含めて紹介しますので

ここでは、ザックリと使い方だけ紹介します~

では、APIの実装に入っていきます。

Step 1:domain層の実装

domain層には、ユーザーエンティティの構造体を定義していましたが、JSON形式のリクエストされたデータも受け取れるように変更を加えます。

package domain

// 各エンティティ項目の`json:~`の部分を追記
type User struct {
	UserID   int    `json:"user_id"`
	Name     string `json:"name"`
	Email    string `json:"email"`
	Password string `json:"password"`
}

Step 2: usecase層の実装

前回まで実装していたGetUserの下に新たにユーザーデータを更新するUpdateUserメソッドを追記します。

GetUserと大きく違うところは下記の2か所です。

  1. user_idだけでなく、domain層のUser構造体ごと引数で受け取っている。
  2. 戻り値にerror、string型の値を設定している。

データ取得APIの場合は、API実行時にSELECT文で取得してきたデータをそのまま実行結果に返答すればいいだけなのですが、更新の場合はUPDATE文を流してもDB側からは何も返答されてきません。

そのため、APIが正しく実行されたのかどうかを、開発者側がAPI実行者側に明示的に分かりやすくすることがセオリーです。

その関係上、戻り値にstring型の値(今回は更新されたデータを示すメッセージ)をセットしています。

package usecase

import (
	"myApiProject/internal/domain"
	"myApiProject/internal/repository"
)

type UserUseCase struct {
	repo repository.UserRepository
}

func NewUserUseCase(repo repository.UserRepository) *UserUseCase {
	return &UserUseCase{repo: repo}
}

/* 
*  ~ 前回までのGetUserロジック ~
*/

// ここから下のコードを新たに追記
func (uc *UserUseCase) UpdateUser(userRes domain.User) (string, error) {
	return uc.repo.UpdateUser(userRes)
}

Step 3: repository層の実装

ここでは、データベース操作を具体的に行なう関数をこの層で実装します。

今回の一番主役となってくるところですね~

大きく関わってくる変数は以下の3つです。

  • update:Updateしたいデータを指定
  • param:リクエストボディで受け取った各値をセット
  • message:更新をかけたデータのメッセージをセット

今回の実装では、受け取ったリクエストボディパラメータの有無によって更新するデータを条件分けしています。

たとえば、user_nameだけ受け取ったらuser_nameだけ更新。

user_name、email、passwordの3つ全て受け取ったら全部に更新を掛ける作りにしています。

package repository

import (
	"database/sql"
	"myApiProject/internal/domain"
	// 新たに"strconv"をimportに追記
	"strconv"
	"strings"
)

/* 
*  ~ 前回までのFindByIDロジック ~
*/

// ここから下のコードを新たに追記
func (r *UserRepository) UpdateUser(userRes domain.User) (string, error) {

	updates := []string{}
	params := []interface{}{}
	message := []string{}

	message = append(message, "ユーザーID'"+strconv.Itoa(userRes.UserID)+"'の")

	if userRes.Name != "" {
		updates = append(updates, "name = ?")
		params = append(params, userRes.Name)
		message = append(message, "名前を'"+userRes.Name+"'に、")
	}
	if userRes.Email != "" {
		updates = append(updates, "email = ?")
		params = append(params, userRes.Email)
		message = append(message, "Emailを'"+userRes.Email+"'に、")
	}
	if userRes.Password != "" {
		updates = append(updates, "password = ?")
		params = append(params, userRes.Password)
		message = append(message, "パスワードを'"+userRes.Password+"'に、")
	}

	message = append(message, "変更しました!")

	if len(updates) == 0 {
	 // 何も更新しない場合は、エラーではなく更新0件メッセージをセット
		return "ユーザーデータの更新は0件数です!", nil 
	}

	query := "UPDATE Users SET " + strings.Join(updates, ", ") + " WHERE user_id = ?"
	params = append(params, userRes.UserID)

	_, err := r.db.Exec(query, params...)
	return strings.Join(message, ""), err
}

Step 4: repository層の実装

ここでは、GetUserと同様にHTTPリクエストデータを処理し、UpdateUserユースケースを呼び出して返答されたデータをHTTPレスポンスに返答しています。

ここの実装はGetUserとほとんど同じですが、HTTPレスポンスで返答している内容がUser構造体ではなくメッセージを返しています。

package handler

import (
	"encoding/json"
	"myApiProject/internal/domain"
	"myApiProject/internal/usecase"
	"net/http"
)

/* 
*  ~ 前回までのGetUserロジック ~
*/

// ここから下のコードを新たに追記
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
	var user domain.User
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		http.Error(w, "User Data is missing", http.StatusBadRequest)
		return
	}

	message, err := h.useCase.UpdateUser(user)
	if err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
  // messageをHTTPレスポンスに返答
	json.NewEncoder(w).Encode(message)
}

Step 5: routeの設定

最後に、APIを呼び出す際のURLをセットします。

package router

import (
	"database/sql"
	"myApiProject/internal/handler"
	"myApiProject/internal/repository"
	"myApiProject/internal/usecase"
	"net/http"
)

func NewRouter(db *sql.DB) *http.ServeMux {
	repo := repository.NewUserRepository(db)
	useCase := usecase.NewUserUseCase(*repo)
	userHandler := handler.NewUserHandler(useCase)

	router := http.NewServeMux()
	router.HandleFunc("/user", userHandler.GetUser)
	// ↓の1行だけ追加
	router.HandleFunc("/user-update", userHandler.UpdateUser)
	return router
}

Step 6:APIの呼び出し

では、実装した更新APIを呼び出してみましょう!

現状、作成していたDBの中は下記のようになっていると思います。

今回は「山田太郎」さんの名前を「田中次郎」に変更しようと思います。

Postmanで下記JSONをリクエストボディに指定して実行してみましょう。

{
    "user_id": 1,
    "name": "田中次郎"
}

実行後、うまく更新されると下記のようにメッセージが表示されると思います。

実際のデータも更新されているか確認してみましょう。

うん、更新されていますね!

まとめ

ということで、今回はデータ更新APIの実装を行なってきました。

データ取得APIと大きく違うところは、

  • SQLがSELECTからUPDATEになっているところ。
  • HTTPレスポンスに返答する値がUser構造体ではなく、メッセージ文字列

という2か所ぐらいですね~

この記事が、更新API実装の助けとなって下されば幸いです。

ということで、今回はこのあたりで!ありがとうございました~!

スポンサーリンク
ABOUT ME
たけのこ
たけのこ
自由奔放エンジニア
現役でエンジニアをやっています! 開発現場で経験したコーディング実装例、実装アーキテクチャの解説などを記事に書き起こしています!
記事URLをコピーしました