Go를 통한 창고 구현(엔트리 도메인 구동 설계 Chapter5)
개요
전술 DDD의 설치 모드는 Go로 이루어진다.
이번에는'입문역 구동 설계'Chapter5의 창고(Repository)를 실시한다.
실현된 원본 코드는 다음과 같다.
참고한 샘플 코드는 다음과 같습니다.
창고.
내가 창고를 설명할게.
다음은 로우엔드 도메인 구동 설계에 설명되어 있습니다.
객체를 재사용하려면 데이터 스토리지에 있는 객체의 데이터를 유지(저장)한 다음 객체를 재구성(복원)해야 합니다.창고는 데이터의 지속성과 재구성 과정을 처리하는 추상적인 대상이다.
대상의 실례를 저장하고자 할 때 데이터 저장소에 직접 쓰지 말고 창고의 지속적인 실례를 요청합니다.지속적인 데이터에서 실례를 재구성하려면 창고에서 데이터를 다시 구축하십시오.
출전: 도메인 이름 구동 설계 입문은 아래에서 위로 알 수 있습니다!도메인 제어 설계의 기본 Chapter5
창고의 지속성을 요청합니다. 도메인 대상에서 영구 층으로 직접 저장하는 것이 아닙니다.
이렇게 되면 도메인 객체 구현 DB 등에 대한 자세한 내용을 모른 채 변경할 수 있게 된다.이것은 DB의 종류를 변경할 때 편리해진다.
다른 주의점은 두 가지가 있다.
첫 번째는 저장소에서 도메인 객체에 대한 목록 작업을 수행하는 방법입니다.방법명은 조작 목록의 이름
Add
,Remove
등을 붙여야 한다.두 번째는 1개의 창고를 모아 통합성을 확보하는 것이다.
이루어지다
본고는 창고
UserRepository
를 통해 실체User
의 영구화 처리를 실현했다.또 UserService
를 통해 사용자를 중복 확인 처리한다.필드 모형도
디렉토리 구조
디렉토리 구성은 다음과 같습니다.
domain/model
각 도메인의 객체를 아래에 정렬합니다.본 보도는 본선에서 벗어나기 위해 테스트, DB, docker, go-migrate의 설명을 생략하였다.
디렉토리 구조
.
├── Makefile
├── db-migration.sh
├── docker
│ ├── go
│ │ └── Dockerfile
│ └── postgres
│ └── Dockerfile
├── docker-compose.yml
├── domain
│ └── model
│ └── user
│ ├── user.go
│ ├── user_test.go
│ ├── userid.go
│ ├── userid_test.go
│ ├── username.go
│ ├── username_test.go
│ ├── userrepository.go
│ ├── userrepository_test.go
│ ├── userservice.go
│ └── userservice_test.go
├── go.mod
├── go.sum
├── main.go
└── migrations
├── 000001_user.down.sql
└── 000001_user.up.sql
7 directories, 20 files
UserRepository
먼저, 해설
UserRepository
.UserRepository
에서 인터페이스UserRepositorier
를 실현했고 두 함수FindByUserName
와 Save
를 기술했다.FindByUserName
와 Save
는 역 대상의 지속적인 처리를 추상화한 함수이다.인터페이스를 사용하는 이유는 다른 도메인 서비스나 공장 등에 인용될 때 의존성을 분리하기 때문이다.
UserService
에서 확인했습니다.UserRepositorier
의 설치 유형UserRepository
에 실제 처리를 기술한다.FindByUserName
는 사용자 이름에서 사용자를 얻는 처리이고 Save
는 사용자를 저장하는 처리입니다.원래는 인터페이스
UserRepositorier
를 역층에 기술하고 실장류UserRepository
를 인프라층에 기술하여 등급을 분리한다.참고한 샘플 코드는 이때 같은 포장에 설치되어 있기 때문에 간소화하기 위해 같은 포장에 설치되어 있습니다.
./domain/model/user/userrepository.go
package user
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
type UserRepositorier interface {
FindByUserName(name *UserName) (*User, error)
Save(user *User) error
}
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) (*UserRepository, error) {
return &UserRepository{db: db}, nil
}
func (ur *UserRepository) FindByUserName(name *UserName) (user *User, err error) {
tx, err := ur.db.Begin()
if err != nil {
return
}
defer func() {
switch err {
case nil:
err = tx.Commit()
default:
tx.Rollback()
}
}()
rows, err := tx.Query("SELECT id, name FROM users WHERE name = $1", name.value)
if err != nil {
return nil, &FindByUserNameQueryError{UserName: *name, Message: fmt.Sprintf("error is occured in userrepository.FindByUserName: %s", err), Err: err}
}
defer rows.Close()
userId := &UserId{}
userName := &UserName{}
for rows.Next() {
err := rows.Scan(&userId.value, &userName.value)
if err != nil {
return nil, err
}
user = &User{id: *userId, name: *userName}
}
err = rows.Err()
if err != nil {
return nil, err
}
return user, nil
}
type FindByUserNameQueryError struct {
UserName UserName
Message string
Err error
}
func (err *FindByUserNameQueryError) Error() string {
return err.Message
}
func (ur *UserRepository) Save(user *User) (err error) {
tx, err := ur.db.Begin()
if err != nil {
return
}
defer func() {
switch err {
case nil:
err = tx.Commit()
default:
tx.Rollback()
}
}()
_, err = tx.Exec("INSERT INTO users(id, name) VALUES ($1, $2)", user.id.value, user.name.value)
if err != nil {
return &SaveQueryRowError{UserName: user.name, Message: fmt.Sprintf("userrepository.Save err: %s", err), Err: err}
}
return nil
}
type SaveQueryRowError struct {
UserName UserName
Message string
Err error
}
func (err *SaveQueryRowError) Error() string {
return err.Message
}
UserService
다음은 도메인 서비스
UserService
입니다.사용자 중복 확인 방법
Exists
을 실시했다.DB와 주고받은 처리를 창고 처리에 맡기기 때문에
UserService.Exists
에서는'중복 확인'의 본질 처리에만 집중된다.또한 생성 시 인터페이스
UserRepositorier
를 매개 변수로 한다.전달 인터페이스를 통해 실현 클래스가 아니라 컴파일링과 테스트를 실행할 수 있으며 구체적인 처리에 의존하지 않는다.도메인 이름 서비스에 대한 자세한 설명은 이전 기사을 참조하십시오.
./domain/model/user/userservice.go
package user
type UserService struct {
userRepository UserRepositorier
}
func NewUserService(userRepository UserRepositorier) (*UserService, error) {
return &UserService{userRepository: userRepository}, nil
}
func (us *UserService) Exists(user *User) (bool, error) {
user, err := us.userRepository.FindByUserName(user.Name())
if err != nil {
return false, err
}
return user != nil, nil
}
User、UserId、UserName
이번 샘플에서 조작 대상인 실체
User
와 구성 요소의 값 대상UserId
,UserName
.아래의 실복을 진행하였다.
구체적인 해설은 이전의 문장솔리드,값 객체을 참조하십시오.
./domain/model/user/user.go
package user
type User struct {
id UserId
name UserName
}
func NewUser(userId UserId, userName UserName) (*User, error) {
return &User{id: userId, name: userName}, nil
}
func (user *User) Id() *UserId {
return &user.id
}
func (user *User) Name() *UserName {
return &user.name
}
./domain/model/user/userid.gopackage user
import (
"fmt"
"reflect"
)
type UserId struct {
value string
}
func NewUserId(value string) (*UserId, error) {
return &UserId{value: value}, nil
}
func (userId *UserId) Equals(other *UserId) bool {
return reflect.DeepEqual(userId.value, other.value)
}
func (userId *UserId) String() string {
return fmt.Sprintf("UserId [value: %s]", userId.value)
}
./domain/model/user/username.gopackage user
import (
"fmt"
"reflect"
)
type UserName struct {
value string
}
func NewUserName(value string) (*UserName, error) {
if len(value) < 3 {
return nil, fmt.Errorf("UserName is more than 3 characters.")
}
if len(value) > 20 {
return nil, fmt.Errorf("UserName is less than 20 characters.")
}
return &UserName{value: value}, nil
}
func (userName *UserName) Equals(other UserName) bool {
return reflect.DeepEqual(userName.value, other.value)
}
func (userName *UserName) String() string {
return fmt.Sprintf("UserName: [value: %s]", userName.value)
}
동작 확인
이전 설치부터 작업을 확인합니다.
docker-compose를 사용합니다.
main.go
main.go
동작을 확인하기 위해 제작되었습니다.main 함수에서 DB와 연결하고 User의 제작 처리는
CreateUser
에서 진행한다.사용자 이름
user-name
, 사용자 IDuser-id
를 저장하는 프로세스입니다.본래 응용 프로그램 서비스층의 총결산이었는데 이번에는 설명을 위해 생략하였다.
./main.go
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/msksgm/go-itddd-05-repository/domain/model/user"
)
func main() {
uri := fmt.Sprintf("postgres://%s/%s?sslmode=disable&user=%s&password=%s&port=%s&timezone=Asia/Tokyo",
os.Getenv("DB_HOST"), os.Getenv("DB_NAME"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_PORT"))
db, err := sql.Open("postgres", uri)
if err != nil {
log.Fatal(err)
}
if err := db.Ping(); err != nil {
log.Fatal(err)
}
log.Println("successfully connected to database")
err = CreateUser(db, "test-user", "test-user-id")
if err != nil {
log.Println(err)
}
}
func CreateUser(db *sql.DB, name string, id string) (err error) {
defer func() {
if err != nil {
err = &CreateUserError{Message: fmt.Sprintf("main.CreateUser err: %v", err), Err: err}
}
}()
userName, err := user.NewUserName("username")
if err != nil {
return err
}
userId, err := user.NewUserId("userid")
if err != nil {
return err
}
newUser, err := user.NewUser(*userId, *userName)
if err != nil {
return err
}
userRepository, err := user.NewUserRepository(db)
if err != nil {
return err
}
userService, err := user.NewUserService(userRepository)
if err != nil {
return err
}
isExists, err := userService.Exists(newUser)
if err != nil {
return err
}
if isExists {
return fmt.Errorf("the user %v is already exists", newUser)
}
if err := userRepository.Save(newUser); err != nil {
return err
}
log.Println("test-user is successfully added in users table")
return nil
}
type CreateUserError struct {
Message string
Err error
}
func (err *CreateUserError) Error() string {
return err.Message
}
컨테이너의 시동 이동
집행 준비를 하다.
부팅 컨테이너
> make up
docker compose up -d
# 完了までまつ
옮기다> make run-migration
docker compose exec app bash db-migration.sh
1/u user (9.199ms)
실행
동작을 확인하다.
1차 시행에서는 등록
test-user
이 없어 등록했다.첫 번째 로그인
> make run
docker compose exec app go run main.go
2022/04/07 22:19:24 successfully connected to database
2022/04/07 22:19:24 test-user is successfully added in users table
두 번째에는 test-user
가 등록되어 있기 때문에 오류가 로그에 출력됩니다.test-user 등록 2차
> make run
docker compose exec app go run main.go
2022/04/07 22:19:34 successfully connected to database
2022/04/07 22:19:34 main.CreateUser err: the user &{{userid} {username}} is already exists
적당한 동작을 확인했다.고찰하다.
다음은 창고를 실적할 때의 고찰을 기술한다.
제 개인적인 견해니까 넘어가도 돼요.
명명 규칙 정보
필드 드라이브 디자인에서 유니버설 언어에 따라 클래스 이름, 함수 이름, 유형 이름, 변수 이름을 명명합니다.따라서
Service
와Factory
처럼 추상적이고 기계적인 이름은 사용하지 않는다.그러나
Repository
접미사에 Repository
를 붙이는 예가 많다.'실천역 구동 설계'도 마찬가지다.이 점에서 나는 창고가 도메인 이름의 상세한 내용보다 기술적인 세부 내용에 가깝기 때문에 허용된다고 생각한다.
같은 이유로 시연층
Controller
에서는 이 이름이 허용됐다.그리고 나는 방법의 명칭을 고려했다.
창고는 목록처럼 처리해야 하기 때문에 방법 이름은 조작 목록의 이름입니다.
그러나
Save
는 목록과 같은 이름이고, FindByUserName
는 역지식이 담긴 이름이다.왜냐하면 고는 교차언어가 없는 언어이기 때문이다.
구체적으로 말하면
Find(userId UserId ,userName UserName)
와 Find(userId UserId)
두 가지를 정의할 수 없다.그래서 이번 실복에서
FindByUserName
라는 이름을 지었다.덮어쓰지 않은 모든 언어는 공통적이며 인프라층의 개념이기 때문에 타협할 수 있는 범위라고 생각한다.
총결산
샘플 코드를 참고하면서 DDD 전술 모델 중 하나인 창고를 Go에서 시행했다.
기술 세부 정보인 DB에 저장하고 확인 처리를 창고에 취합하여 응집도가 높은 소스 코드가 된다.
본고는 창고 인터페이스의 역층을 생략하고 클래스를 인프라 시설층(분리 인터페이스)에 설치하면 더욱 분리할 수 있다.
이처럼 전술 DDD에서는 영역과 다른 영역을 분리해 실천에 옮기는 것이 권고된다.
O/R 맵을 사용할 때 도메인에 영향을 주지 않고 창고에 보관할 수도 있습니다.
앞으로 실시하면 기고문을 할 겁니다.
Reference
이 문제에 관하여(Go를 통한 창고 구현(엔트리 도메인 구동 설계 Chapter5)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/msksgm/articles/20220408-go-itddd-05-repository텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)