Open5

jackc/pgtype#Daterange の使い方

ピン留めされたアイテム
シロシロ

このコードの一部は、pgtype.Daterange構造体を定義し、そのプロパティを設定しています。以下に、各プロパティの詳細を説明します。

  1. LowerおよびUpper: これらはpgtype.Date型で、日付範囲の下限(開始日)と上限(終了日)を表します。pgtype.Date構造体には、Timetime.Time型)とStatusプロパティがあります。Timeプロパティには、日付を格納します。Statusプロパティは、日付が存在するかどうかを示すpgtype.Status型です。

  2. Status: pgtype.Statusは、値の状態を示す列挙型で、以下の値を持ちます。

    • pgtype.Present: 値が存在します。
    • pgtype.Null: 値がNULLです。
    • pgtype.Undefined: 値が未定義です。これは、値がデータベースから読み取られていないことを意味します。
  3. LowerTypeおよびUpperType: これらは、範囲の下限と上限がそれぞれ閉区間(含む)か開区間(含まない)かを示すpgtype.RangeType型です。以下の値があります。

    • pgtype.Inclusive: 閉区間(範囲に値を含む)を示します。
    • pgtype.Exclusive: 開区間(範囲に値を含まない)を示します。
    • pgtype.Empty: 範囲が空です。これは、範囲の下限と上限が未定義であることを意味します。

この例では、Daterange構造体のLowerUpperプロパティに、開始日と終了日を設定しています。また、Statuspgtype.Presentを設定して、値が存在することを示しています。LowerTypepgtype.Inclusiveを、UpperTypepgtype.Exclusiveを設定しています。つまり、この日付範囲は開始日を含み、終了日を含まない範囲です。

シロシロ

jackc/pgtypeは、Go言語でPostgreSQLデータベースとのやり取りをする際に、PostgreSQL特有のデータ型をサポートするためのライブラリです。Daterangeはその中の1つで、日付の範囲を表現します。

まず、jackc/pgtypeパッケージをインストールしてください。

go get github.com/jackc/pgtype

以下は、Daterangeを使って日付範囲を操作する例です:

package main

import (
    "database/sql"
    "fmt"
    "github.com/jackc/pgtype"
    _ "github.com/jackc/pgx/v4/stdlib"
    "log"
    "time"
)

func main() {
    connStr := "user=yourusername password=yourpassword dbname=yourdb host=localhost sslmode=disable"
    db, err := sql.Open("pgx", connStr)
    if err != nil {
        log.Fatal(err)
    }

    // Create a table with a daterange column
    _, err = db.Exec("CREATE TEMP TABLE events (id serial PRIMARY KEY, dates daterange)")
    if err != nil {
        log.Fatal(err)
    }

    // Insert a daterange value into the table
    startDate := time.Date(2023, 4, 15, 0, 0, 0, 0, time.UTC)
    endDate := time.Date(2023, 4, 20, 0, 0, 0, 0, time.UTC)

    daterange := pgtype.Daterange{
        Lower: pgtype.Date{
            Time:   startDate,
            Status: pgtype.Present,
        },
        Upper: pgtype.Date{
            Time:   endDate,
            Status: pgtype.Present,
        },
        LowerType: pgtype.Inclusive,
        UpperType: pgtype.Exclusive,
        Status: pgtype.Present,
    }

    _, err = db.Exec("INSERT INTO events (dates) VALUES ($1)", daterange)
    if err != nil {
        log.Fatal(err)
    }

    // Query the table and read the daterange value
    var queriedDaterange pgtype.Daterange
    err = db.QueryRow("SELECT dates FROM events WHERE id = 1").Scan(&queriedDaterange)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Daterange: %v\n", queriedDaterange)
}

この例では、以下のことを行っています:

  1. jackc/pgxドライバを使用してPostgreSQLデータベースに接続します。
  2. daterange型のカラムを持つ一時テーブルeventsを作成します。
  3. Daterange構造体を作成し、LowerUpperプロパティに範囲の開始と終了日付を設定します。LowerTypeUpperTypeで範囲の境界タイプを設定します。
  4. 作成したDaterange構造体をテーブルに挿入します。
  5. テーブルからDaterange値を取得し、表示します。
シロシロ

開始日と終了日が同じ日の場合のサンプルコード

開始日と終了日が同じ日の場合でも、pgtype.Daterange構造体を使用して日付範囲を表現できます。次のサンプルコードでは、開始日と終了日が同じ日であることを示す日付範囲を作成しています。

package main

import (
    "database/sql"
    "fmt"
    "github.com/jackc/pgtype"
    _ "github.com/jackc/pgx/v4/stdlib"
    "log"
    "time"
)

func main() {
    connStr := "user=yourusername password=yourpassword dbname=yourdb host=localhost sslmode=disable"
    db, err := sql.Open("pgx", connStr)
    if err != nil {
        log.Fatal(err)
    }

    // Create a table with a daterange column
    _, err = db.Exec("CREATE TEMP TABLE events (id serial PRIMARY KEY, dates daterange)")
    if err != nil {
        log.Fatal(err)
    }

    // Insert a daterange value with the same start and end date into the table
    eventDate := time.Date(2023, 4, 15, 0, 0, 0, 0, time.UTC)

    daterange := pgtype.Daterange{
        Lower: pgtype.Date{
            Time:   eventDate,
            Status: pgtype.Present,
        },
        Upper: pgtype.Date{
            Time:   eventDate.AddDate(0, 0, 1), // Add one day to make the range valid
            Status: pgtype.Present,
        },
        LowerType: pgtype.Inclusive,
        UpperType: pgtype.Exclusive,
        Status: pgtype.Present,
    }

    _, err = db.Exec("INSERT INTO events (dates) VALUES ($1)", daterange)
    if err != nil {
        log.Fatal(err)
    }

    // Query the table and read the daterange value
    var queriedDaterange pgtype.Daterange
    err = db.QueryRow("SELECT dates FROM events WHERE id = 1").Scan(&queriedDaterange)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Daterange: %v\n", queriedDaterange)
}

この例では、eventDateが開始日と終了日の両方になります。ただし、Upperプロパティには、eventDateに1日を加算した値を設定しています。これは、上限が開区間(終了日を含まない)であるため、範囲が正しく表現されるようにするためです。

このサンプルコードを実行すると、同じ開始日と終了日を持つ日付範囲がデータベースに保存され、その範囲がコンソールに表示されます。

シロシロ

pgtype.Daterangeのポインタ型は使用しないのでしょうか?

pgtype.Daterangeのポインタ型(*pgtype.Daterange)を使用することもできますが、通常は必要ありません。pgtype.Daterange構造体は、LowerおよびUpperプロパティを持ち、それぞれpgtype.Date型です。pgtype.Dateは、Statusプロパティを持ち、これにより値が存在するかどうかを示すことができます。そのため、ポインタ型を使用せずに、pgtype.Daterange構造体自体でNULL値や未定義の値を表現できます。

ただし、特定のユースケースやコードの構造に応じて、ポインタ型を使用することが適切な場合もあります。たとえば、関数がpgtype.Daterange構造体を返す場合、エラーまたは未定義の状況でnilを返すことができます。この場合、ポインタ型を使用することが適切です。

以下は、pgtype.Daterangeのポインタ型を使用して日付範囲をデータベースに挿入するサンプルコードです。

package main

import (
	"database/sql"
	"fmt"
	"github.com/jackc/pgtype"
	_ "github.com/jackc/pgx/v4/stdlib"
	"log"
	"time"
)

func main() {
	connStr := "user=yourusername password=yourpassword dbname=yourdb host=localhost sslmode=disable"
	db, err := sql.Open("pgx", connStr)
	if err != nil {
		log.Fatal(err)
	}

	// Create a table with a daterange column
	_, err = db.Exec("CREATE TEMP TABLE events (id serial PRIMARY KEY, dates daterange)")
	if err != nil {
		log.Fatal(err)
	}

	startDate := time.Date(2023, 4, 15, 0, 0, 0, 0, time.UTC)
	endDate := time.Date(2023, 4, 20, 0, 0, 0, 0, time.UTC)

	// Create a Daterange pointer
	daterange := &pgtype.Daterange{
		Lower: pgtype.Date{
			Time:   startDate,
			Status: pgtype.Present,
		},
		Upper: pgtype.Date{
			Time:   endDate,
			Status: pgtype.Present,
		},
		LowerType: pgtype.Inclusive,
		UpperType: pgtype.Exclusive,
		Status: pgtype.Present,
	}

	_, err = db.Exec("INSERT INTO events (dates) VALUES ($1)", daterange)
	if err != nil {
		log.Fatal(err)
	}

	// Query the table and read the daterange value
	var queriedDaterange pgtype.Daterange
	err = db.QueryRow("SELECT dates FROM events WHERE id = 1").Scan(&queriedDaterange)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Daterange: %v\n", queriedDaterange)
}

このサンプルコードでは、pgtype.Daterangeのポインタ型を使用して、日付範囲をデータベースに挿入しています。しかし、この例ではポインタ型を使用する特別な理由はありません。pgtype.Daterange構造体自体でNULL値や未定義の値を表現できるため、通常はポインタ型を使用する必要はありません。

ポインタ型を使用する主な理由は、関数がpgtype.Daterange構造体を返し、エラーまたは未定義の状況でnilを返す場合です。この場合、ポインタ型を使用することが適切です。ただし、ほとんどのユースケースでは、pgtype.Daterange構造体を直接使用して、範囲の下限および上限の状態を制御することが推奨されます。

一般的に、pgtype.Daterangeのポインタ型を使用することは珍しく、通常は非ポインタ型の構造体を使用して日付範囲を表現します。ただし、特定のユースケースに応じて、ポインタ型を使用することが適切な場合もあります。

シロシロ

LowerTypeUpperTypeと同列のStatusを設定しないとDBの保存時にcannot encode status undefinedのエラーとなる。

	daterange := &pgtype.Daterange{
		Lower: pgtype.Date{
			Time:   startDate,
			Status: pgtype.Present,
		},
		Upper: pgtype.Date{
			Time:   endDate,
			Status: pgtype.Present,
		},
		LowerType: pgtype.Inclusive,
		UpperType: pgtype.Exclusive,
		Status: pgtype.Present, // ← ここを設定しないと `cannot encode status undefined` となる
	}