こんにちは、イサムです!
本記事では、Go言語のORMであるXORMについてまとめていきます。
Go言語でDBを操作していきます。
今回は、ソースコードが長いので理解しにくいと思います、申し訳ありません。
- Go言語のORMのXORMついて知りたい!
- Go言語でDBを操作したい!
- Go言語のORMのXORMついてのまとめ
はじめに
一般的にリレーショナルデータベース(RDB: Relational DataBase)内のレコードを扱う際には、DBのための専用言語であるSQL(Structured Query Language)を使用しますが、DBはデータを効率的に管理、検索することに特化した構造となっているため、アプリケーションを実装する際によく使用されるオブジェクト指向の構造とは大きな差があります。
そこで、ORM(Object Relational Mapping)は、このRDBとオブジェクト指向の二つの世界の構造を自動でマッピングしてくれます。
機能としては、下記になります。
- オブジェクト(Goでは構造体)とDBから取得したレコードの関連付け
- SQL文の組み立て
ORMを使用することで、Goの型とDBのテーブルを透過的に扱うことができるため、生のSQL文を使用せずに関数を使ってSELECTやINSERTなどのSQLクエリを組み立てる機能を持つライブラリもあります。
ライブラリの種類としては、GORM、XORM、SQLBoiler、gorpなどがありますが、今回はXORMについてみていきたいと思います。
用語
簡単にですが、DBを扱う際に使用する用語を下記にまとめます。
データベース(DB: DataBase)
決まった形式で整理されたデータの集まりのことで、データを1か所に集めることで、いつでもすぐに利用できたり、大量のデータを安全に管理できます。
DBの種類としては下記のようなものがあります。
- RDBMS(Relational DataBase Management System)
- 階層型データベース
- オブジェクト指向データベース
- XMLデータベース
- NoSQLデータベース
今回は、2次元表で管理するRDBMSを使用します。
SQL(Structured Query Language)
RDBがデータ操作できるために備えている言語。
データの定義、操作、制御などができます。
クエリ
DBに対してデータの検索などの処理を行うように要求する命令文。
サブクエリ
1つのクエリの中で別のSELECT(検索と取得)クエリのことで、取得したデータはメインクエリで使用できます。
CRUD
CRUDは、Create(追加)、Read(検索)、Update(更新)、Delete(削除)のイニシャルを並べた用語になり、DBではよく使用されます。
エンジン
DBを操作(追加/検索/更新/削除)するための基礎となるソフトウェア。
ドライバー
アプリケーションがDBとやり取りするためAPIが実装されたソフトウェア。
コネクション
SQLを実行するためのアプリケーションとDBの通信経路。
コネクションプール
アプリケーションがDBへアクセスする際、アクセス要求の度に接続や切断を繰り返すのではなく、一度生成した接続窓口を維持し続けて再利用することになります。
セッション
2つ以上の通信機器が接続を確立してから切断するまでの一連の通信。
トランザクション
データの検索や更新等を含む一連の処理の単位。
コミット
トランザクション処理を確定させてDBに結果を反映させること。
ロールバック
データ更新などトランザクション処理中に障害が発生した時に、その前の状態に戻すこと。
XORM使い方
ディレクトリ構成
今回XORMを使用できる環境のディレクトリの構造は下記になり、ドメイン駆動とレイヤードアーキテクチャのオニオンアーキテクチャを採用しています。
(ドメイン駆動とレイヤードアーキテクチャについては、今後別の記事にまとめます。)
tree api/
├── app ※アプリケーション層
│ └── server
│ └── handler ※ハンドラ層(リクエストの処理の責務/サービス層に依存)
│ └── request ※リクエストのパラメータの定義
│ └── departments.go
│ └── users.go
│ └── response ※レスポンスの定義
│ └── departments.go
│ └── users.go
│ └── departments.go
│ └── error.go
│ └── handler.go
│ └── users.go
│ └── validator.go ※リクエストのバリデーション
│ └── server.go ※リクエスト処理サーバー
├── cmd ※アプリケーションのコマンド
│ └── main.go
├── config ※アプリケーションの設定処理関連
│ └── config.go
├── db ※DB関連
│ └── .env
│ └── conf.d
│ └── my.conf
│ └── init.d
│ └── 000001_init_scheme.up.sql ※ 今回使用するDBデータ定義
│ └── 000002_init_data.up.sql
├── domain ※ドメイン層
│ └── repo ※リポジトリ(データ処理のインターフェイス定義/サービス層とインフラ層を結合)
│ └── model ※業務ドメイン知識の定義
│ └── departments.go
│ └── users.go
│ └── departments.go
│ └── repo.go
│ └── users.go
│ └── service ※サービス層(業務ロジック処理の責務/インフラ層に依存)
│ └── departments.go
│ └── error.go
│ └── service.go
│ └── users.go
├── infra ※インフラ層(データ処理の責務)
│ └── departments.go
│ └── error.go
│ └── infra.go
│ └── users.go
├── .env ※環境変数定義
├── Dockerfile.yml
├── docker-compose.yml
├── Makefile
インストール
下記コマンドでxormのパッケージをインストールします。
go install xorm.io/xorm
// DBとやり取りするためにドライバが必要(今回はMySQL)
go install github.com/go-sql-driver/mysql
// サブクエリを使用する場合に使用
go install xorm.io/builder
XORMタグについて
DBアクセス時に使用する構造体のタグに、xormタグを付与することができます。
タグはいくつか種類がありそれぞれのタグで機能が異なります。
タグ | ルール |
---|---|
column_name | 対応するDBのカラム名を指定し、自動で判断 (DBカラム: FirstName -> xormタグ: first_name) |
pk | 指定のカラムをプライマリキー制約として自動で判断 |
null | not null | 指定のカラムをnullまたはnot null制約として自動で判断 |
extends | 結合したテーブルのデータをマッピングする構造体のフィールドには、結合する各テーブルのデータの構造体を定義し、その構造体にこのタグを付与することにより、結合したテーブルのデータをマッピング |
created | 新規レコード追加(Insert)時に現在時刻を自動登録 |
updated | 新規レコード追加(Insert)/レコード更新(Update)時に現在時刻を自動登録 |
deleted | レコード削除(Delete)時に現在時刻を自動登録 このタグを付与すれば、論理削除になり、データ自体は削除されない (Unscopedメソッドを使用した場合、物理削除される) |
default | デフォルト値を設定 |
comment | 指定したカラムのコメント(説明)を設定 (MySQLのみの機能) |
deletedタグの論理削除と物理削除についてですが、
論理削除は、deleted_atなどのカラムを用意し、そのカラムがnull以外のデータがあることで「削除されているレコード」とみなし、実際にレコードは削除しません。
物理削除は、レコードをDB上から完全に削除してしまうことになります。
論理削除の場合は、削除実行後もデータは存在するため復元が可能ですが、データ量が多い場合DB容量を圧迫してしまったり、データの検索等で毎回削除されているかの確認の処理が必要になります。物理削除は、論理削除の反対の結果になるため、システムの仕様によってどちらかを選択する必要があります。
また、deletedタグを使用すると、自動的にwhere句にdeleted_atカラムがNULLまたはゼロ値かという条件が追加されますが、後述するUnscopedメソッドを使用することで無効化することができます。
"deleted_at" IS NULL OR "deleted_at" = '0001-01-01 00:00:00'
使用するソースコード
今回使用する全てのソースコードは下記になります。
各メソッドの紹介する章では、必要な部分のみを抜粋します。
package request
type Departments struct {
IsUsed bool `query:"is_used"`
}
package request
type CreateUser struct {
EmployeeNumber string `json:"employee_number" validate:"required,len=8"`
FirstName string `json:"first_name" validate:"required,max=255"`
LastName string `json:"last_name" validate:"required,max=255"`
Email string `json:"email" validate:"required,max=255"`
DepartmentID int `json:"department_id" validate:"required,min=1,max=255"`
}
type CreateUsers struct {
CreateUsers []CreateUser `json:"users" validate:"required,max=100,unique_employee_number,unique_email"`
}
type GetUser struct {
EmployeeNumber string `query:"employee_number" validate:"required,len=8"`
}
type GetUsers struct {
QueryEmployeeNumbers string `query:"employee_number"`
EmployeeNumbers []string `validate:"required,dive,len=8"`
IsDelete bool `query:"is_delete"`
IsJoinedDepartment bool `query:"is_joined_department"`
}
type UpdateUser struct {
FirstName string `json:"first_name" validate:"max=255"`
LastName string `json:"last_name" validate:"max=255"`
Email string `json:"email" validate:"max=255"`
DepartmentID int `json:"department_id" validate:"min=1,max=255"`
}
type UpdateUsers struct {
ParamEmployeeNumbers string `param:"employee_number" validate:"required"`
EmployeeNumbers []string `validate:"required,unique,dive,len=8"`
UpdateUsers []UpdateUser `json:"users" validate:"required,max=100,unique_email"`
}
type DeleteUsers struct {
ParamEmployeeNumbers string `param:"employee_number" validate:"required"`
EmployeeNumbers []string `validate:"required,unique,dive,len=8"`
IsPhysical bool `query:"is_physical"`
}
package response
import "time"
type Departments struct {
Departments []Department `json:"departments"`
}
type Department struct {
ID string `json:"id"`
DepartmentName string `json:"department_name"`
CreatedAt time.Time `json:"created"`
UpdatedAt time.Time `json:"updated"`
DeletedAt time.Time `json:"deleted"`
}
package response
import "time"
type Users struct {
Users []User `json:"users"`
}
type User struct {
ID string `json:"id"`
EmployeeNumber string `json:"employee_number"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
DepartmentID int `json:"department_id"`
CreatedAt time.Time `json:"created"`
UpdatedAt time.Time `json:"updated"`
DeletedAt time.Time `json:"deleted"`
}
type JoinedDepartment struct {
EmployeeNumber string `json:"employee_number"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
DepartmentName string `json:"department_name"`
CreatedAt time.Time `json:"created"`
UpdatedAt time.Time `json:"updated"`
DeletedAt time.Time `json:"deleted"`
}
package handler
import (
"net/http"
"github.com/github-name/repository-name/app/server/handler/request"
"github.com/github-name/repository-name/domain/service"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
)
type Departments interface {
GetDepartments(c echo.Context) error
}
type departments struct {
service service.Service
}
func NewDepartments(service service.Service) Departments {
return &departments{
service: service,
}
}
func (d *departments) GetDepartments(c echo.Context) error {
var req request.Departments
if err := c.Bind(&req); err != nil {
err = errors.Wrap(err, ErrBindRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
if err := c.Validate(&req); err != nil {
err = errors.Wrap(err, ErrValidateRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
departments, err := d.service.GetDepartments(c, req)
if err != nil {
err = errors.Wrap(err, "GetDepartments in service")
return c.JSON(http.StatusInternalServerError, NewErrorResponse(http.StatusInternalServerError, ErrGetDepartment, err))
}
return c.JSON(http.StatusOK, departments)
}
package handler
import (
"reflect"
)
var (
ErrCodeBadRequest = 1
ErrCodeGetUser = 2
ErrCodeGetUsers = 3
ErrCodeCreateUsers = 4
ErrCodeUpdateUsers = 5
ErrCodeDeleteUsers = 6
ErrGetDepartment = 7
)
var (
ErrBindRequest = "failed to bind request data"
ErrValidateRequest = "invalid condition parameter"
)
type Error struct {
Message string `json:"message"`
Code int `json:"code"`
}
type ErrorResponse struct {
Status int `json:"status"`
Error Error
}
func NewErrorResponse(httpStatus int, errCode int, errMeasage interface{}) ErrorResponse {
return ErrorResponse{
Status: httpStatus,
Error: Error{
Message: getString(errMeasage),
Code: errCode,
},
}
}
func getString(i interface{}) string {
switch v := i.(type) {
case error:
return v.Error()
default:
return reflect.ValueOf(v).String()
}
}
package handler
import "github.com/github-name/repository-name/domain/service"
type Handler interface {
Users
Departments
}
type handler struct {
Users
Departments
}
func NewHandler(service service.Service) Handler {
return &handler{
Users: NewUsers(service),
Departments: NewDepartments(service),
}
}
package handler
import (
"net/http"
"github.com/github-name/repository-name/app/server/handler/request"
"github.com/github-name/repository-name/domain/service"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
)
type Users interface {
GetUser(c echo.Context) error
GetUsers(c echo.Context) error
CreateUsers(c echo.Context) error
UpdateUsers(c echo.Context) error
DeleteUsers(c echo.Context) error
}
type users struct {
service service.Service
}
func NewUsers(s service.Service) Users {
return &users{
service: s,
}
}
func (u *users) GetUser(c echo.Context) error {
var req request.GetUser
if err := c.Bind(&req); err != nil {
err = errors.Wrap(err, ErrBindRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
if err := c.Validate(req); err != nil {
err = errors.Wrap(err, ErrValidateRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
user, err := u.service.GetUser(c, req)
if err != nil {
err = errors.Wrap(err, "GetUser() in service")
resp := NewErrorResponse(http.StatusInternalServerError, ErrCodeGetUsers, err)
return c.JSON(http.StatusInternalServerError, resp)
}
return c.JSON(http.StatusOK, user)
}
// 構造体にインターフェースのメソッドを実装
func (u *users) GetUsers(c echo.Context) error {
var req request.GetUsers
if err := c.Bind(&req); err != nil {
err = errors.Wrap(err, ErrBindRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
if req.QueryEmployeeNumbers != "" {
req.EmployeeNumbers = strings.Split(req.QueryEmployeeNumbers, ",")
}
if err := c.Validate(&req); err != nil {
err = errors.Wrap(err, ErrValidateRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
if req.IsJoinedDepartment {
usersJoinedDepartments, err := u.service.GetUsersJoinedDepartment(c, req)
if err != nil {
err = errors.Wrap(err, "GetUsersJoinedDepartment in service")
resp := NewErrorResponse(http.StatusInternalServerError, ErrCodeGetUsers, err)
return c.JSON(http.StatusInternalServerError, resp)
}
return c.JSON(http.StatusOK, usersJoinedDepartments)
} else {
users, err := u.service.GetUsers(c, req)
if err != nil {
err = errors.Wrap(err, "GetUsers in service")
resp := NewErrorResponse(http.StatusInternalServerError, ErrCodeGetUsers, err)
return c.JSON(http.StatusInternalServerError, resp)
}
return c.JSON(http.StatusOK, users)
}
}
func (u *users) CreateUsers(c echo.Context) error {
var req request.CreateUsers
if err := c.Bind(&req); err != nil {
err = errors.Wrap(err, ErrBindRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
if err := c.Validate(req); err != nil {
err = errors.Wrap(err, "invalid condition parameter")
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
users, err := u.service.CreateUsers(c, req)
if err != nil {
err = errors.Wrap(err, "CreateUsers() in service")
resp := NewErrorResponse(http.StatusInternalServerError, ErrCodeCreateUsers, err)
return c.JSON(http.StatusInternalServerError, resp)
}
return c.JSON(http.StatusOK, users)
}
func (u *users) UpdateUsers(c echo.Context) error {
var req request.UpdateUsers
if err := c.Bind(&req); err != nil {
err = errors.Wrap(err, ErrBindRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeUpdateUsers, err))
}
if req.ParamEmployeeNumbers != "" {
req.EmployeeNumbers = strings.Split(req.ParamEmployeeNumbers, ",")
}
if err := c.Validate(&req); err != nil {
err = errors.Wrap(err, ErrValidateRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeUpdateUsers, err))
}
users, err := u.service.UpdateUsers(c, req)
if err != nil {
err = errors.Wrap(err, "UpdateUsers() in service")
resp := NewErrorResponse(http.StatusBadRequest, ErrCodeUpdateUsers, err)
return c.JSON(http.StatusInternalServerError, resp)
}
return c.JSON(http.StatusOK, users)
}
func (u *users) DeleteUsers(c echo.Context) error {
var req request.DeleteUsers
if err := c.Bind(&req); err != nil {
err = errors.Wrap(err, ErrBindRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
if req.ParamEmployeeNumbers != "" {
req.EmployeeNumbers = strings.Split(req.ParamEmployeeNumbers, ",")
}
if err := c.Validate(&req); err != nil {
err = errors.Wrap(err, ErrValidateRequest)
return c.JSON(http.StatusBadRequest, NewErrorResponse(http.StatusBadRequest, ErrCodeBadRequest, err))
}
err := u.service.DeleteUsers(c, req)
if err != nil {
err = errors.Wrap(err, "DeleteUsers() in service")
return c.JSON(http.StatusInternalServerError, NewErrorResponse(http.StatusInternalServerError, ErrCodeDeleteUsers, err))
}
return c.JSON(http.StatusNoContent, nil)
}
package handler
import (
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
type CustomValidator struct {
validator *validator.Validate
}
func NewValidator() echo.Validator {
validator := validator.New()
_ = validator.RegisterValidation("unique_employee_number", IsUniqueEmployeeNumber)
_ = validator.RegisterValidation("unique_email", IsUniqueEmail)
return &CustomValidator{validator: validator}
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
func IsUniqueEmployeeNumber(fl validator.FieldLevel) bool {
if fl.Field().Kind() != reflect.Slice {
return false
}
validEmployeeNumbers := map[string]struct{}{}
for i := 0; i < fl.Field().Len(); i++ {
if fl.Field().Index(i).Kind() != reflect.Struct {
return false
}
switch value := fl.Field().Index(i).FieldByName("EmployeeNumber"); value.Kind() {
case reflect.String:
if _, has := validEmployeeNumbers[value.String()]; has {
return false
} else {
validEmployeeNumbers[value.String()] = struct{}{}
}
default:
return false
}
}
return true
}
func IsUniqueEmail(fl validator.FieldLevel) bool {
if fl.Field().Kind() != reflect.Slice {
return false
}
validEmails := map[string]struct{}{}
for i := 0; i < fl.Field().Len(); i++ {
if fl.Field().Index(i).Kind() != reflect.Struct {
return false
}
switch value := fl.Field().Index(i).FieldByName("Email"); value.Kind() {
case reflect.String:
if _, has := validEmails[value.String()]; has {
return false
} else {
validEmails[value.String()] = struct{}{}
}
default:
return false
}
}
return true
}
package server
import (
"github.com/github-name/repository-name/app/server/handler"
"github.com/labstack/echo/v4"
)
type server struct {
echo *echo.Echo
}
func NewServer(e *echo.Echo) *server {
return &server{
echo: e,
}
}
func (s *server) RegisterHandler(h handler.Handler) *server {
s.echo.GET("/user", h.GetUser)
s.echo.GET("/users", h.GetUsers)
s.echo.POST("/users", h.CreateUsers)
s.echo.PUT("/users/:employee_number", h.UpdateUsers)
s.echo.DELETE("/user/:employee_number", h.DeleteUsers)
s.echo.GET("/departments", h.GetDepartments)
return s
}
func (s *server) Start(port string) error {
return s.echo.Start(":" + port)
}
package main
import (
"github.com/github-name/repository-name/app/server"
"github.com/github-name/repository-name/app/server/handler"
"github.com/github-name/repository-name/config"
"github.com/github-name/repository-name/domain/service"
"github.com/github-name/repository-name/infra"
_ "github.com/go-sql-driver/mysql"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"xorm.io/xorm"
xlog "xorm.io/xorm/log"
)
func NewDB() *xorm.Engine {
// DBエンジン作成のための文字列作成
dns := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=UTC",
config.DB.ID, config.DB.Password, config.DB.Host, config.DB.Port, config.DB.DB)
// エンジン作成
db, err := xorm.NewEngine("mysql", dns)
if err != nil {
log.Fatal(err)
}
// ロガーの作成と各種設定
logger := xlog.NewSimpleLogger(os.Stdout)
db.SetLogger(logger)
db.ShowSQL(true)
db.SetLogLevel(config.DB.LogLevel)
return db
}
func NewHandler(db *xorm.Engine) handler.handler {
repo := infra.NewDBRepository(db)
service := service.NewService(repo)
handler := handler.NewHandler(service)
return handler
}
func main() {
// 環境設定ファイルを読み込み
if err := config.ReadConfig(); err != nil {
log.Fatal(err)
}
// DBインスタンス作成
db := NewDB()
// APIサーバー構築
handler := NewHandler(db)
echo := echo.New()
e.Validator = handler.NewValidator()
// ハンドラ登録
server := server.NewServer(echo).RegisterHandler(handler)
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: config.App.AllowOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}))
server := server.NewServer(e).RegisterHandler(h)
log.Fatal(server.Start(config.App.APIPort))
}
package config
import (
"fmt"
"os"
"strconv"
"strings"
xlog "xorm.io/xorm/log"
)
type db struct {
ID string `env:"DB_ID"`
Password string `env:"DB_PASSWORD"`
Host string `env:"DB_HOST"`
Port string `env:"DB_PORT"`
DB string `env:"DB_DB"`
LogLevel xlog.LogLevel `env:"DB_LOG_LEVEL"`
MaxConnection int `env:"DB_MAX_CONNECTION"`
}
type app struct {
AllowOrigins []string `env:"ALLOW_CORS_ORIGINS"`
APIPort string `env:"API_PORT"`
}
var (
DB *db
App *app
)
func ReadConfig() error {
envKeys := []string{
"DB_ID",
"DB_PASSWORD",
"DB_HOST",
"DB_PORT",
"DB_DB",
"ALLOW_CORS_ORIGINS",
"API_PORT",
}
env := map[string]string{}
for _, k := range envKeys {
v := os.Getenv(k)
if v == "" {
return fmt.Errorf("cannot get environment variable %s", k)
}
env[k] = v
}
dbLogLevel := getLogLevelFromStr(os.Getenv("DB_LOG_LEVEL"))
DB = &db{
ID: env["DB_ID"],
Password: env["DB_PASSWORD"],
Host: env["DB_HOST"],
Port: env["DB_PORT"],
DB: env["DB_DB"],
LogLevel: dbLogLevel,
MaxConnection: dbMaxConn,
}
orgins := []string{}
orgins = strings.Split(env["ALLOW_CORS_ORIGINS"], ",")
App = &app{
AllowOrigins: orgins,
APIPort: env["API_PORT"],
}
return nil
}
// ログレベルの設定
func getLogLevelFromStr(str string) xlog.LogLevel {
switch str {
case "DEBUG":
return xlog.LOG_DEBUG
case "INFO":
return xlog.LOG_INFO
case "WARNING":
return xlog.LOG_WARNING
case "ERR":
return xlog.LOG_ERR
case "OFF":
return xlog.LOG_OFF
default:
return xlog.LOG_INFO
}
}
[mysqld]
character-set-server=utf8mb4
explicit-defaults-for-timestamp=1
general-log=1
general-log-file=/var/log/mysql/mysqld.log
[client]
default-character-set=utf8mb4
-- 部署マスタ
CREATE TABLE m_department (
id INT NOT NULL,
department_name VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME,
PRIMARY KEY (id)
);
-- ユーザーマスタ
create table users(
id VARCHAR(20) NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(20) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
PRIMARY KEY (id)
);
INSERT INTO
m_department
VALUES
(1, '総務部', NOW(), NOW(), NULL),
(2, '人事部', NOW(), NOW(), NULL),
(3, '事業部', NOW(), NOW(), NULL),
(4, '営業部', NOW(), NOW(), NULL),
(5, '経理部', NOW(), NOW(), NULL),
(6, '開発部', NOW(), NOW(), NULL),
(7, '製造部', NOW(), NOW(), NULL);
MYSQL_ROOT_PASSWORD=users
MYSQL_DATABASE=users
MYSQL_USER=users
MYSQL_PASSWORD=users
package model
import "time"
type Department struct {
ID int `xorm:"id not null"`
DepartmentName string `xorm:"not null"`
CreatedAt time.Time `xorm:"created"`
UpdatedAt time.Time `xorm:"updated"`
DeletedAt time.Time `xorm:"deleted"`
IsUsed bool `xorm:"-"`
}
func (Department) TableName() string {
return "m_department"
}
package model
import "time"
type User struct {
ID string `xorm:"id pk not null"`
EmployeeNumber string `xorm:"not null"`
FirstName string `xorm:"not null"`
LastName string `xorm:"not null"`
Email string `xorm:"not null"`
DepartmentID int `xorm:"department_id not null"`
CreatedAt time.Time `xorm:"created"`
UpdatedAt time.Time `xorm:"updated"`
DeletedAt time.Time `xorm:"deleted"`
}
func (User) TableName() string {
return "t_users"
}
type GetUsersCondition struct {
EmployeeNumbers []string
IsDelete bool
}
type JoinedDepartment struct {
User User `xorm:"extends"`
Department Department `xorm:"extends"`
}
func (JoinedDepartment) TableName() string {
return "t_users"
}
type UpdateUser struct {
EmployeeNumber string `xorm:"not null"`
FirstName string `xorm:"not null"`
LastName string `xorm:"not null"`
Email string `xorm:"not null"`
DepartmentID int `xorm:"department_id not null"`
CreatedAt time.Time `xorm:"created"`
UpdatedAt time.Time `xorm:"updated"`
}
func (UpdateUser) TableName() string {
return "t_users"
}
type UpdateUsers struct {
EmployeeNumbers []string
UpdateUsers []UpdateUser
}
func (UpdateUsers) TableName() string {
return "t_users"
}
type DeleteUsers struct {
EmployeeNumbers []string
IsPhysical bool
}
func (DeleteUsers) TableName() string {
return "t_users"
}
package repo
import (
"github.com/github-name/repository-name/domain/repo/model"
"github.com/labstack/echo/v4"
)
type Departments interface {
GetDepartments(c echo.Context, cond model.Department) ([]model.Department, error)
}
package repo
type Repo interface {
Users
Departments
}
package repo
import (
"github.com/github-name/repository-name/domain/repo/model"
"github.com/labstack/echo/v4"
)
type Users interface {
GetUser(c echo.Context, user model.User) (*model.User, error)
GetUsers(c echo.Context, condition model.GetUsersCondition) ([]model.User, error)
GetUsersJoinedDepartment(c echo.Context, condition model.GetUsersCondition) ([]model.JoinedDepartment, error)
CreateUsers(c echo.Context, users []model.User) ([]model.User, error)
UpdateUsers(c echo.Context, users model.UpdateUsers) ([]model.User, error)
DeleteUsers(c echo.Context, users model.DeleteUsers) error
}
package service
import (
"github.com/github-name/repository-name/app/server/handler/request"
"github.com/github-name/repository-name/app/server/handler/response"
"github.com/github-name/repository-name/domain/repo"
"github.com/github-name/repository-name/domain/repo/model"
"github.com/jinzhu/copier"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
)
type Departments interface {
GetDepartments(c echo.Context, req request.Department) (*response.Departments, error)
}
type departments struct {
repo repo.Repo
}
func NewDepartments(r repo.Repo) Departments {
return &departments{
repo: r,
}
}
func (d *departments) GetDepartments(c echo.Context, req request.Department) (*response.Departments, error) {
var cond model.Department
if err := copier.Copy(&cond, req); err != nil {
return nil, errors.Wrap(err, ErrCopyRequest)
}
departments, err := d.repo.GetDepartments(c, cond)
if err != nil {
return nil, errors.Wrap(err, "GetDepartments() in infra")
}
var resp response.Departments
if err := copier.Copy(&resp.Departments, departments); err != nil {
return nil, errors.Wrap(err, "failed to copy departments")
}
return &resp, nil
}
package service
var (
ErrCopyRequest = "failed to copy request"
)
package service
import "github.com/github-name/repository-name/domain/repo"
type Service interface {
Users
Departments
}
type service struct {
Users
Departments
}
func NewService(repo repo.Repo) Service {
return &service{
Users: NewUsers(repo),
Departments: NewDepartments(repo),
}
}
package service
import (
"github.com/github-name/repository-name/app/server/handler/response"
"github.com/github-name/repository-name/domain/repo"
"github.com/jinzhu/copier"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
)
type Users interface {
GetUser(c echo.Context, req request.GetUser) (*response.User, error)
GetUsers(c echo.Context, req request.GetUsers) (*response.Users, error)
GetUsersJoinedDepartment(c echo.Context, req request.GetUsers) ([]response.JoinedDepartment, error)
CreateUsers(c echo.Context, req request.CreateUsers) (*response.Users, error)
UpdateUsers(c echo.Context, req request.UpdateUsers) (*response.Users, error)
DeleteUsers(c echo.Context, req request.DeleteUsers) error
}
type users struct {
repo repo.Repo
}
func NewUsers(r repo.Repo) Users {
return &users{
repo: r,
}
}
func (u *users) GetUser(c echo.Context, req request.GetUser) (*response.User, error) {
var modelUser model.User
if err := copier.Copy(&modelUser, req); err != nil {
return nil, errors.Wrap(err, ErrCopyRequest)
}
user, err := u.repo.GetUser(c, modelUser)
if err != nil {
return nil, errors.Wrap(err, "GetUser() in infra")
}
var res response.User
if err := copier.Copy(&res, user); err != nil {
return nil, errors.Wrap(err, "failed to copy user")
}
return &res, nil
}
func (u *users) GetUsers(c echo.Context, req request.GetUsers) (*response.Users, error) {
var conditionGetUsers model.GetUsersCondition
conditionGetUsers.EmployeeNumbers = req.EmployeeNumbers
conditionGetUsers.IsDelete = req.IsDelete
users, err := u.repo.GetUsers(c, conditionGetUsers)
if err != nil {
return nil, errors.Wrap(err, "GetUsers() in infra")
}
var res response.Users
if err := copier.Copy(&res.Users, users); err != nil {
return nil, errors.Wrap(err, "failed to copy users")
}
return &res, nil
}
func (u *users) GetUsersJoinedDepartment(c echo.Context, req request.GetUsers) ([]response.JoinedDepartment, error) {
var conditionGetUsers model.GetUsersCondition
conditionGetUsers.EmployeeNumbers = req.EmployeeNumbers
conditionGetUsers.IsDelete = req.IsDelete
usersJoinedDepartments, err := u.repo.GetUsersJoinedDepartment(c, conditionGetUsers)
if err != nil {
return nil, errors.Wrap(err, "GetUsersJoinedDepartment() in infra")
}
res := make([]response.JoinedDepartment, 0, len(usersJoinedDepartments))
for _, userJoinedDepartment := range usersJoinedDepartments {
res = append(res, response.JoinedDepartment{
EmployeeNumber: userJoinedDepartment.User.EmployeeNumber,
FirstName: userJoinedDepartment.User.FirstName,
LastName: userJoinedDepartment.User.LastName,
Email: userJoinedDepartment.User.Email,
DepartmentName: userJoinedDepartment.Department.DepartmentName,
CreatedAt: userJoinedDepartment.User.CreatedAt,
UpdatedAt: userJoinedDepartment.User.UpdatedAt,
DeletedAt: userJoinedDepartment.User.DeletedAt,
})
}
return res, nil
}
func (u *users) CreateUsers(c echo.Context, req request.CreateUsers) (*response.Users, error) {
var newUsers []model.User
if err := copier.Copy(&newUsers, req.CreateUsers); err != nil {
return nil, errors.Wrap(err, ErrCopyRequest)
}
users, err := u.repo.CreateUsers(c, newUsers)
if err != nil {
return nil, errors.Wrap(err, "CreateUsers() in infra")
}
var res response.Users
if err := copier.Copy(&res.Users, users); err != nil {
return nil, errors.Wrap(err, "failed to copy created users")
}
return &res, nil
}
func (u *users) UpdateUsers(c echo.Context, req request.UpdateUsers) (*response.Users, error) {
var modelUpdateUsers model.UpdateUsers
if err := copier.Copy(&modelUpdateUsers, req); err != nil {
err = errors.Wrap(err, ErrCopyRequest)
}
users, err := u.repo.UpdateUsers(c, modelUpdateUsers)
if err != nil {
return nil, errors.Wrap(err, "UpdateUsers() in infra")
}
var res response.Users
if err := copier.Copy(&res.Users, users); err != nil {
return nil, errors.Wrap(err, "failed to copy updated users")
}
return &res, nil
}
func (u *users) DeleteUsers(c echo.Context, req request.DeleteUsers) error {
var modelDeleteUsers model.DeleteUsers
if err := copier.Copy(&modelDeleteUsers, req); err != nil {
return errors.Wrap(err, ErrCopyRequest)
}
err := u.repo.DeleteUsers(c, modelDeleteUsers)
if err != nil {
return errors.Wrap(err, "DeleteUsers() in infra")
}
return nil
}
package infra
import (
"github.com/github-name/repository-name/domain/repo"
"github.com/github-name/repository-name/domain/repo/model"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"xorm.io/builder"
"xorm.io/xorm"
)
type departments struct {
engine *xorm.Engine
}
func NewDepartments(engine *xorm.Engine) repo.Departments {
return &departments{
engine: engine,
}
}
func (d *departments) GetDepartments(c echo.Context, cond model.Department) ([]model.Department, error) {
var modelDepartment []model.Department
if cond.IsUsed {
subQuery := builder.
Select("department_id").
From("t_users").
Where(builder.IsNull{"deleted_at"})
count, err := d.engine.In("id", subQuery).Asc("id").FindAndCount(&modelDepartment)
if err != nil {
return nil, errors.Wrap(err, "failed to get used department")
}
if count == 0 {
return nil, errors.New("All department data are not used")
}
} else {
count, err := d.engine.FindAndCount(&modelDepartment)
if err != nil {
return nil, errors.Wrap(err, "failed to get used department")
}
if count == 0 {
return nil, errors.New("Department data do not extst")
}
}
return modelDepartment, nil
}
package infra
var (
ErrSessionCommit = "failed to commit db session"
ErrSessionBegin = "failed to begin db session"
ErrGetUsers = "failed to get users"
ErrGetUser = "failed to get user"
ErrCheckExistenceEmployeeNumber = "failed to check existence of employee numnber"
ErrExistenceEmployeeNumber = "Some employee nunmbers are already existing"
ErrCheckExistenceEmail = "failed to check existence of email"
ErrExistenceEmail = "Some Emails are already existing"
ErrCheckExistenceDepartmentID = "failed to check existence of Department ID"
ErrExistenceDepartmentID = "Some Department ids do not exist"
)
package infra
import (
"github.com/github-name/repository-name/domain/repo"
"xorm.io/xorm"
)
type repository struct {
repo.Users
repo.Departments
}
func NewDBRepository(engine *xorm.Engine) repo.Repo {
return &repository{
Users: NewUsers(engine),
Departments: NewDepartments(engine),
}
}
package infra
import (
"github.com/github-name/repository-name/domain/repo"
"github.com/github-name/repository-name/domain/repo/model"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/rs/xid"
"xorm.io/xorm"
)
type users struct {
engine *xorm.Engine
}
func NewUsers(engine *xorm.Engine) repo.Users {
return &users{
engine: engine,
}
}
func (u *users) GetUser(c echo.Context, user model.User) (*model.User, error) {
exist, err := u.engine.Where("employee_number = ?", user.EmployeeNumber).Get(&user)
if err != nil {
return nil, errors.Wrap(err, ErrGetUser)
}
if !exist {
return nil, errors.New("This user does not exist")
}
return &user, nil
}
func (u *users) GetUsers(c echo.Context, condition model.GetUsersCondition) ([]model.User, error) {
var users []model.User
if condition.IsDelete {
if err := u.engine.In("employee_number", condition.EmployeeNumbers).Unscoped().Find(&users); err != nil {
return nil, errors.Wrap(err, ErrGetUsers)
}
} else {
if err := u.engine.In("employee_number", condition.EmployeeNumbers).Find(&users); err != nil {
return nil, errors.Wrap(err, ErrGetUsers)
}
}
return users, nil
}
func (u *users) GetUsersJoinedDepartment(c echo.Context, condition model.GetUsersCondition) ([]model.JoinedDepartment, error) {
var usersJoinedDepartments []model.JoinedDepartment
if condition.IsDelete {
if err := u.engine.
Join("INNER", "m_department", "t_users.department_id = m_department.id").
In("employee_number", condition.EmployeeNumbers).
Unscoped().
Find(&usersJoinedDepartments); err != nil {
return nil, errors.Wrap(err, ErrGetUsers)
}
} else {
if err := u.engine.
Join("INNER", "m_department", "t_users.department_id = m_department.id").
In("employee_number", condition.EmployeeNumbers).
Find(&usersJoinedDepartments); err != nil {
return nil, errors.Wrap(err, ErrGetUsers)
}
}
return usersJoinedDepartments, nil
}
func (u *users) CreateUsers(c echo.Context, users []model.User) ([]model.User, error) {
session := u.engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
return nil, errors.Wrap(err, ErrSessionBegin)
}
for _, user := range users {
exist, err := session.Cols("employee_number").Exist(&model.User{EmployeeNumber: user.EmployeeNumber})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceEmployeeNumber)
}
if exist {
return nil, errors.New(ErrExistenceEmployeeNumber)
}
exist, err = session.Cols("email").Exist(&model.User{Email: user.Email})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceEmail)
}
if exist {
return nil, errors.New(ErrExistenceEmail)
}
exist, err = session.Table("m_department").Cols("id").Exist(&model.Department{
DepartmentID: user.DepartmentID,
})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceDepartmentID)
}
if !exist {
return nil, errors.New("ErrExistenceDepartmentID")
}
}
newIDs := make([]string, len(users))
for i := range users {
newIDs[i] = xid.New().String()
users[i].ID = newIDs[i]
}
if _, err := session.Insert(&users); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, "failed to insert users")
}
var createdUsers []model.User
if err := session.In("id", newIDs).Find(&createdUsers); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, "failed to get created users")
}
if err := session.Commit(); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, ErrSessionCommit)
}
return createdUsers, nil
}
func (u *users) UpdateUsers(c echo.Context, users model.UpdateUsers) ([]model.User, error) {
session := u.engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
return nil, errors.Wrap(err, ErrSessionBegin)
}
for _, empNo := range users.EmployeeNumbers {
exist, err := session.Cols("employee_number").Exist(&model.UpdateUser{EmployeeNumber: empNo})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceEmployeeNumber)
}
if !exist {
return nil, errors.New(ErrExistenceDepartmentID)
}
}
for i, user := range users.UpdateUsers {
var modelUsers []model.User
if user.Email != "" {
exist, err := session.Cols("email").Exist(&model.UpdateUser{
Email: user.Email,
})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceEmail)
}
if len(modelUsers) == 0 && users.EmployeeNumbers[i] != modelUsers[0].EmployeeNumber {
return nil, errors.New(ErrExistenceEmail)
}
}
if user.DepartmentID != 0 {
exist, err := session.Table("m_department").Cols("id").Exist(&model.Department{
ID: user.DepartmentID,
})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceDepartmentID)
}
if !exist {
return nil, errors.New(ErrExistenceDepartmentID)
}
}
}
for i, user := range users.UpdateUsers {
_, err := session.Where("employee_number = ?", users.EmployeeNumbers[i]).Update(&user)
if err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, "failed to update users")
}
}
var updatedUsers []model.User
if err := session.In("employee_number", users.EmployeeNumbers).Find(&updatedUsers); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, "failed to get updated users")
}
if err := session.Commit(); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, ErrSessionCommit)
}
return updatedUsers, nil
}
func (u *users) DeleteUsers(c echo.Context, users model.DeleteUsers) error {
session := u.engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
_ = session.Rollback()
return errors.Wrap(err, ErrSessionBegin)
}
for _, empNo := range users.EmployeeNumbers {
exist, err := session.Cols("employee_number").Exist(&model.User{EmployeeNumber: empNo})
if err != nil {
_ = session.Rollback()
return errors.Wrap(err, ErrCheckExistenceEmployeeNumber)
}
if !exist {
_ = session.Rollback()
return errors.New("Some employee Numbers do not exist")
}
if _, err := session.Delete(&model.User{
EmployeeNumber: empNo,
}); err != nil {
_ = session.Rollback()
return errors.Wrap(err, "failed to delete users")
}
}
if err := session.Commit(); err != nil {
_ = session.Rollback()
return errors.Wrap(err, ErrSessionCommit)
}
return nil
}
# DB接続時に使用するユーザーのID
DB_ID=users
# DB接続時に使用するユーザーのパスワード
DB_PASSWORD=users
# DBのエンドポイント
DB_HOST=db
# DB接続時に使用するポート番号
DB_PORT=3306
# DB接続時に対象とするDB名
DB_DB=users
# DBのログレベル(DEBUG, INFO, WARNING, ERR, OFF のいずれかを指定)
DB_LOG_LEVEL=ERR
# image取得(容量が小さいalpineを選択)
FROM golang:1.17.6-alpine
# アップデートとalpineにgitがないのでインストール
RUN apk add --update && apk add git
# ホストのファイルをコンテナの作業ディレクトリにコピー
COPY . /go/src/app
# ワーキングディレクトリの設定
WORKDIR /go/src/app/cmd
version: "3.7"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: users-app
networks:
- users
depends_on:
- "db"
ports:
- "80:80"
volumes:
- .:/go/src/app
- ../../../../../../pkg/mod:/go/pkg/mod
tty: true
env_file:
- ./.env
db:
image: mysql:5.7
container_name: users-db
networks:
- users
ports:
- "3306:3306"
volumes:
- ./db/init.d:/docker-entrypoint-initdb.d
- ./db/conf.d:/etc/mysql/conf.d
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
env_file:
- db/.env
networks:
users:
name: users
.PHONY: build, up, exec, down
build:
docker-compose build --no-cache
up: build
docker-compose up -d
exec: up
docker-compose exec app go run main.go
down:
docker-compose down
DB接続
xormを使用するためには、DBエンジンを作成する必要があり、今回はMySQLを利用します。(DBは、Dockerでコンテナを立ち上げています。)
// DBエンジンの構造体
type Engine struct {
・・・
driverName string
dataSourceName string
・・・
}
// ロガーの構造体
type SimpleLogger struct {
DEBUG *log.Logger
ERR *log.Logger
INFO *log.Logger
WARN *log.Logger
level LogLevel
showSQL bool
}
// DBエンジンを作成する関数
func NewEngine(driverName string, dataSourceName string) (*Engine, error)
// ロガーを作成する関数
func NewSimpleLogger(out io.Writer) *SimpleLogger
// ロガーを設定する関数
func (engine *Engine) SetLogger(logger interface{})
// 出力するログレベルを設定する関数(設定したログレベルのみ出力される)
func (engine *Engine) SetLogLevel(level log.LogLevel)
// 実行するSQL文を標準出力に表示するかしないかを決める関数
func (engine *Engine) ShowSQL(show ...bool)
// DBエンジン作成のための文字列作成
dns := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=UTC",
config.DB.ID, config.DB.Password, config.DB.Host, config.DB.Port, config.DB.DB)
// エンジン作成
db, err := xorm.NewEngine("mysql", dns)
if err != nil {
log.Fatal(err)
}
// ロガーの作成と各種設定
logger := xlog.NewSimpleLogger(os.Stdout)
db.SetLogger(logger)
db.ShowSQL(true)
db.SetLogLevel(config.DB.LogLevel)
CRUD – Create(Insert)
まずは、CRUDの中のデータの追加(Create)を行っていきます。
今回対象のデータは、ユーザーデータと部署データになります。
データの中には、重複が不可のユニークキーが割り当てられているデータがあるので、それらが存在していないことを確認してからデータの追加を行っていきます。
セッションを作成し、データの存在/重複チェック、データの追加、追加したデータの取得までを1つのセッションで行っていきます。その間で何かエラーがあれば、ロールバックして元の状態に戻すようにします。
セッションを生成する場合は、Session型のメソッド、生成しない場合は、Engine型のメソッドを使用します。
// DBエンジンの構造体
type Engine struct {
・・・
driverName string
dataSoutceName string
・・・
}
// DBセッションの構造体
type Session struct {
・・・
engine *Engine
・・・
}
// DBセッションを作成する関数
func (engine *Engine) NewSession() *Session
// セッションを使用して、トランザクションを開始する関数
func (session *Session) Begin() error
// セッションをリリースして、DBとの接続を切断する関数
func (session *Session) Close() error
// トランザクション中に何かのエラーがあったときに、ロールバック(元に戻す)する関数
func (session *Session) Rollback() error
// トランザクション中の全てのDB操作をコミットし、DBに結果を反映させる関数
func (session *Session) Commit() error
// 指定したテーブルにレコードを追加する関数(1レコードまたは複数レコード可)
func (session *Session) Insert(beans ...interface{}) (int64, error)
// 指定したデータがレコードに存在するかを確認する関数(存在する: true/存在しない: false)
func (session *Session) Exist(bean ...interface{}) (bool, error)
// テーブルのレコードを取得する関数
func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) error
// 第1引数に指定したカラムに第2引数で指定したデータを持つレコードを抽出するための条件関数
func (session *Session) In(column string, args ...interface{}) *Session
// 第1引数に指定したテーブル名に対して操作を行うようにする関数
func (session *Session) Table(tableNameOrBean interface{}) *Session
// 第1引数に指定したカラム名に対して操作を行うようにする関数
func (session *Session) Cols(columns ...string) *Session
// 全てのカラム名に対して操作を行うようにする関数
func (session *Session) AllCols() *Session
func (u *users) CreateUsers(c echo.Context, users []model.User) ([]model.User, error) {
session := u.engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
return nil, errors.Wrap(err, ErrSessionBegin)
}
// データの存在/重複確認
for _, user := range users {
// Colsメソッドで対象のカラムを指定(構造体でデータがゼロ値以外のデータのみ使用するので、実際は不要)
exist, err := session.Cols("employee_number").Exist(&model.User{EmployeeNumber: user.EmployeeNumber})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceEmployeeNumber)
}
if exist {
return nil, errors.New(ErrExistenceEmployeeNumber)
}
// Colsメソッドで対象のカラムを指定(構造体でデータがゼロ値以外のデータのみ使用するので、実際は不要)
exist, err = session.Cols("email").Exist(&model.User{Email: user.Email})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceEmail)
}
if exist {
return nil, errors.New(ErrExistenceEmail)
}
// テーブル名指定(構造体のメソッドでテーブル名は指定しているので、実際は不要)
exist, err = session.Table("m_department").Cols("id").Exist(&model.Department{
DepartmentID: user.DepartmentID,
})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceDepartmentID)
}
if !exist {
return nil, errors.New("ErrExistenceDepartmentID")
}
}
// DBに登録するIDを生成
newIDs := make([]string, len(users))
for i := range users {
newIDs[i] = xid.New().String()
users[i].ID = newIDs[i]
}
// DBにユーザーを登録
if _, err := session.Insert(&users); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, "failed to insert users")
}
// 登録したユーザーを取得
var createdUsers []model.User
if err := session.In("id", newIDs).Find(&createdUsers); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, "failed to get created users")
}
if err := session.Commit(); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, ErrSessionCommit)
}
return createdUsers, nil
}
curl -X POST -H "Content-Type: application/json" http://localhost:80/users -d '{"users": [{"first_name": "Ichiro", "last_name": "Suzuki", "email": "taro_susuki@example.com"},{"first_name": "Jiro", "last_name": "Suzuki", "email": "jiro_susuki@example.com"}]}'
{"users":[{"id":"c9phu1ouj5q4aocfjs20","first_name":"Ichiro","last_name":"Suzuki","email":"taro_susuki@example.com","created":"2022-05-05T00:49:11Z","updated":"2022-05-05T00:49:11Z","deleted":"0001-01-01T00:00:00Z"},{"id":"c9phu1ouj5q4aocfjs2g","first_name":"Jiro","last_name":"Suzuki","email":"jiro_susuki@example.com","created":"2022-05-05T00:49:11Z","updated":"2022-05-05T00:49:11Z","deleted":"0001-01-01T00:00:00Z"}]}
{"status":400,"Error":{"message":"invalid condition parameter: Key: 'CreateUsers.CreateUsers' Error:Field validation for 'CreateUsers' failed on the 'unique_employee_number' tag","code":1}}
{"status":400,"Error":{"message":"invalid condition parameter: Key: 'CreateUsers.CreateUsers' Error:Field validation for 'CreateUsers' failed on the 'unique_email' tag","code":1}}
{"status":500,"Error":{"message":"CreateUsers() in service: CreateUsers() in infra: Some employee nunmbers are already existing","code":4}}
{"status":500,"Error":{"message":"CreateUsers() in service: CreateUsers() in infra: Some Email are already existing","code":2}}
{"status":500,"Error":{"message":"CreateUsers() in service: CreateUsers() in infra: Some Department ids do not exist","code":4}}
INSERT INTO "t_users"(
"employee_number"
"first_name",
"last_name",
"email",
"department_id",
"created_at",
"updated_at"
"deleted_at"
)
VALUES(
$1,
$2,
$3,
$4,
$5
),
(
・・・
)
EngineまたはSession型(今回はSession型)のInsertメソッドに構造体を渡すとその構造体名をスネークケースに変換したもの(UserLog → user_log)と同じテーブル名に対して、insertしてくれます。(insertだけでなく、他のCRUDも同様)
もし、構造体名のスネークケースと違うテーブルにinsertしたい場合は、その構造体にテーブル名を返すTableNameメソッドを実装することで任意のテーブルにinsertすることができます。
その他にも、EngineまたはSession型のTableメソッドを使用し、直接テーブル名を指定することもできます。
テーブル名の参照優先順位としては、下記になります。
- EngineまたはSession型のTableメソッドで指定したテーブル名
- 使用する構造体のTableNameメソッドで返すテーブル名
- 構造体名をスネークケースに変換したテーブル名
また、構造体のフィールドも同様に、フィールド名をスネークケースに変換したものと同じカラムに対して処理を行います。
もし、構造体のフィールド名と異なるカラム名に対して処理したい場合は、xormタグにカラム名を指定することで、指定したカラム名に対して処理を行います。
その他にも、EngineまたはSession型のColsメソッドを使用し、直接カラム名を指定することもできます。AllColsメソッドを使用すると全てのカラムに対して処理を行います。
カラム名の参照優先順位としては、下記になります。
- EngineまたはSession型のAllColsメソッドまたはColsメソッドで指定したカラム名
- 使用する構造体のキー名のxormタグで指定したカラム名
- 構造体のキー名をスネークケースに変換したカラム名
CRUDで指定する構造体でColsメソッドで列名を指定しなかった場合、フィールドの型のゼロ値であれば、そのデータをDBに反映することはありませんが、指定した場合は、ゼロ値でも反映することになります。
AllColsメソッドを使用した場合も同様にゼロ値でもDBに反映することになります。
CRUD – Read(Get/Find)
次は、CRUDの中のデータの取得(Read)を行っていきます。
データの取得には2種類あり、単体取得のGetメソッドと複数取得のFindメソッドがあります。
データを取得する際は条件を付けて取得したいかと思いますが、その時は、WhereメソッドやInメソッドを使用し、複数の条件を指定したい場合は、AndメソッドまたはOrメソッドを使用することで実現できます。
また、検索データを並べ替えたい場合は、OrderByメソッド、Ascメソッド、Descメソッドなどを用いることで実現できます。
// DBエンジンの構造体
type Engine struct {
・・・
driverName string
dataSoutceName string
・・・
}
// DBセッションの構造体
type Session struct {
・・・
engine *Engine
・・・
}
// テーブルの1レコードをf取得する関数
func (engine *Engine) Get(beans ...interface{}) (bool, error)
// テーブルの複数レコードをf取得する関数
func (engine *Engine) Find(beans interface{}, condiBeans ...interface{}) error
// 複数レコードとレコードの件数をf取得する関数
func (engine *Engine) FindAndCount(rowsSlicePtr interface{}, condiBean ...interface{}) (int64, error)
// 第1引数に指定した条件に第2引数で指定したデータでレコードを抽出するための条件関数
func (engine *Engine) Where(query interface{}, args ...interface{}) *Session
// 論理削除したデータも扱えるようになる関数
func (engine *Engine) Unscoped() *Session
// 第1引数に指定した条件に第2引数で指定したデータでレコードを抽出するための条件関数(今回未使用)
func (session *Session) And(query interface{}, args ...interface{}) *Session
// 第1引数に指定した条件に第2引数で指定したデータでレコードを抽出するための条件関数(今回未使用)
func (session *Session) Or(query interface{}, args ...interface{}) *Session
// 第1引数に指定したカラム名に対して昇順にするための条件関数(今回未使用)
func (engine *Engine) Asc(colNames ...string) *Session
// 第1引数に指定したカラム名に対して降順にするための条件関数(今回未使用)
func (engine *Engine) Desc(colNames ...string) *Session
Get
ユーザーデータを1件取得します。
func (u *users) GetUser(c echo.Context, user model.User) (*model.User, error) {
// ユーザーデータを1件取得
exist, err := u.engine.Where("employee_number = ?", user.EmployeeNumber).Get(&user)
if err != nil {
return nil, errors.Wrap(err, ErrGetUser)
}
// ユーザーが存在しなければエラー
if !exist {
return nil, errors.New("This user does not exist")
}
return &user, nil
}
curl -X GET http://localhost:80/user?employee_number=J0000001
{"users":[{"id":"c9phu1ouj5q4aocfjs20","first_name":"Ichiro","last_name":"Suzuki","email":"taro_susuki@example.com","created":"2022-05-05T00:49:11Z","updated":"2022-05-05T00:49:11Z","deleted":"0001-01-01T00:00:00Z"},{"id":"c9phu1ouj5q4aocfjs2g","first_name":"Jiro","last_name":"Suzuki","email":"jiro_susuki@example.com","created":"2022-05-05T00:49:11Z","updated":"2022-05-05T00:49:11Z","deleted":"0001-01-01T00:00:00Z"}]}
{"status":500,"Error":{"message":"GetUser() in service: GetUser() in infra: This user does not exist","code":3}}
SELECT
*
FROM
"t_users"
WHERE
"employee_number" = $1
AND
"deleted_at" IS NULL OR "deleted_at" = '0001-01-01 00:00:00' // deletedタグによる
LIMIT 1
Find
複数のユーザーデータを取得します。
従業員番号が、J0000001のユーザーデータを論理削除し、Unscopedメソッドを使用した場合としなかった場合で結果をそれぞれ見ていきます。
func (u *users) GetUsers(c echo.Context, condition model.GetUsersCondition) ([]model.User, error) {
var users []model.User
if condition.IsDelete {
// 論理削除したユーザーデータも含めて取得
if err := u.engine.In("employee_number", condition.EmployeeNumbers).Unscoped().Find(&users); err != nil {
return nil, errors.Wrap(err, ErrGetUsers)
}
} else {
// 論理削除したユーザーデータも含めず取得
if err := u.engine.In("employee_number", condition.EmployeeNumbers).Find(&users); err != nil {
return nil, errors.Wrap(err, ErrGetUsers)
}
}
return users, nil
}
curl -X GET http://localhost:80/users\?employee_number=J0000001,J0000002\&is_delete\=true
{"users":[{"id":"ca51c7b2lt3ngcu1iop0","employee_number":"J0000001","first_name":"Ichiro","last_name":"Suzuki","email":"taro_susuki@example.com","department_id":1,"created":"2022-05-22T10:53:17Z","updated":"2022-05-22T10:53:17Z","deleted":"2022-05-22T20:37:42Z"},{"id":"ca51c7b2lt3ngcu1iopg","employee_number":"J0000002","first_name":"Jiro","last_name":"Suzuki","email":"jiro_susuki@example.com","department_id":2,"created":"2022-05-22T10:53:17Z","updated":"2022-05-22T10:53:17Z","deleted":"0001-01-01T00:00:00Z"}]}
curl -X GET http://localhost:80/users\?employee_number=J0000001,J0000002\&is_delete\=false
{"users":[{"id":"ca51c7b2lt3ngcu1iopg","employee_number":"J0000002","first_name":"Jiro","last_name":"Suzuki","email":"jiro_susuki@example.com","department_id":2,"created":"2022-05-22T10:53:17Z","updated":"2022-05-22T10:53:17Z","deleted":"0001-01-01T00:00:00Z"}]}
// Unscopedあり
SELECT
*
FROM
"t_users"
WHERE
"employee_number" = $1
IN
($1, $2)
// Unscopedなし
SELECT
*
FROM
"t_users"
WHERE
"employee_number"
IN
($1, $2)
AND
"deleted_at" IS NULL OR "deleted_at" = '0001-01-01 00:00:00' // deletedタグによる
Unscopedメソッドを使用することで、論理削除されたレコード(deletedタグとマッピングされているカラムにレコードが存在)も取得することができます。
テーブルを結合したRead
ユーザーと部署テーブルを結合し、ユーザーに紐づく部署データを取得します。
前述しましたが、結合したテーブルのデータを取得するには、結合したテーブルのデータをマッピングする構造体を別途用意する必要があり、そのフィールドには、結合する各テーブルのデータの構造体を定義し、その構造体にextendsタグを付与することにより、結合したテーブルのデータをマッピングすることができます。
// DBエンジンの構造体
type Engine struct {
・・・
driverName string
dataSoutceName string
・・・
}
// 第1引数に結合の種類、第2引数に結合先のテーブル名、第3引数に結合条件を指定しテーブル結合する関数
func (engine *Engine) Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session
func (u *users) GetUsersJoinedDepartment(c echo.Context, condition model.GetUsersCondition) ([]model.JoinedDepartment, error) {
var usersJoinedDepartments []model.JoinedDepartment
if condition.IsDelete {
// ユーザーが所属する部門データも併せて取得( 論理削除したユーザーデータも含める)
if err := u.engine.
Join("INNER", "m_department", "t_users.department_id = m_department.id").
In("employee_number", condition.EmployeeNumbers).
Unscoped().
Find(&usersJoinedDepartments); err != nil {
return nil, errors.Wrap(err, ErrGetUsers)
}
} else {
// ユーザーが所属する部門データも併せて取得( 論理削除したユーザーデータも含めない)
if err := u.engine.
Join("INNER", "m_department", "t_users.department_id = m_department.id").
In("employee_number", condition.EmployeeNumbers).
Find(&usersJoinedDepartments); err != nil {
return nil, errors.Wrap(err, ErrGetUsers)
}
}
return usersJoinedDepartments, nil
}
curl -X GET http://localhost:80/users\?employee_number=J0000001,J0000002\&is_delete\=true\&is_joined_department\=true
[{"employee_number":"J0000001","first_name":"Ichiro","last_name":"Suzuki","email":"taro_susuki@example.com","department_name":"人事部","created":"2022-05-22T10:53:17Z","updated":"2022-05-22T10:53:17Z","deleted":"2022-05-22T20:37:42Z"},{"employee_number":"J0000002","first_name":"Jiro","last_name":"Suzuki","email":"jiro_susuki@example.com","department_name":"経理部","created":"2022-05-22T10:53:17Z","updated":"2022-05-22T10:53:17Z","deleted":"0001-01-01T00:00:00Z"}]
curl -X GET http://localhost:80/users\?employee_number=J0000001,J0000002\&is_delete\=false\&is_joined_department\=true
[{"employee_number":"J0000002","first_name":"Jiro","last_name":"Suzuki","email":"jiro_susuki@example.com","department_name":"経理部","created":"2022-05-22T10:53:17Z","updated":"2022-05-22T10:53:17Z","deleted":"0001-01-01T00:00:00Z"}]
// Unscopedあり
SELECT
*
FROM
"t_users" as t1
INNER JOIN
"m_department" as t2
ON
t1.department_id = t2.id
WHERE
"employee_number" = $1
IN
($1, $2)
// Unscopedなし
SELECT
*
FROM
"t_users" as t1
INNER JOIN
"m_department" as t2
ON
t1.department_id = t2.id
WHERE
"employee_number"
IN
($1, $2)
AND
"deleted_at" IS NULL OR "deleted_at" = '0001-01-01 00:00:00' // deletedタグによる
サブクエリを使用したRead
サブクエリを使用する場合、xormのbuilderパッケージを使用します。
サブクエリの結果は今回使用するようなWHERE句の条件やテーブルをJOINする条件として使用することができます。
// ビルダーの構造体
type Builder struct {
・・・
subquery *Builder
・・・
}
// 第1引数で指定したカラムのでデータを取得する関数
func (b *Builder) Select(cols ...string) *Builder
// 第1, 2引数で操作するデータのソースを指定する関数
func (b *Builder) From(subject interface{}, alias ...string) *Builder
// 第1引数で操作するデータの条件を指定する関数
func (b *Builder) Where(cond Cond) *Builder
// 指定したカラムがNULLのデータを返すように条件を指定する型
type IsNull
string
func (d *departments) GetDepartments(c echo.Context, cond model.Department) ([]model.Department, error) {
var modelDepartment []model.Department
// サブクエリを作成し、条件にするユーザーデータに使用されている部門データを抽出
subQuery := builder.
Select("department_id").
From("t_users").
Where(builder.IsNull{"deleted_at"})
// サブクエリで抽出したデータを使用し、メインクエリを実行して、部門データを取得
count, err := d.engine.In("id", subQuery).Asc("id").FindAndCount(&modelDepartment)
if err != nil {
return nil, errors.Wrap(err, "failed to get used department")
}
if count == 0 {
return nil, errors.New("All department data are not used")
}
return modelDepartment, nil
}
curl -X GET http://localhost:80/departments
{"Departments":[{"id":"\u0001","department_name":"総務部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"},{"id":"\u0002","department_name":"人事部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"},{"id":"\u0003","department_name":"事業部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"},{"id":"\u0004","department_name":"営業部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"},{"id":"\u0005","department_name":"経理部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"},{"id":"\u0006","department_name":"開発部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"},{"id":"\u0007","department_name":"製造部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"}]}
curl -X GET http://localhost:80/departments?is_used=true
{"Departments":[{"id":"\u0001","department_name":"総務部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"},{"id":"\u0002","department_name":"人事部","created":"2022-05-24T23:20:28Z","updated":"2022-05-24T23:20:28Z","deleted":"0001-01-01T00:00:00Z"}]}
{"status":500,"Error":{"message":"GetDepartments in service: GetDepartments() in infra: All department data are not used","code":7}}
SELECT
*
FROM
"m_department"
WHERE
"id" IN (
SELECT
department_id
FROM
t_users
WHERE
"deleted_at" IS NULL OR "deleted_at" = '0001-01-01 00:00:00' // deletedタグによる
)
CRUD – Update
次は、CRUDの中のデータの更新(Update)を行っていきます。
今回は行いませんが、前述した通り、AllColsメソッドを使用すれば、構造体のフィールドでゼロ値のデータがある場合、ゼロ値で更新され、Colsメソッドで指定したカラムと紐づくフィールドがゼロ値の場合もゼロ値で更新されてしまいます。
// DBセッションの構造体
type Session struct {
・・・
engine *Engine
・・・
}
// 指定したテーブルのレコードを更新する関数
func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int64, error)
func (u *users) UpdateUsers(c echo.Context, users model.UpdateUsers) ([]model.User, error) {
session := u.engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
return nil, errors.Wrap(err, ErrSessionBegin)
}
// データの存在/重複確認
for _, empNo := range users.EmployeeNumbers {
exist, err := session.Cols("employee_number").Exist(&model.UpdateUser{EmployeeNumber: empNo})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceEmployeeNumber)
}
if !exist {
return nil, errors.New(ErrExistenceDepartmentID)
}
}
for i, user := range users.UpdateUsers {
var modelUsers []model.User
if user.Email != "" {
exist, err := session.Cols("email").Exist(&model.UpdateUser{
Email: user.Email,
})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceEmail)
}
if len(modelUsers) == 0 && users.EmployeeNumbers[i] != modelUsers[0].EmployeeNumber {
return nil, errors.New(ErrExistenceEmail)
}
}
if user.DepartmentID != 0 {
exist, err := session.Table("m_department").Cols("id").Exist(&model.Department{
ID: user.DepartmentID,
})
if err != nil {
return nil, errors.Wrap(err, ErrCheckExistenceDepartmentID)
}
if !exist {
return nil, errors.New(ErrExistenceDepartmentID)
}
}
}
// ユーザーデータの更新
for i, user := range users.UpdateUsers {
_, err := session.Where("employee_number = ?", users.EmployeeNumbers[i]).Update(&user)
if err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, "failed to update users")
}
}
var updatedUsers []model.User
// 更新したユーザーデータの取得
if err := session.In("employee_number", users.EmployeeNumbers).Find(&updatedUsers); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, "failed to get updated users")
}
if err := session.Commit(); err != nil {
_ = session.Rollback()
return nil, errors.Wrap(err, ErrSessionCommit)
}
return updatedUsers, nil
}
curl -X PUT -H "Content-Type: application/json" http://localhost:80/users/J0000001,J0000002 -d '{"users": [{"first_name": "Saburo", "last_name": "Tanaka", "email": "saburo_tanaka@example.com","department_id":1},{"first_name": "Shiro", "last_name": "Tanaka", "email": "shiro_tanaka@example.com","department_id":2}]}'
{"users":[{"id":"ca6mub5snfvtbvpspphg","employee_number":"J0000001","first_name":"Saburo","last_name":"Tanaka","email":"saburo_tanaka@example.com","department_id":1,"created":"2022-05-24T23:50:04Z","updated":"2022-05-27T00:28:59Z","deleted":"0001-01-01T00:00:00Z"},{"id":"ca6mub5snfvtbvpsppi0","employee_number":"J0000002","first_name":"Shiro","last_name":"Tanaka","email":"shiro_tanaka@example.com","department_id":2,"created":"2022-05-24T23:50:04Z","updated":"2022-05-27T00:28:59Z","deleted":"0001-01-01T00:00:00Z"}]}
{"status":400,"Error":{"message":"invalid condition parameter: Key: 'UpdateUsers.UpdateUsers' Error:Field validation for 'UpdateUsers' failed on the 'unique_email' tag","code":5}}
{"status":500,"Error":{"message":"CreateUsers() in service: CreateUsers() in infra: Some Email are already existing","code":2}}
UPDATE
"t_users"
SET
"first_name" = $1,
"last_name" = $2,
"email" = $3,
"department_id" = $4,
)
WHERE
"employee_number" = id
AND
"deleted_at" IS NULL OR "deleted_at" = '0001-01-01 00:00:00' // deletedタグによる
CRUD – Delete
最後に、CRUDの中のデータの削除(Delete)を行っていきます。
前述した通り、削除には論理削除と物理削除があります。
論理削除の場合、deleted_atなどのカラムを用意して、そのカラムとマッピングする構造体のフィールドのxormタグにdeletedタグを付与し、そのカラムをnull以外のデータに更新することで「削除されているレコード」とみなします。実際にレコードは削除しません。
ただし、Unscopedメソッドを使用した場合、deletedタグは無効化されるので、物理削除され、データが完全に削除されるため、復元は不可能になります。
また、対象のテーブルとマッピングする構造体のどのフィールドのxormタグにもdeletedタグを付与しないで、Deleteメソッドを実行すれば、レコードをDB上から完全に削除してしまうことができます。
// DBセッションの構造体
type Session struct {
・・・
engine *Engine
・・・
}
// 指定したテーブルのデータを削除する関数
func (session *Session) Delete(beans ...interface{}) (int64, error)
func (u *users) DeleteUsers(c echo.Context, users model.DeleteUsers) error {
session := u.engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
_ = session.Rollback()
return errors.Wrap(err, ErrSessionBegin)
}
for _, empNo := range users.EmployeeNumbers {
// ユーザーデータの存在確認
exist, err := session.Cols("employee_number").Exist(&model.User{EmployeeNumber: empNo})
if err != nil {
_ = session.Rollback()
return errors.Wrap(err, ErrCheckExistenceEmployeeNumber)
}
if !exist {
_ = session.Rollback()
return errors.New("Some employee Numbers do not exist")
}
if users.IsPhysical {
// 物理削除
if _, err := session.Unscoped().Delete(&model.User{
EmployeeNumber: empNo,
}); err != nil {
_ = session.Rollback()
return errors.Wrap(err, "failed to delete users")
}
} else {
// 論理削除
if _, err := session.Delete(&model.User{
EmployeeNumber: empNo,
}); err != nil {
_ = session.Rollback()
return errors.Wrap(err, "failed to delete users")
}
}
}
if err := session.Commit(); err != nil {
_ = session.Rollback()
return errors.Wrap(err, ErrSessionCommit)
}
return nil
}
curl -X DELETE http://localhost:80/user/J0000001?is_physical=false
// なし
curl -X DELETE http://localhost:80/user/J0000002?is_physical=true
// なし
// Unscopedあり
DELETE
FROM
"t_users"
WHERE
"employee_number" = id
// Unscopedなし
UPDATE
"t_users"
SET
"deleted_at" = $1
)
WHERE
"employee_number" = id
AND
"deleted_at" IS NULL OR "deleted_at" = '0001-01-01 00:00:00' // deletedタグによる
まとめ
この記事では、Go言語のXORMについてまとめました。
少しGo言語をいじってみたいと思ったら、ブラウザ上で実行できる公式チュートリアルでコードを実際に書いてみると雰囲気がつかめるかもしれません。
今後、さらにGo言語のことについて記事にしていきたいと思います。
もし「記事の内容が間違えている!」等ありましたら、Twitterまたはお問い合わせフォームからご連絡いただければと思います。
最後までご覧いただきありがとうございました。