- DB : postgresql 사용
- vscode 사용
- Gin Framework 사용 (https://gin-gonic.com/docs/)
- sqlboiler 라이브러리 사용 (https://github.com/volatiletech/sqlboiler) , (https://github.com/gurleensethi/go-sql-boiler-example/blob/main/main.go) (https://thedevelopercafe.com/articles/sql-in-go-with-sqlboiler-ac8efc4c5cb8)
위 사이트들을 많이 참고해서 만들었다.
** go mod init 모듈 - 만든 디렉토리 위치에서 모듈을 만들기!!!
[게시판 만드는 순서]
1. 우선 jieun87 디렉토리 안에 goboard 디렉토리를 만들었다.
** go.work 디렉토리는 필수적이지는 않지만, 다른 디렉토리 안의 패키지를 import 를 편하게 하기 위해 사용한다.
=> Go Workspace에 대한 내용은 이 블로그가 잘 정리되어 있는 것 같다!(https://go-kalee.tistory.com/6)
$ go mod init
$ go work use ./goboard
$ go work use ./myginiproject
** 그러면 아래처럼 go.work에 이렇게 코드가 생성된다.
go 1.20
use (
./goboard
./myginiproject
)
2. Sqlboiler CLI 도구 설치 (정적 ORM 클라이언트를 생성하려면 Sqlboiler CLI가 필요하다.
$ go install github.com/volatiletech/sqlboiler/v4@latest
3. Postgresql DB를 사용하니 postgres 드라이버를 설치
$ go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-psql@latest
4. init.sql 생성하고 만들 테이블을 연결
CREATE TABLE board (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
writer VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
5. 몇 가지 패키지를 설치할 예정이므로 새 프로젝트를 생성하고 초기화!
$ go get github.com/lib/pq // Go Postgres 드라이버
$ go get github.com/volatiletech/sqlboiler/v4/boil // Sqlboiler 패키지
$ go get github.com/volatiletech/null/v8 // nil 유형을 더 잘 처리하기 위한 패키지
6. main.go 파일 생성
package main
import (
"context"
"database/sql"
"html/template"
models "jieun87/goboard/db/models" // 모델 패키지 경로를 수정
"log"
"net/http"
"strconv" // 문자열과 숫자 간의 변환을 제공하는 패키지
"github.com/gin-gonic/gin"
_ "github.com/lib/pq" // Go 언어에서는 변수를 선언하면 무조건 써야하고 패키지를 import 하면 무조건 사용해야 한다. 하지만 부가적인 효과만을 얻고자 한다면 밑줄(_)을 패키지 앞에 붙여주면 된다.
"github.com/volatiletech/null/v8"
"github.com/volatiletech/sqlboiler/v4/boil"
)
var db *sql.DB // sqlboiler 라이브러리, sqlboiler.toml과 연결
var tmpl = template.Must(template.ParseGlob("form/*"))
func main() {
db = connectDB() // db 연결 , connectDB() 호출
defer db.Close() // main 함수 종료 시 데이터베이스 연결을 닫도록 변경
// defer : 해당 구문이 함수의 호출 부분 맨 첫 부분에 입력되어있더라도 가장 마지막에 실행시킨다.
boil.SetDB(db)
r := gin.Default()
r.GET("/api/v4/", Index)
r.GET("/api/v4/board/:id", Show)
r.GET("/api/v4/boards", New)
r.POST("/api/v4/board", Insert)
r.GET("/api/v4/boards/:id", Edit)
r.POST("/api/v4/boards", Update)
r.GET("/api/v4/boardss/:id", Delete)
r.Run(":3000")
}
func connectDB() *sql.DB {
db, err := sql.Open("postgres", "user=testuser password=1234 dbname=test sslmode=disable")
if err != nil {
log.Fatal(err)
}
return db
}
func Index(c *gin.Context) {
// 데이터베이스에서 게시물을 가져오기
boards, err := models.Boards().All(context.Background(), db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// HTML 템플릿에 boards 슬라이스 전달
if err := tmpl.ExecuteTemplate(c.Writer, "Index", boards); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
func Insert(c *gin.Context) {
if c.Request.Method == "POST" { // http 요청의 메서드가 POST인지 확인
title := c.PostForm("title")
content := c.PostForm("content")
writer := c.PostForm("writer")
// 게시글 생성
newBoard := models.Board{
Title: null.StringFrom(title),
Content: content,
Writer: null.StringFrom(writer),
}
// 게시글을 데이터베이스에 추가
if err := newBoard.Insert(context.Background(), db, boil.Infer()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Redirect(http.StatusSeeOther, "/api/v4/")
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request method"})
}
}
func Show(c *gin.Context) {
idStr := c.Param("id") // id 매개변수 추출 후 문자열 형태로 저장
//fmt.Printf("idStr: %v\n", idStr) // id값 로그 찍어보기
id, err := strconv.Atoi(idStr) // 문자열 형태를 정수로 변환
if err != nil {
c.JSON(400, gin.H{"error": "Invalid ID"})
return
}
// 사용자 모델 (예: User)을 가져와서 'board' 테이블과 연결합니다.
boardItem, err := models.Boards(models.BoardWhere.ID.EQ(id)).One(context.Background(), db)
if err != nil {
c.JSON(500, gin.H{"error": "Error retrieving board data"})
return
}
if boardItem == nil {
c.JSON(404, gin.H{"error": "Board not found"})
return
}
// 이제 'boardItem' 변수를 템플릿에 전달할 수 있습니다.
tmpl.ExecuteTemplate(c.Writer, "Show", boardItem)
}
func New(c *gin.Context) {
tmpl.ExecuteTemplate(c.Writer, "New", nil)
}
func Edit(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr) //문자열 형태를 정수로 변환
if err != nil {
c.JSON(400, gin.H{"error": "Invalid ID"})
return
}
boardItem, err := models.Boards(models.BoardWhere.ID.EQ(id)).One(context.Background(), db)
if err != nil {
c.JSON(500, gin.H{"error": "Error retrieving board data"})
return
}
if boardItem == nil {
c.JSON(404, gin.H{"error": "Board not found"})
return
}
// 이제 'boardItem' 변수를 템플릿에 전달할 수 있습니다.
tmpl.ExecuteTemplate(c.Writer, "Edit", boardItem)
}
func Update(c *gin.Context) {
if c.Request.Method == "POST" { // http 요청의 메서드가 POST인지 확인
// 폼 데이터에서 필요한 정보 추출
idStr := c.PostForm("id") // id 매개변수 추출 후 문자열 형태로 저장
id, err := strconv.Atoi(idStr) // 문자열 형태를 정수로 변환
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
// 기존 게시물 가져오기
boardItem, err := models.Boards(models.BoardWhere.ID.EQ(id)).One(context.Background(), db)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving board data"})
return
}
if boardItem == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Board not found"})
return
}
// 폼 데이터에서 수정할 정보 추출
title := c.PostForm("title")
content := c.PostForm("content")
writer := c.PostForm("writer")
// 수정된 정보로 게시물 업데이트
boardItem.Title = null.StringFrom(title) // null.String타입으로 변환하여 데이터를 저장
boardItem.Content = content
boardItem.Writer = null.StringFrom(writer) // null.String타입으로 변환하여 데이터를 저장
// 게시물을 데이터베이스에 업데이트
if _, err := boardItem.Update(context.Background(), db, boil.Infer()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Redirect(http.StatusSeeOther, "/api/v4/")
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request method"})
}
}
func Delete(c *gin.Context) {
idStr := c.Param("id") //id 매개변수 추출 후 문자열 형태로 저장
id, err := strconv.Atoi(idStr) // 문자열 형태를 정수로 변환
if err != nil {
c.JSON(400, gin.H{"error": "Invalid ID"})
return
}
boardItem, err := models.Boards(models.BoardWhere.ID.EQ(id)).One(context.Background(), db)
if err != nil {
c.JSON(500, gin.H{"error": "Error retrieving board data"})
return
}
if boardItem == nil {
c.JSON(404, gin.H{"error": "Board not found"})
return
}
// 게시물 삭제
_, deleteErr := boardItem.Delete(context.Background(), db)
if deleteErr != nil {
c.JSON(500, gin.H{"error": "Error deleting board"})
return
}
// 게시물 삭제 성공 시, 게시물 목록 페이지로 리다이렉트
c.Redirect(http.StatusSeeOther, "/api/v4/")
}
7. sqlboiler 구성 파일 생성 - sqlboiler.toml
output = "db/models"
wipe = true
no-tests = true
add-enum-types = true
pkgname = "dbmodels"
add-global-variants = true
[psql]
dbname = "test"
host = "localhost"
port = 5432
user = "testuser"
pass = "1234"
sslmode = "disable"
output = "db/models"
-> Sqlboiler가 생성한 코드를 저장할 디렉토리를 지정한다. sqlboiler는 데이터베이스 스키마를 분석하고 go 코드를 생성하며,
-> 이 코드는 db/models 디렉토리에 저장된다.
wipe = true
-> 이 설정은 이전에 생성된 코드를 새로운 코드로 완전히 덮어쓴다.
-> 따라서 각 번 코드를 재생성할 때 이전 코드가 삭제된다.
no-tests = true
add-enum-types = true
pkgname = "dbmodels"
-> 이 설정은 생성된 Go 패키지의 이름을 지정한다. 생성된 코드는 이 패키지에 속하게 된다.
add-global-variants = true
[psql]
-> 부분은 PostgreSQL 데이터베이스 구성을 지정한다.
-> 이 부분에서는 데이터베이스 이름, 호스트, 포트, 사용자 이름 및 비밀번호를 설정한다.
dbname = "test"
host = "localhost"
port = 5432
user = "testuser"
pass = "1234"
sslmode = "disable"
8. 이제 cli가 설치되고 구성 파일이 설정되었으므로 orm 클라이언트를 생성할 수 있다.
$ sqlboiler psql
-> 위 명령어를 실행하면 아래 사진처럼 자동으로 디렉토리가 생기고 코드가 생긴다.
9. form 형식에 html 넣어서 연결하는 것은 gorm이랑 같다.
[sqlboiler를 사용하다가 막혔던 부분]
1. 아래와 같이 board를 불러왔을때 {}, true가 자동으로 붙어왔다.
2. id값을 받지 못해 view,edit,delete가 실행되지 않았다.
[해결]
1. 저 부분은 insert메서드나 update 메서드 코드를 보면 null.StringFrom을 사용했다.
사용한 이유는 Go 언어에서 데이터베이스와 상호 작용할 때, null 가능한 값이 있는 필드를 효과적으로 처리하기 위함이다.
null.StringFrom 함수는 Go언어의 database/sql 패키지와 함께 사용되며, null 가능한 문자열 값을 나타낸다. Go에서 문자열은 기본적으로 null 값을 가질 수 없다. 그러나 데이터베이스 테이블의 문자열 필드는 null 값을 가질 수 있으므로, 이를 처리하기 위해 null.StringFrom을 사용한다!!!!!
그리고 아래 코드에서 null.StringFrom 함수를 사용하지않고 content 처럼 title, writer를 사용하면 아래 문구처럼 에러가 뜬다..
cannot use title (variable of type string) as null.String value in struct literal
func Insert(c *gin.Context) {
if c.Request.Method == "POST" {
title := c.PostForm("title")
content := c.PostForm("content")
writer := c.PostForm("writer")
// 게시글 생성
newBoard := models.Board{
Title: null.StringFrom(title),
Content: content,
Writer: null.StringFrom(writer),
}
// 게시글을 데이터베이스에 추가
if err := newBoard.Insert(context.Background(), db, boil.Infer()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Redirect(http.StatusSeeOther, "/api/v4/")
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request method"})
}
}
func Update(c *gin.Context) {
if c.Request.Method == "POST" {
// 폼 데이터에서 필요한 정보 추출
title := c.PostForm("title")
content := c.PostForm("content")
writer := c.PostForm("writer")
// 새 게시물 생성
newBoard := models.Board{
Title: null.NewString(title, true),
Content: content,
Writer: null.NewString(writer, true),
}
log.Println("neBoard:", newBoard)
// 게시물을 데이터베이스에 추가
if err := newBoard.Insert(context.Background(), db, boil.Infer()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Redirect(http.StatusSeeOther, "/api/v4/")
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request method"})
}
}
1-1. 이 분을 해결했는데 html에서 아직도 {},true가 나왔다. 그래서 html을 의심했다..
기존 코드는 {{ .Title }} , {{ .Content }}, {{ .Writer }} 로 되어 있었는데 이 부분 때문이였다.
템플릿에서 {{ if .Title.Valid }} 와 같은 코드를 사용하는 이유는 Go 언어의 html/template 패키지에서 null 가능한 값을 처리하기 위한 것이다. 데이터베이스에서 가져온 데이터의 일부는 null 가능한 필드일 수 있으며, Go에서 null 값을 나타내는 표준 방법이 없다. 따라서 Go 언어에서 null 가능한 값에 대한 처리를 명시적으로 해줘야 한다.
{{ define "Edit" }}
<!DOCTYPE html>
<html>
<head>
<title>Edit Board Entry</title>
</head>
<body>
{{ template "Menu" . }}
<h2>Edit Board Entry</h2>
<form method="POST" action="/api/v4/boards">
<input type="hidden" name="id" value="{{ .ID }}" />
<label for="title">Title</label>
<input type="text" id="title" name="title" value="{{ if .Title.Valid }}{{ .Title.String }}{{ end }}" /><br />
<label for="content">Content</label>
<textarea id="content" name="content">{{ if .Content }}{{ .Content }}{{ end }}</textarea><br />
<label for="writer">Writer</label>
<input type="text" id="writer" name="writer" value="{{ if .Writer.Valid }}{{ .Writer.String }}{{ end }}" /><br />
<input type="submit" value="Save Board" />
</form><br />
</body>
</html>
{{ end }}
{{ define "Index" }}
{{ template "Menu" }}
<!DOCTYPE html>
<html>
<head>
<title>게시판연습</title>
</head>
<body>
<h1>게시판연습</h1>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Writer</th>
<th>CreatedAt</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{ range . }}
<tr>
<td>{{ .ID }}</td>
<td>
{{ if .Title.Valid }}
{{ .Title.String }}
{{ end }}
</td>
<td>
{{ if .Writer.Valid }}
{{ .Writer.String }}
{{ end }}
</td>
<td>
{{ if .CreatedAt.Valid }}
{{ .CreatedAt.Time }}
{{ end }}
</td>
<td>
<a href="/api/v4/board/{{ .ID }}">View</a>
<a href="/api/v4/boards/{{ .ID }}">Edit</a>
<a href="/api/v4/boardss/{{ .ID }}">Delete</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</body>
</html>
{{ end }}
{{ define "Menu" }}
<!DOCTYPE html>
<html>
<head>
<title>Menu</title>
</head>
<body>
<a href="/api/v4/">HOME</a> |
<a href="/api/v4/boards/">NEW</a>
</body>
</html>
{{ end }}
{{ define "New" }}
{{ template "Menu" }}
<h2>Create New Board Entry</h2>
<form method="POST" action="/api/v4/board">
<label> Title </label><input type="text" name="title" /><br />
<label> Content </label><textarea name="content"></textarea><br />
<label> Writer </label><input type="text" name="writer" /><br />
<input type="submit" value="Save Board" />
</form>
{{ end }}
{{ define "Show" }}
<!DOCTYPE html>
<html>
<head>
<title>게시물 #{{ .ID }}</title>
</head>
<body>
{{ template "Menu" }}
<h2>게시물 #{{ .ID }}</h2>
{{ if .Title.Valid }}<p>제목: {{ .Title.String }}</p>{{ end }}
{{ if .Content }}<p>내용: {{ .Content }}</p>{{ end }}
{{ if .Writer.Valid }}<p>작성자: {{ .Writer.String }}</p>{{ end }}
<br /><a href="/api/v4/boards/{{ .ID }}">수정</a>
</body>
</html>
{{ end }}
2. show, delete, edit 메서드들은 모두 파라미터값으로 id를 받아와야 하는데.. 받아오질 못했다.
계속 웹페이지에서 {"error":"Invalid ID"} 라는 문구가 떴다.
알고보니 c.Param("id")를 가져오면 문자열 형태로 가져오게 되는 것이었고,
func (*gin.Context).Param(key string) string
위 부분 EQ(id) 에서 func (models.whereHelperint).EQ(x int)
int로 받아와야해서 타입 에러가 났다.
그래서 결국 문자열 형태인 id를 정수로 변환 후 넣어줬더니 오류가 나지 않았다.
func Show(c *gin.Context) {
idStr := c.Param("id") //문자열 형태로 저장
//fmt.Printf("idStr: %v\n", idStr) //id값 로그 찍어보기
id, err := strconv.Atoi(idStr) //문자열 형태를 정수로 변환
if err != nil {
c.JSON(400, gin.H{"error": "Invalid ID"})
return
}
// 사용자 모델 (예: User)을 가져와서 'board' 테이블과 연결합니다.
boardItem, err := models.Boards(models.BoardWhere.ID.EQ(id)).One(context.Background(), db)
if err != nil {
c.JSON(500, gin.H{"error": "Error retrieving board data"})
return
}
if boardItem == nil {
c.JSON(404, gin.H{"error": "Board not found"})
return
}
// 이제 'boardItem' 변수를 템플릿에 전달할 수 있습니다.
tmpl.ExecuteTemplate(c.Writer, "Show", boardItem)
}
func Edit(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr) //문자열 형태를 정수로 변환
if err != nil {
c.JSON(400, gin.H{"error": "Invalid ID"})
return
}
boardItem, err := models.Boards(models.BoardWhere.ID.EQ(id)).One(context.Background(), db)
if err != nil {
c.JSON(500, gin.H{"error": "Error retrieving board data"})
return
}
if boardItem == nil {
c.JSON(404, gin.H{"error": "Board not found"})
return
}
// 이제 'boardItem' 변수를 템플릿에 전달할 수 있습니다.
tmpl.ExecuteTemplate(c.Writer, "Edit", boardItem)
}
func Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid ID"})
return
}
boardItem, err := models.Boards(models.BoardWhere.ID.EQ(id)).One(context.Background(), db)
if err != nil {
c.JSON(500, gin.H{"error": "Error retrieving board data"})
return
}
if boardItem == nil {
c.JSON(404, gin.H{"error": "Board not found"})
return
}
// 게시물 삭제
_, deleteErr := boardItem.Delete(context.Background(), db)
if deleteErr != nil {
c.JSON(500, gin.H{"error": "Error deleting board"})
return
}
// 게시물 삭제 성공 시, 게시물 목록 페이지로 리다이렉트
c.Redirect(http.StatusSeeOther, "/api/v4/")
}
'Language > Go' 카테고리의 다른 글
gorilla/mux - swagger 적용 (0) | 2023.11.16 |
---|---|
GO - sqlboiler (0) | 2023.10.26 |
Go 게시판 정리- 2 (0) | 2023.10.12 |
Go[postgresql db] - 간단한 게시판 만들기 (0) | 2023.10.11 |
Go 시작 (0) | 2023.09.17 |