[Go] MySQL 연결해보기

윤동환·2023년 7월 20일
0

Go

목록 보기
17/18
post-thumbnail

import for MySQL

Go에서는 database/sql package를 위한 MySQL 드라이버인 mysql package를 제공합니다.

설치

$ go get -u github.com/go-sql-driver/mysql

용법

Go MySQL Driver는 Go의 database/sql/driver인터페이스를 구현한 것입니다. 드라이버를 가져오기만 하면 전체 database/sqlAPI를 사용할 수 있습니다.

MySql 드라이버가 설치된 후, 아래와 같이 database/sql과 MySql 드라이버를 import하는데, MySql 드라이버 패키지는 _ 로 alias를 주어 개발자가 드라이버 패키지를 직접 사용하지 않게 합니다. 이 경우 드라이버 패키지는 database/sql 패키지가 내부적으로 사용하게 되며, 개발자는 database/sql를 통해서 모든 SQL 프로세싱을 진행할 수 있게됩니다.

간단 예시

import (
	"database/sql"
	"time"

	_ "github.com/go-sql-driver/mysql"
)
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
	panic(err)
}
// See "Important settings" section.
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
  1. db, err := sql.Open("mysql", "user:password@/dbname")
    -> database에 대한 handle을 가져옵니다.

    Package database/sql은 연결 풀을 background에서 관리합니다. 그리고 개발자가 필요로 하기 전까지 그 어떤 연결도 하지 않습니다. 그러므로 sql.Open()은 직접 연결을 하지 않습니다.
    결과적으로 sql.Open()은 만약 server가 사용불가능하거나, 연결 data가 올바르지 않더라도 error를 return하지 않습니다. 만약 queries를 만들기 전 db.Ping()을 사용하여 이러한 문제점을 확인할 수 있습니다.
    코드에서 반환받는 err는 객체를 받아오지 못 할 때의 err입니다.
    -> 즉, sql.DB는 드라이버종류와 Connection 정보를 가지고는 있지만, 실제 DB를 연결하지 않으며, 많은 경우 Connection 정보조차 체크하지도 않는다. 실제 DB Connection은 Query 등과 같이 실제 DB 연결이 필요한 시점에 이루어지게 됩니다.

  2. db.SetConnMaxLifetime()
    MySQL 서버, OS 또는 기타 미들웨어에 의해 연결이 닫히기 전에 드라이버에 의해 안전하게 연결이 닫히도록 하기 위해 필요합니다. 일부 미들웨어는 유휴 연결을 5분 단위로 종료하므로 5분 미만의 시간 제한을 권장합니다. 이 설정은 로드 밸런싱 및 시스템 변수 변경에도 도움이 됩니다.

  3. db.SetMaxOpenConns()
    응용 프로그램에서 사용하는 연결 수를 제한하는 것이 좋습니다. 애플리케이션 및 MySQL 서버에 따라 다르기 때문에 권장되는 제한 번호는 없습니다.

  4. db.SetMaxIdleConns()
    db.SetMaxOpenConns()와 동일하게 설정하는 것이 좋습니다. SetMaxOpenConns()보다 작으면 예상보다 훨씬 더 자주 연결을 열고 닫을 수 있습니다.

DSN 설명

DSN

driverName으로는 mysql, 유효한 DSN로는 dataSourceName같은 것이 있으며, 이러한 DSN을 사용하는 것을 추천합니다.
데이터 소스 이름은 예를 들어 PEAR DB에서 사용하는 것과 같은 공통 형식을 갖지만 유형 접두어는 없습니다(대괄호로 표시된 선택적 부분).

//규격
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
//완전한 형태
username:password@protocol(address)/dbname?param=value
//최소 DSN
/dbname
[더 많은 예시](https://github.com/go-sql-driver/mysql#examples)

Password

암호는 모든 문자로 구성될 수 있습니다. escape 문은 필요 하지 않습니다.

Protocol

가능하면 Unix domain socket을 사용해야 하며, 그렇지 못할경우엔 최고의 성능을 위해 TCP를 사용하여야 합니다.

Address

TCP 및 UDP 네트워크의 경우 주소 형식은 host[:port]입니다. 포트를 생략하면 기본 포트가 사용됩니다. 호스트가 리터럴 IPv6 주소인 경우 대괄호로 묶어야 합니다.

net.JoinHostPort 및 net.SplitHostPort 함수는 이 형식의 주소를 조작합니다.

Unix 도메인 소켓의 경우 주소는 MySQL-Server-socket의 절대 경로입니다
(예: /var/run/mysqld/mysqld.sock또는 /tmp/mysql.sock).

Parameters

매개변수는 대소문자를 구분합니다!

true, TRUE, True 혹은 1을 구별할 수 있으며, 기본적으로 boolean 타입의 true를 허용합니다. 당연히 false는 , false, FALSE, False or 0 중 하나로 지정할 수 있습니다.

allowAllFiles

Type:           bool
Valid Values:   true, false
Default:        false

allowAllFiles=true
LOAD DATA LOCAL INFILE를 위함 파일 허용 목록을 비활성화 하고 모든 파일을 허용합니다

안전하지 않습니다!

allowCleartextPasswords

Type:           bool
Valid Values:   true, false
Default:        false

allowCleartextPasswords=truePAM 인증 플러그인 으로 정의된 계정과 같은 계정에서 필요한 경우 일반 텍스트 클라이언트 측 플러그인을 사용할 수 있습니다.
암호를 일반 텍스트로 보내는 것은 일부 구성에서 보안 문제가 될 수 있습니다.
암호를 보호하는 방법(TLS/SSL , IPsec 또는 개인 네트워크)을 사용하여 MySQL Server에 연결해야 합니다.

allowFallbackToPlaintext

Type:           bool
Valid Values:   true, false
Default:        false

allowFallbackToPlaintext=true서버에 연결하기 위한 명령 옵션--ssl-mode=PREFERRED 에 설명된 대로 MySQL 클라이언트 처럼 작동합니다.

allowNativePasswords

Type:           bool
Valid Values:   true, false
Default:        false

allowNativePasswords=falseMySQL 기본 암호 방법의 사용을 허용하지 않습니다.

allowOldPasswords

Type:           bool
Valid Values:   true, false
Default:        false

charset

Type:           string
Valid Values:   <name>
Default:        none

클라이언트-서버 상호 작용에 사용되는 문자 집합을 설정합니다
charset=utf8mb4,utf8.

checkConnLiveness

Type:           bool
Valid Values:   true, false
Default:        true

지원하는 플랫폼에서 연결을 사용하기 전에 연결 풀에서 살아있는 연결을 확인하여 가져옵니다.

collation

Type:           string
Valid Values:   <name>
Default:        utf8mb4_general_ci

clientFoundRows

Type:           bool
Valid Values:   true, false
Default:        false

clientFoundRows=trueUPDATE가 변경된 행 수 대신 일치하는 행 수를 반환하도록 합니다.

columnsWithAlias

Type:           bool
Valid Values:   true, false
Default:        false

columnsWithAlias값이 true라면 sql.Rows.Columns()를 실행했을 때 id를 반환하는 대신 u.id를 반환합니다.

interpolateParams

Type:           bool
Valid Values:   true, false
Default:        false

interpolateParams가 true인 경우 에 ?대한 호출의 자리 표시자( )는 지정된 매개 변수를 사용 db.Query()하여 db.Exec()단일 쿼리 문자열로 보간됩니다. 이렇게 하면 드라이버가 명령문을 준비하고 주어진 매개변수로 실행해야 하므로 왕복 횟수가 줄어듭니다.
interpolateParams=false로 명령문을 다시 닫습니다.

loc

Type:           string
Valid Values:   <escaped name>
Default:        UTC

time.Time 값의 위치를 설정합니다

maxAllowedPacket

Type:          decimal number
Default:       64*1024*1024

허용되는 최대 패킷 크기(바이트)입니다. 기본값은 64MiB이며 서버 설정과 일치하도록 조정해야 합니다.
maxAllowedPacket=0은 모든 연결에서 서버에서 max_allowed_packet 변수를 자동으로 가져오는 데 사용할 수 있습니다.

multiStatements

Type:           bool
Valid Values:   true, false
Default:        false

하나의 쿼리에서 여러 문을 허용합니다. 이것은 여러 쿼리를 처리하는 데 사용할 수 있습니다. Rows.NextResultSet()을 사용하여 두 번째 및 후속 쿼리의 결과를 가져옵니다.

parseTime

Type:           bool
Valid Values:   true, false
Default:        false

parseTime=true는 DATE 및 DATETIME 값의 출력 유형을 []byte / string 대신 time.Time으로 변경합니다.
0000-00-00 00:00:00과 같은 날짜 또는 날짜/시간은 time.Time의 0 값으로 변환됩니다.

readTimeout

Type:           duration
Default:        0

I/O read timeout입니다.
값은 "30s", "0.5m" 또는 "1m30s"와 같이 단위 접미사("ms", "s", "m", "h")가 있는 십진수여야 합니다.

rejectReadOnly

Type:           bool
Valid Values:   true, false
Default:        false

rejectReadOnly=true드라이버가 읽기 전용 연결을 거부하도록 합니다. 이것은 mysql 클라이언트가 장애 조치 후 읽기 전용 복제본에 연결되는 자동 장애 조치 중에 발생할 수 있는 경쟁 조건에 대한 것입니다.

tls

Type:           bool / string
Valid Values:   true, false, skip-verify, preferred, <name>
Default:        false

tls=true서버에 대한 TLS/SSL 암호화 연결을 활성화합니다. skip-verify자체 서명 또는 유효하지 않은 인증서(서버 측)를 사용하려는 경우 사용 하거나 preferred서버에서 광고할 때만 TLS를 사용하는 데 사용합니다.

writeTimeout

I/O write timeout입니다.
값은 "30s", "0.5m" 또는 "1m30s"와 같이 단위 접미사("ms", "s", "m", "h")가 있는 십진수여야 합니다.

connectionAttributes

Type:           comma-delimited string of user-defined "key:value" pairs
Valid Values:   (<name1>:<value1>,<name2>:<value2>,...)
Default:        none

연결 속성은 애플리케이션 프로그램이 연결 시 서버에 전달할 수 있는 키-값 쌍입니다.

조회 예시

MySQL에서 쿼리를 조회할 때 2종류의 메서드를 사용합니다.
하나의 Row만 리턴할 경우 QueryRow() 메서드를, 복수개의 Row를 리턴할 경우 Query() 메서드를 사용합니다.
하나의 Row에서 실제 데이타를 읽어 로컬 변수에 할당하기 위해 Scan() 메서드를 사용하며, 복수 Row에서 다음 Row로 이동하기 위해 Next() 메서드를 사용합니다.

Single Row

package main
 
import (
    "database/sql"
    "fmt"
    "log"
    _ "github.com/go-sql-driver/mysql"
)
 
func main() {
    // sql.DB 객체 생성
    db, err := sql.Open("mysql", "root:pwd@tcp(127.0.0.1:3306)/testdb")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
 
    // 하나의 Row를 갖는 SQL 쿼리
    var name string
    err = db.QueryRow("SELECT name FROM test1 WHERE id = 1").Scan(&name)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(name)

Multirows

func main() {
    // sql.DB 객체 생성
    db, err := sql.Open("mysql", "root:pwd@tcp(127.0.0.1:3306)/testdb")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
 
    // 복수 Row를 갖는 SQL 쿼리
    var id int
    var name string
    rows, err := db.Query("SELECT id, name FROM test1 where id >= ?", 1)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close() //반드시 닫는다 (지연하여 닫기)
 
    for rows.Next() {
        err := rows.Scan(&id, &name)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(id, name)
    }
}

삽입 및 조회 예시2

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, err := sql.Open("mysql", "user:password@/database")
	if err != nil {
		panic(err.Error())  // Just for example purpose. You should use proper error handling instead of panic
	}
	defer db.Close()

	// Prepare statement for inserting data
	stmtIns, err := db.Prepare("INSERT INTO squareNum VALUES( ?, ? )") // ? = placeholder
	if err != nil {
		panic(err.Error()) // proper error handling instead of panic in your app
	}
	defer stmtIns.Close() // Close the statement when we leave main() / the program terminates

	// Prepare statement for reading data
	stmtOut, err := db.Prepare("SELECT squareNumber FROM squarenum WHERE number = ?")
	if err != nil {
		panic(err.Error()) // proper error handling instead of panic in your app
	}
	defer stmtOut.Close()

	// Insert square numbers for 0-24 in the database
	for i := 0; i < 25; i++ {
		_, err = stmtIns.Exec(i, (i * i)) // Insert tuples (i, i^2)
		if err != nil {
			panic(err.Error()) // proper error handling instead of panic in your app
		}
	}

	var squareNum int // we "scan" the result in here

	// Query the square-number of 13
	err = stmtOut.QueryRow(13).Scan(&squareNum) // WHERE number = 13
	if err != nil {
		panic(err.Error()) // proper error handling instead of panic in your app
	}
	fmt.Printf("The square number of 13 is: %d", squareNum)

	// Query another number.. 1 maybe?
	err = stmtOut.QueryRow(1).Scan(&squareNum) // WHERE number = 1
	if err != nil {
		panic(err.Error()) // proper error handling instead of panic in your app
	}
	fmt.Printf("The square number of 1 is: %d", squareNum)
}

Prepared Statement는 데이타베이스 서버에 Placeholder를 가진 SQL문을 미리 준비시키는 것으로, 차후 해당 Statement를 호출할 때 준비된 SQL문을 빠르게 실행하도록 하는 기법이다. Go에서 Prepared Statement를 사용하기 위해서는 sql.DB의 Prepare() 메서드를 써서 Placeholder를 가진 SQL문을 미리 준비시키고, sql.Stmt 객체를 리턴받는다. 차후 이 sql.Stmt 객체의 Exec (혹은 Query/QueryRow) 메서드를 사용하여 준비된 SQL문을 실행한다.

RawBytes를 키값으로 나눠 출력하기

RawBytes는 데이터베이스 자체가 소유한 메모리에 대한 참조를 보유하는 바이트 슬라이스입니다. RawBytes로 스캔한 후 슬라이스는 다음에 Next, Scan 또는 Close를 호출할 때까지만 유효합니다.

Go 1.1부터는 time.Time이 DATE 및 DATETIME 값을 스캔할 수 있는 유일한 변수 유형이 됩니다. Date부분은 RawBytes를 지원이 중단 된다고합니다. 출처 (작성일 2023-07-20)

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	// Open database connection
	db, err := sql.Open("mysql", "user:password@/dbname")
	if err != nil {
		panic(err.Error())  // Just for example purpose. You should use proper error handling instead of panic
	}
	defer db.Close()

	// Execute the query
	rows, err := db.Query("SELECT * FROM table")
	if err != nil {
		panic(err.Error()) // proper error handling instead of panic in your app
	}

	// Get column names
	columns, err := rows.Columns()
	if err != nil {
		panic(err.Error()) // proper error handling instead of panic in your app
	}

	// Make a slice for the values
	values := make([]sql.RawBytes, len(columns))

	// rows.Scan wants '[]interface{}' as an argument, so we must copy the
	// references into such a slice
	// See http://code.google.com/p/go-wiki/wiki/InterfaceSlice for details
	scanArgs := make([]interface{}, len(values))
	for i := range values {
		scanArgs[i] = &values[i]
	}

	// Fetch rows
	for rows.Next() {
		// get RawBytes from data
		err = rows.Scan(scanArgs...)
		if err != nil {
			panic(err.Error()) // proper error handling instead of panic in your app
		}

		// Now do something with the data.
		// Here we just print each column as a string.
		var value string
		for i, col := range values {
			// Here we can check if the value is nil (NULL value)
			if col == nil {
				value = "NULL"
			} else {
				value = string(col)
			}
			fmt.Println(columns[i], ": ", value)
		}
		fmt.Println("-----------------------------------")
	}
	if err = rows.Err(); err != nil {
		panic(err.Error()) // proper error handling instead of panic in your app
	}
}

Reference

https://pkg.go.dev/database/sql#pkg-types
http://golang.site/go/article/110-MS-SQL-Server-%EC%82%AC%EC%9A%A9
http://golang.site/go/article/107-MySql-%EC%82%AC%EC%9A%A9---%EC%BF%BC%EB%A6%AC
https://pkg.go.dev/github.com/go-sql-driver/mysql
https://github.com/go-sql-driver/mysql#usage
https://github.com/go-sql-driver/mysql/wiki/Examples
https://pear.php.net/manual/en/package.database.db.intro-dsn.php

profile
모르면 공부하고 알게되면 공유하는 개발자

2개의 댓글

comment-user-thumbnail
2023년 7월 20일

아주 유용한 정보네요!

1개의 답글