Skip to content

Instantly share code, notes, and snippets.

@ohtsuchi
Last active May 1, 2019 14:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ohtsuchi/972cd176503c921b2aec32b7bed89240 to your computer and use it in GitHub Desktop.
Save ohtsuchi/972cd176503c921b2aec32b7bed89240 to your computer and use it in GitHub Desktop.

Golangでバックエンドハンズオン 記録

  • Golangでバックエンドハンズオン (2019/04/29) の 実装内容の記録
    • study_groups の CRUD が サンプルとして実装済み
    • events の CRUD を ハンズオン で実装する
      • 2019/05/01 現在、 POST /api/events の実装しか完成していない
        • そもそもこの実装で正解なのかどうか不明..
  • connpass の記述で事前告知されていたとおり、ハンズオン終了後に本家のリポジトリは非公開
  • ワイヤーフレーム は 公開(放置)し続けてくれるらしい

events の CRUD ハンズオン 実装記録

  • 確認/追加/修正したソースのみ、以下に記録

1. 事前確認

既に用意されている ファイルの内容確認

1-1. DDL の 確認

├── mysql
│   └── initdb.d
│       └── schema.sql  ● 確認
/*  events: イベント */
DROP TABLE IF EXISTS events;
CREATE TABLE events
(
    id             SERIAL PRIMARY KEY,
    title          VARCHAR(256)    NOT NULL comment 'タイトル',
    sub_title      VARCHAR(256) DEFAULT NULL comment 'サブタイトル',
    image_path     VARCHAR(256) DEFAULT NULL comment 'トップ画像の保存パス',
    study_group_id BIGINT UNSIGNED NOT NULL comment '所属グループ',
    event_start    DATETIME        NOT NULL comment '開催開始',
    event_end      DATETIME        NOT NULL comment '開催終了',
    apply_start    DATETIME        NOT NULL comment '募集開始',
    apply_end      DATETIME        NOT NULL comment '募集終了',
    summary        VARCHAR(2000)   NOT NULL comment 'イベント説明',
    user_id        BIGINT UNSIGNED NOT NULL comment '主催者',
    published      BOOLEAN      DEFAULT FALSE comment '公開済みかどうか',
    created_at     DATETIME     DEFAULT CURRENT_TIMESTAMP,
    updated_at     DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

    # , FOREIGN KEY (user_id) REFERENCES users (id)
    # , FOREIGN KEY (study_group_id) REFERENCES study_groups (id)
);
  • (コメントアウトされているけれど) foreign key として study_group_id, user_id が必要

1-2. Model の 確認

├── go
│   ├── domain
│   │   ├── model
│   │   │   ├── events.go  ● 確認
type Event struct {
	ID           int64
	Title        string
	SubTitle     *string
	ImagePath    *string
	StudyGroupID *int64
	EventStart   time.Time
	EventEnd     time.Time
	ApplyStart   time.Time
	ApplyEnd     time.Time
	Summary      string
	UserID       int64
	Published    bool
	CreatedAt    time.Time
	UpdatedAt    time.Time

	StudyGroup *StudyGroup
	User       *User
}

func (m *Event) TableName() string {
	return "events"
}
  • DB の study_group_id カラムは NOT NULL なのに StudyGroupID *int64null 指定可能になっているのが謎だった
    • -> 結局 null のままではエラーになるため、登録時にデフォルトの固定値(0)を指定した
    • -> 【 2-1-2-1. 登録時に ERROR 発生 (1) 】参照

1-3-1. Repository の IF 確認

  • IF の定義 event_repository.go (package repository) の
├── go
│   ├── domain
│   │   └── repository
│   │        ├── event_repository.go  ● 確認
  • インタフェース: IEventRepository
type IEventRepository interface {
	FindByID(id int64) (*model.Event, error)
	FindByIDs(ids []int64) ([]model.Event, error)

	Store(seminar model.Event) (*int64, error)
}

1-3-2. Repository の 実装 確認

  • event_repository.go (package gateway) の
├── go
│   ├── adapter
│   │   ├── gateway
│   │   │   ├── event_repository.go  ● 確認
  • 構造体: eventRepository
type eventRepository struct {
	db *gorm.DB
}

func NewEventRepository(db *gorm.DB) repository.IEventRepository {
	return &eventRepository{
		db: db,
	}
}

func (r *eventRepository) FindByID(id int64) (*model.Event, error) {
	event := model.Event{}
	if err := r.db.First(&event, id).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, nil
		}
		log.Error(err)
		return nil, errors.New(fmt.Sprintf("db access error. id: %d", id))
	}
	return &event, nil
}

func (r *eventRepository) FindByIDs(ids []int64) ([]model.Event, error) {
	var events []model.Event
	if err := r.db.Where("id IN (?)", ids).Find(&events).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, nil
		}
		log.Error(err)
		return nil, ErrDBAccessError
	}
	return events, nil
}

func (r *eventRepository) Store(event model.Event) (*int64, error) {
	if err := r.db.Create(&event).Error; err != nil {
		return nil, err
	}
	return &event.ID, nil
}

2. POST /api/events 実装 追加

2-1-1. Usecase の 追加. event_usecase.go ファイル 追加.

├── go
│   ├── usecase
│   │   ├── event_usecase.go  ★ 新規追加

2-1-2. インタフェース(IEventUseCase), 構造体(EventUseCase), Create メソッド 実装

package usecase

import (
	"github.com/sminoeee/sample-app/go/adapter/gateway"
	"github.com/sminoeee/sample-app/go/domain/model"
	"github.com/sminoeee/sample-app/go/domain/repository"
	db2 "github.com/sminoeee/sample-app/go/external/db"
	"time"
)

type (
	IEventUseCase interface {
		Create(userID int64, title string) (*int64, error)
	}
	EventUseCase struct {
		EventRepo repository.IEventRepository
	}
)

func NewEventUseCase() IEventUseCase {
	return &EventUseCase{
		EventRepo: gateway.NewEventRepository(db2.Conn),
	}
}

func (uc *EventUseCase) Create(userID int64, title string) (*int64, error) {
	defaultStudyGroupID := int64(0) // 登録時は 0固定
	defaultTime := time.Unix(0, 0)  // 登録時は 1970-01-01 00:00:00 固定

	event := model.Event{
		Title:        title,
		StudyGroupID: &defaultStudyGroupID,
		EventStart:   defaultTime,
		EventEnd:     defaultTime,
		ApplyStart:   defaultTime,
		ApplyEnd:     defaultTime,
		UserID:       userID,
	}

	eventID, err := uc.EventRepo.Store(event)
	if err != nil {
		return nil, err
	}

	return eventID, nil
}

2-1-2-1. 登録時に ERROR 発生 (1): Error 1048: Column 'study_group_id' cannot be null

  • 最初、以下の実装にしたところ、
	event := model.Event{
		Title:  title,
		UserID: userID,
	}
  • 【 2-6. POST /api/events 動作確認 】 の際に Error 1048: Column 'study_group_id' cannot be null が発生
    • -> study_group_id には 固定で 0 を登録する事で回避
    • -> 【 1-2. Model の 確認 】 参照
	defaultStudyGroupID := int64(0) // 登録時は 0固定
	event := model.Event{
		Title:        title,
		StudyGroupID: &defaultStudyGroupID,
		UserID:       userID,
	}

2-1-2-2. 登録時に ERROR 発生 (2): Error 1292: Incorrect datetime value: '0000-00-00' for column 'event_start' at row 1

	defaultStudyGroupID := int64(0) // 登録時は 0固定
	defaultTime := time.Unix(0, 0)  // 登録時は 1970-01-01 00:00:00 固定

	event := model.Event{
		Title:        title,
		StudyGroupID: &defaultStudyGroupID,
		EventStart:   defaultTime,
		EventEnd:     defaultTime,
		ApplyStart:   defaultTime,
		ApplyEnd:     defaultTime,
		UserID:       userID,
	}

2-2. request の実装. event.go ファイル (package request) 追加.

├── go
│   ├── adapter
│   │   ├── handler
│   │   │   ├── request
│   │   │      ├── event.go  ★ 新規追加
  • Create 用の 構造体 CreateEventRequest 追加
package request

type (
	CreateEventRequest struct {
		Title string
	}
)

2-3. response の実装. event.go ファイル (package response) 編集.

├── go
│   ├── adapter
│   │   ├── handler
│   │   │   ├── response
│   │   │      ├── event.go  ● 最初から用意
  • Create 用の 構造体 CreateEventResponse 追加
// (※前略)
type (
	EventResponses struct {
		Events []EventResponse
	}

	// レスポンス用のイベント
	EventResponse struct {
		ID           int64
		Title        string
		SubTitle     *string
		ImagePath    *string
		StudyGroupID *int64
		EventStart   time.Time
		EventEnd     time.Time
		ApplyStart   time.Time
		ApplyEnd     time.Time
		Summary      string
		UserID       int64
		Published    bool
	}

+	CreateEventResponse struct {
+		ID int64
+	}
)
// (※以下略)

2-4-1. Handler の実装. event_handler.go ファイル 追加.

├── go
│   ├── adapter
│   │   ├── handler
│   │   │   ├── request
│   │   │      ├── event.go  ★ 2-2 で 新規追加
│   │   │   ├── response
│   │   │      ├── event.go  ● 最初から用意. 2-3 で編集
│   │   │   └── event_handler.go  ★ 新規追加

2-4-2. インタフェース(IEventHandler), 構造体(EventHandler), Create メソッド 実装

package handler

import (
	"github.com/labstack/echo"
	"github.com/labstack/gommon/log"
	"github.com/sminoeee/sample-app/go/adapter/handler/request"
	"github.com/sminoeee/sample-app/go/adapter/handler/response"
	"github.com/sminoeee/sample-app/go/usecase"
	"github.com/sminoeee/sample-app/go/util"
	"net/http"
)

type (
	IEventHandler interface {
		Create(ctx echo.Context) error
	}

	EventHandler struct {
		EventUseCase usecase.IEventUseCase
	}
)

func NewEventHandler() IEventHandler {
	return &EventHandler{
		EventUseCase: usecase.NewEventUseCase(),
	}
}

func (e EventHandler) Create(ctx echo.Context) error {
	req := request.CreateEventRequest{}
	if err := ctx.Bind(&req); err != nil {
		log.Error(err)
		return NewApplicationError(http.StatusBadRequest, "bad request.")
	}

	userID := util.GetUserIDFromRequest(ctx)

	eventID, err := e.EventUseCase.Create(userID, req.Title)
	if err != nil {
		return NewApplicationError(http.StatusInternalServerError, "server error.")
	}

	return ctx.JSON(
		http.StatusCreated,
		response.CreateEventResponse{
			ID: *eventID,
		},
	)
}

2-5. Router 編集. POST /api/events 追加.

├── go
│   ├── adapter
│   │   └── router
│   │        └── router.go  ● 編集
// (※前略)
func Router(e *echo.Echo) *echo.Echo {
	// prefix: /api
	g := e.Group("/api")

	// (※中略)
	// CRUD events
	//
+	eventHandler := handler.NewEventHandler()
+	g.POST("/events", eventHandler.Create) // event 作成

	return e
}

2-6. POST /api/events 動作確認

  • 本来は User 登録 が必要だが set_default_user.go の中で デフォルトとして users.id: 1 がセットされる作りになっているため 今回は User 登録 は省略.

2-6-1. REST API で Event 登録

以下 Visual Studio Code の REST Client を使用して 動作確認

Request

### Event 登録

POST http://localhost:1323/api/events HTTP/1.1
content-type: application/json

{
    "title": "event-title1"
}

Response

HTTP/1.1 201 Created
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=UTF-8
Vary: Origin
Date: Wed, 01 May 2019 13:42:39 GMT
Content-Length: 14
Connection: close

{
  "ID": 1
}

2-6-2. SQL で データ 確認

  • docker-compose exec で コンテナ db に接続
% docker-compose exec db /bin/bash
  • コンテナ内で mysql 接続
root@9b8b07e1de44:/# mysql -u sample_user -D sample_db -p
Enter password:
  • SQL で確認
mysql> select * from events\G
*************************** 1. row ***************************
            id: 1
         title: event-title1
     sub_title: NULL
    image_path: NULL
study_group_id: 0
   event_start: 1970-01-01 00:00:00
     event_end: 1970-01-01 00:00:00
   apply_start: 1970-01-01 00:00:00
     apply_end: 1970-01-01 00:00:00
       summary:
       user_id: 1
     published: 0
    created_at: 2019-05-01 13:42:39
    updated_at: 2019-05-01 13:42:39
1 row in set (0.00 sec)

mysql> exit
Bye
  • コンテナ 内から抜け出す
root@9b8b07e1de44:/# exit

3. POST /api/events 以外の 実装 追加

TODO


感想

  • connpass の イベント説明の内容から「初心者向け」の簡単なサンプルかと思ったら、本格的な作りでビビった。
  • 今回は 2時間で3千円だったけれど、正直、時間が足りない
    • 数万円出費しても良いので、がっつり何日か掛けて学びたいなぁと思ったり。独学すれば良いだけですけど。

補足 A: Intellij の ショートカット

  • ソースの編集に Intellij (有料版) + Go plugin を利用. (普段は Java で使用してる)
  • 個人的に便利と感じている Intellij の ショートカット

補足 A-1: Cmd + U: Super Method (実装している Interface へ Jump)

  • 例: study_group_repository.go (package gateway) の
├── go
│   ├── adapter
│   │   ├── gateway
│   │   │   ├── study_group_repository.go
  • 構造体: studyGroupRepository の 実装メソッド(例: Create) の 定義内で Cmd + U
// グループを登録
func (r *studyGroupRepository) Create(studyGroup model.StudyGroup) (*int64, error) {
	if err := r.db.Create(&studyGroup).Error; err != nil {
		log.Error(err)
		return nil, ErrDBAccessError
	}
	return &studyGroup.ID, nil
}

  • IF の定義 study_group_repository.go (package repository) の
├── go
│   ├── domain
│   │   └── repository
│   │        ├── study_group_repository.go
  • IStudyGroupRepositoryCreate に Jump
type IStudyGroupRepository interface {
	// (※中略)
	Create(studyGroup model.StudyGroup) (*int64, error)
	// (※中略)
}

補足 A-2: Opt + Cmd + B: Implementation(s) (Interface を実装しているメソッドへ Jump)

  • 補足 A-1 Cmd + U の 「逆」
  • IStudyGroupRepositoryCreate にカーソルを置いて Opt + Cmd + B
  • -> 実装クラス study_group_repository.go (package gateway) の Create に Jump

補足 A-3: Cmd + B: Goto Declaration

  • IStudyGroupRepositoryCreate にカーソルを置いて Cmd + B
  • -> IStudyGroupRepositoryCreate の呼び出し元へ Jump
    • -> study_group_usecase.go
├── go
│   ├── usecase
│   │   ├── study_group_usecase.go
  • メソッド Create に Jump
func (uc *StudyGroupUseCase) Create(userID int64, title string) (*int64, error) {
	// (※中略)
	groupID, err := uc.StudyGroupRepo.Create(sg)
	// (※中略)
}
  • この行(groupID, err := uc.StudyGroupRepo.Create(sg)) の
  • Create にカーソルを置いて また Cmd + B
    • -> IStudyGroupRepositoryCreate の定義へ Jump して 戻る

補足 B: docker-compose build 時の error

ハマったのは自分だけで他の参加者は大丈夫だったのでレアケースかとは思いますが、 一応、記録として共有しておきます

補足 B-1: error 内容 (Version in "./docker-compose.yml" is unsupported.)

docker-compose build 実行時に 以下のエラー が出ました。

ERROR: Version in "./docker-compose.yml" is unsupported.
# (※中略)
Either specify a supported version (e.g "2.2" or "3.3")
# (※以下略)

補足 B-2: 解決法1: docker-compose.yml 修正

docker-compose.yml の 1行目の 3.73.3 に変更 して、正常に動作しました。

補足 B-3: docker-compose の version (1.21.21.23.2 で食い違い)

  • Terminal で docker-compose --version の結果
    • -> 1.21.2 でした。
  • しかし、About Docker Desktop の 表示は
    • -> 1.23.2 と表示
      • 何故 食い違うのか ??

場所を確認したところ、何故か Python の下に docker-compose が存在

% which docker-compose
/Users/ohtsuchi/Library/Python/3.7/bin/docker-compose

% pip3 list | grep docker-compose
docker-compose    1.21.2

自分では忘れていましたが、 どうやら pip3 で過去にインストールしていた?? ようです。

補足 B-4: 解決法2: pip3 uninstall docker-compose

  • 結局 pip3 uninstall docker-compose で uninstall して
  • /usr/local/bin/docker-compose (version 1.23.2) の方が呼ばれるように」することで解決しました。
% pip3 uninstall docker-compose
Uninstalling docker-compose-1.21.2:
  Would remove:
    /Users/ohtsuchi/Library/Python/3.7/bin/docker-compose
    /Users/ohtsuchi/Library/Python/3.7/lib/python/site-packages/compose/*
    /Users/ohtsuchi/Library/Python/3.7/lib/python/site-packages/docker_compose-1.21.2.dist-info/*
Proceed (y/n)? y
  Successfully uninstalled docker-compose-1.21.2

% which docker-compose
/usr/local/bin/docker-compose

% docker-compose --version
docker-compose version 1.23.2, build 1110ad01
# docker-compose.yml の 1行目を元に戻して 再実行
% docker-compose build
Building db
Step 1/5 : FROM mysql:8.0.15
# (※中略)
Successfully built a0c2c261a212
Successfully tagged sample-app_api:latest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment