- Golangでバックエンドハンズオン (2019/04/29) の 実装内容の記録
study_groups
の CRUD が サンプルとして実装済みevents
の CRUD を ハンズオン で実装する- 2019/05/01 現在、
POST /api/events
の実装しか完成していない- そもそもこの実装で正解なのかどうか不明..
- 2019/05/01 現在、
- connpass の記述で事前告知されていたとおり、ハンズオン終了後に本家のリポジトリは非公開
- ワイヤーフレーム は 公開(放置)し続けてくれるらしい
- 確認/追加/修正したソースのみ、以下に記録
既に用意されている ファイルの内容確認
├── 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
が必要
├── 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 *int64
とnull
指定可能になっているのが謎だった- -> 結局
null
のままではエラーになるため、登録時にデフォルトの固定値(0
)を指定した - -> 【 2-1-2-1. 登録時に ERROR 発生 (1) 】参照
- -> 結局
- 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)
}
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
}
├── go
│ ├── usecase
│ │ ├── event_usecase.go ★ 新規追加
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
}
- 最初、以下の実装にしたところ、
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
- 次は
- 【 2-6.
POST /api/events
動作確認 】 の際にError 1292: Incorrect datetime value: '0000-00-00' for column 'event_start' at row 1
が発生 - What is the
zero
value for time.Time in Go? - Stack Overflow によると、time.Time
の ゼロ値 は0001-01-01 00:00:00
らしいが
- Inserting Go's zero time.Time value into a MySQL DATETIME column errors - Stack Overflow によると、
-
The supported range is '1000-01-01 00:00:00' to '9999-12-31 23:59:59'.
- これが原因 ??
- -> ひとまず 固定で
1970-01-01 00:00:00
を登録する事で回避
- -> ひとまず 固定で
-
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,
}
├── go
│ ├── adapter
│ │ ├── handler
│ │ │ ├── request
│ │ │ ├── event.go ★ 新規追加
Create
用の 構造体CreateEventRequest
追加
package request
type (
CreateEventRequest struct {
Title string
}
)
├── 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
+ }
)
// (※以下略)
├── go
│ ├── adapter
│ │ ├── handler
│ │ │ ├── request
│ │ │ ├── event.go ★ 2-2 で 新規追加
│ │ │ ├── response
│ │ │ ├── event.go ● 最初から用意. 2-3 で編集
│ │ │ └── event_handler.go ★ 新規追加
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,
},
)
}
├── 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
}
- 本来は User 登録 が必要だが
set_default_user.go
の中で デフォルトとして users.id: 1 がセットされる作りになっているため 今回は User 登録 は省略.
以下 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
}
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
TODO
- connpass の イベント説明の内容から「初心者向け」の簡単なサンプルかと思ったら、本格的な作りでビビった。
- 今回は 2時間で3千円だったけれど、正直、時間が足りない
- 数万円出費しても良いので、がっつり何日か掛けて学びたいなぁと思ったり。独学すれば良いだけですけど。
- ソースの編集に Intellij (有料版) + Go plugin を利用. (普段は Java で使用してる)
- 個人的に便利と感じている Intellij の ショートカット
- 例:
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
IStudyGroupRepository
のCreate
に Jump
type IStudyGroupRepository interface {
// (※中略)
Create(studyGroup model.StudyGroup) (*int64, error)
// (※中略)
}
- 補足 A-1
Cmd + U
の 「逆」 IStudyGroupRepository
のCreate
にカーソルを置いてOpt + Cmd + B
- -> 実装クラス
study_group_repository.go
(package gateway
) のCreate
に Jump
IStudyGroupRepository
のCreate
にカーソルを置いてCmd + B
- ->
IStudyGroupRepository
のCreate
の呼び出し元へ 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
- ->
IStudyGroupRepository
のCreate
の定義へ Jump して 戻る
- ->
ハマったのは自分だけで他の参加者は大丈夫だったのでレアケースかとは思いますが、 一応、記録として共有しておきます
docker-compose build
実行時に 以下のエラー が出ました。
ERROR: Version in "./docker-compose.yml" is unsupported.
# (※中略)
Either specify a supported version (e.g "2.2" or "3.3")
# (※以下略)
docker-compose.yml
の 1行目の 3.7
を 3.3
に変更
して、正常に動作しました。
- 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
で過去にインストールしていた?? ようです。
- 結局
pip3 uninstall docker-compose
で uninstall して - 「
/usr/local/bin/docker-compose
(version1.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