Goでページネーションを実装する方法:リソース分割と効率的なデータ取得

Go言語で大量のデータを扱う際、効率的なデータ分割と取得は欠かせません。特にWebアプリケーションやAPI開発では、クライアントに対して膨大なデータを一度に送信するのではなく、少量ずつ分割して提供する仕組みが必要です。これが「ページネーション」と呼ばれる手法です。ページネーションを適切に設計・実装することで、ユーザー体験を向上させ、アプリケーションのパフォーマンスを最適化できます。本記事では、Go言語を使用して、ページネーションの基本概念から実装方法、さらに応用までを詳しく解説します。初めてページネーションを実装する方から、実務で役立つ高度なテクニックを学びたい方まで、幅広い読者に役立つ内容を目指します。

目次

ページネーションとは何か


ページネーションとは、大量のデータやリソースを一定数ごとに分割して表示する手法のことです。たとえば、検索エンジンの検索結果やECサイトの商品リストがその典型例です。ページネーションを適切に実装することで、以下のような利点があります。

ユーザー体験の向上


すべてのデータを一度に表示すると、読み込み時間が長くなり、ユーザーが必要な情報を探すのに苦労します。ページネーションにより、情報を小分けにして表示することで、スムーズな操作性が実現します。

サーバーとクライアントの負荷軽減


データを分割して少しずつクライアントに提供することで、サーバーの処理負荷を軽減できます。また、クライアント側のメモリ使用量を抑えることも可能です。

具体例:ECサイトの商品リスト


例えば、ECサイトで1000件の商品がある場合、それらを一度に表示するのではなく、1ページあたり20件ずつ表示することで、ユーザーが必要な商品を簡単に探せるようになります。

ページネーションの実装方法


ページネーションは次の要素から構成されます:

  • ページサイズ:1ページに表示するアイテムの数。
  • ページ番号:現在のページを特定する番号。
  • オフセット:データベースなどのデータセットから特定の位置までスキップするための値。

これらの要素を活用して、データを効率的に分割・提供する仕組みを構築できます。次章では、Go言語を用いたページネーションの基本的な構造について解説します。

Go言語でのページネーションの基礎構造


ページネーションをGo言語で実装するには、主にデータの分割と取得を効率的に行う仕組みを構築する必要があります。以下に、基本的なページネーションの構造を示します。

基本的な考え方


ページネーションの実装には、以下の2つのパラメータが不可欠です。

  • ページ番号(Page): 現在のページを特定するための値。
  • ページサイズ(Limit): 1ページあたりのデータ件数。

これらを基に、データベースやデータセットから必要な範囲のデータを取得します。

基本的なコード例


以下は、Goでページネーションを実装するシンプルな例です。

package main

import (
    "fmt"
    "math"
)

// サンプルデータ
var data = []string{"item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10"}

// ページネーション関数
func paginate(data []string, page int, limit int) ([]string, int, int) {
    // データの総数
    total := len(data)
    // 開始インデックスと終了インデックスを計算
    start := (page - 1) * limit
    end := start + limit

    // 範囲外のチェック
    if start >= total {
        return []string{}, total, int(math.Ceil(float64(total) / float64(limit)))
    }
    if end > total {
        end = total
    }

    // 対象データをスライス
    return data[start:end], total, int(math.Ceil(float64(total) / float64(limit)))
}

func main() {
    // ページ番号とページサイズを指定
    page := 2
    limit := 3

    // ページネーションを実行
    paginatedData, total, totalPages := paginate(data, page, limit)

    // 結果を表示
    fmt.Printf("Page %d/%d\n", page, totalPages)
    fmt.Println("Data:", paginatedData)
    fmt.Printf("Total items: %d\n", total)
}

コード解説

  1. データの範囲計算
    startend を計算し、対象データをスライスします。
  2. 範囲外チェック
    ページ番号がデータ総数を超える場合は空のリストを返します。
  3. ページ数の計算
    総データ数をページサイズで割り、全ページ数を算出します。

出力結果


ページ番号を2、ページサイズを3に設定すると、以下の結果が得られます:

Page 2/4
Data: [item4 item5 item6]
Total items: 10

このように、ページ番号とページサイズを柔軟に変更できる構造を用意することで、効率的なページネーションを実現できます。次章では、データベースを使用した具体的な実装方法について説明します。

リソースの分割方法


リソースの分割は、ページネーションを実現するための重要なステップです。データを適切に分割することで、効率的なデータ取得と表示が可能になります。本節では、リソースを分割する際に考慮すべきポイントと、その実装方法について解説します。

リソース分割の基本概念


データを分割する際、以下の2つの要素を考慮します:

  1. ページサイズ(Limit): 1ページに表示するデータの件数を決定します。一般的に10~50件が適切です。
  2. 開始位置(Offset): 分割されたデータのどこから取得を開始するかを指定します。

これにより、大量のデータを一度に処理せず、小分けに扱うことができます。

Goにおけるリソース分割の例


以下に、Goを使用したリソース分割の例を示します。これは、リストを指定したページサイズごとに分割する単純な方法です。

package main

import "fmt"

// リソース分割関数
func splitResources(data []string, page, limit int) ([]string, error) {
    // 開始位置と終了位置を計算
    start := (page - 1) * limit
    end := start + limit

    // 範囲外チェック
    if start >= len(data) || start < 0 {
        return nil, fmt.Errorf("指定されたページ番号が範囲外です")
    }
    if end > len(data) {
        end = len(data)
    }

    // 分割されたデータを返す
    return data[start:end], nil
}

func main() {
    // サンプルデータ
    data := []string{"item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10"}

    // ページ番号とページサイズを指定
    page := 3
    limit := 4

    // リソース分割
    splitData, err := splitResources(data, page, limit)
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }

    // 結果を表示
    fmt.Println("分割されたデータ:", splitData)
}

コード解説

  1. 開始位置と終了位置の計算
    start(page - 1) * limit として計算し、対象データの範囲を決定します。
  2. 範囲外チェック
    start がデータの長さを超える場合、エラーを返します。また、end がデータ長を超える場合は、最大値を適用します。
  3. データスライス
    data[start:end] を利用して、指定範囲のデータを取得します。

出力例


ページ番号を3、ページサイズを4に指定した場合の出力:

分割されたデータ: [item9 item10]

リソース分割の課題と改善方法

  1. 膨大なデータセット
    データが数百万件を超える場合、オフセット計算にコストがかかります。この場合、IDやタイムスタンプによるデータ取得が効果的です。
  2. 非対称データ
    一部のデータが表示されない問題を防ぐため、適切な分割ロジックを設計します。

次章予告


次章では、データベースとページネーションの連携方法について、具体的なSQLクエリとGo言語での実装例を用いて詳しく解説します。

データベースとページネーションの統合


Go言語を使用してデータベースとページネーションを統合することで、大量のデータを効率的に取得できます。本節では、SQLクエリを活用してページネーションを実装する方法と、Goでの具体的な実装例を紹介します。

SQLでのページネーション


データベースでページネーションを実現するには、以下のようなクエリ構文を使用します。

SELECT * 
FROM products
ORDER BY id
LIMIT 10 OFFSET 20;
  • LIMIT: 1ページあたりのデータ件数を指定します(この例では10件)。
  • OFFSET: 取得を開始する位置を指定します(この例では21件目から)。

このクエリでは、products テーブルからID順に21件目から30件目までのデータを取得します。

Goでの実装例


Go言語を用いて、データベースからページネーションを行うコード例を以下に示します。

package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq" // PostgreSQLドライバ
)

// 商品データの構造体
type Product struct {
    ID    int
    Name  string
    Price float64
}

// ページネーション関数
func fetchPaginatedData(db *sql.DB, page, limit int) ([]Product, error) {
    // 開始位置を計算
    offset := (page - 1) * limit

    // クエリを構築
    query := `SELECT id, name, price FROM products ORDER BY id LIMIT $1 OFFSET $2`

    // クエリ実行
    rows, err := db.Query(query, limit, offset)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    // データを格納するスライス
    var products []Product

    // 結果を読み取る
    for rows.Next() {
        var p Product
        if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
            return nil, err
        }
        products = append(products, p)
    }

    return products, nil
}

func main() {
    // データベース接続
    connStr := "user=username dbname=mydb sslmode=disable"
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // ページ番号とページサイズを指定
    page := 2
    limit := 5

    // ページネーションを実行
    products, err := fetchPaginatedData(db, page, limit)
    if err != nil {
        log.Fatal("エラー:", err)
    }

    // 結果を表示
    fmt.Printf("Page %d:\n", page)
    for _, p := range products {
        fmt.Printf("ID: %d, Name: %s, Price: %.2f\n", p.ID, p.Name, p.Price)
    }
}

コード解説

  1. 開始位置の計算
    ページ番号とページサイズからoffsetを計算します。
  2. クエリの作成と実行
    パラメータ化されたクエリを用いて、安全にSQLを実行します。
  3. データの読み取り
    結果を構造体にマッピングし、スライスに格納します。

出力例


ページ番号を2、ページサイズを5に設定した場合の出力例:

Page 2:
ID: 6, Name: Product 6, Price: 60.00
ID: 7, Name: Product 7, Price: 70.00
ID: 8, Name: Product 8, Price: 80.00
ID: 9, Name: Product 9, Price: 90.00
ID: 10, Name: Product 10, Price: 100.00

SQLとGoの統合の課題

  1. 大規模データセットのパフォーマンス
    OFFSETが大きい場合、クエリのパフォーマンスが低下します。この問題を回避するには、カーソルや範囲クエリを使用する方法があります。
  2. 複雑なフィルタリング
    フィルタ条件やソート条件が増えると、クエリが複雑化します。適切なインデックス設計が重要です。

次章では、Goの便利なライブラリを活用して、さらに効率的なページネーションの実装方法を紹介します。

Goの便利なライブラリの活用


Goでは、ページネーションを効率的に実装するために役立つライブラリが多数存在します。これらを活用することで、コードの簡潔さや保守性が向上します。本節では、ページネーション向けの主要なライブラリを紹介し、その使い方を解説します。

主要なライブラリの紹介

  1. gorm.io/gorm
    データベースORMライブラリで、ページネーションの機能を簡単に実現できます。
  2. gin-gonic/gin
    Webフレームワークで、APIページネーションの実装に適しています。
  3. xorm.io/xorm
    シンプルで使いやすいORMライブラリ。ページネーション機能をサポートしています。

GORMを使ったページネーションの実装


以下に、GORMを使用したページネーションの実装例を示します。

package main

import (
    "fmt"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

// 商品データの構造体
type Product struct {
    ID    uint
    Name  string
    Price float64
}

// ページネーション関数
func paginate(db *gorm.DB, page, limit int) ([]Product, error) {
    var products []Product

    // クエリ構築
    offset := (page - 1) * limit
    if err := db.Limit(limit).Offset(offset).Find(&products).Error; err != nil {
        return nil, err
    }

    return products, nil
}

func main() {
    // SQLiteデータベース接続
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic("データベース接続に失敗しました")
    }

    // データベースの初期化
    db.AutoMigrate(&Product{})

    // サンプルデータの挿入
    db.Create(&Product{Name: "Product 1", Price: 10.0})
    db.Create(&Product{Name: "Product 2", Price: 20.0})
    db.Create(&Product{Name: "Product 3", Price: 30.0})
    db.Create(&Product{Name: "Product 4", Price: 40.0})
    db.Create(&Product{Name: "Product 5", Price: 50.0})

    // ページ番号とページサイズを指定
    page := 1
    limit := 3

    // ページネーションを実行
    products, err := paginate(db, page, limit)
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }

    // 結果を表示
    fmt.Printf("Page %d:\n", page)
    for _, p := range products {
        fmt.Printf("ID: %d, Name: %s, Price: %.2f\n", p.ID, p.Name, p.Price)
    }
}

コード解説

  1. LimitOffset メソッド
    GORMのメソッドを利用して、SQLのLIMITOFFSETに対応するクエリを構築します。
  2. 自動マイグレーション
    db.AutoMigrate を使用して、データベースの構造を自動的にセットアップします。
  3. 簡潔なデータ取得
    必要なページデータをFindメソッドで簡単に取得できます。

出力例


ページ番号を1、ページサイズを3に指定した場合の出力:

Page 1:
ID: 1, Name: Product 1, Price: 10.00
ID: 2, Name: Product 2, Price: 20.00
ID: 3, Name: Product 3, Price: 30.00

ライブラリ活用のメリット

  1. 効率性の向上
    手動でSQLクエリを書く必要がなくなり、コードが簡潔になります。
  2. 安全性の向上
    ライブラリがSQLインジェクションを防ぐ仕組みを提供します。
  3. 統合性の向上
    データベース操作とアプリケーションロジックがシームレスに統合されます。

課題と解決方法

  • 複雑なフィルタ条件
    GORMのWhereメソッドを組み合わせることで柔軟なクエリが可能です。
  • 大量データのパフォーマンス
    カーソルベースのページネーションを検討することで、スケーラビリティを向上できます。

次章では、RESTful APIにおけるページネーションの実装について、実例を挙げて解説します。

APIでのページネーションの実装


ページネーションは、RESTful APIにおいて特に重要な要素です。APIの利用者に大量のデータを効率よく提供するためには、ページネーションの仕組みを適切に実装する必要があります。本節では、Go言語を使用したAPIでのページネーション実装を解説します。

RESTful APIでのページネーションの基本設計


RESTful APIでのページネーションは、以下の要素をクエリパラメータとして受け取るのが一般的です:

  • page: ページ番号(例: ?page=2
  • limit: 1ページあたりのデータ数(例: ?limit=10

APIのレスポンスには、以下の情報を含めると利用者にとって便利です:

  • 取得したデータのリスト
  • 総データ数
  • 現在のページ番号
  • 総ページ数

実装例: Ginフレームワークを使用したAPI

以下は、Ginフレームワークを用いてページネーション対応のAPIを実装した例です。

package main

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
)

// 商品データの構造体
type Product struct {
    ID    uint   `json:"id"`
    Name  string `json:"name"`
    Price float64 `json:"price"`
}

// サンプルデータ
var products = []Product{
    {ID: 1, Name: "Product 1", Price: 10.0},
    {ID: 2, Name: "Product 2", Price: 20.0},
    {ID: 3, Name: "Product 3", Price: 30.0},
    {ID: 4, Name: "Product 4", Price: 40.0},
    {ID: 5, Name: "Product 5", Price: 50.0},
    {ID: 6, Name: "Product 6", Price: 60.0},
    {ID: 7, Name: "Product 7", Price: 70.0},
}

// ページネーション用のハンドラ
func getPaginatedProducts(c *gin.Context) {
    // クエリパラメータを取得
    pageStr := c.DefaultQuery("page", "1")
    limitStr := c.DefaultQuery("limit", "3")

    // ページ番号とリミットを変換
    page, err := strconv.Atoi(pageStr)
    if err != nil || page < 1 {
        page = 1
    }

    limit, err := strconv.Atoi(limitStr)
    if err != nil || limit < 1 {
        limit = 3
    }

    // 開始位置と終了位置を計算
    start := (page - 1) * limit
    end := start + limit
    if start >= len(products) {
        c.JSON(http.StatusOK, gin.H{
            "data":       []Product{},
            "total":      len(products),
            "page":       page,
            "totalPages": (len(products) + limit - 1) / limit,
        })
        return
    }
    if end > len(products) {
        end = len(products)
    }

    // 分割されたデータを返す
    c.JSON(http.StatusOK, gin.H{
        "data":       products[start:end],
        "total":      len(products),
        "page":       page,
        "totalPages": (len(products) + limit - 1) / limit,
    })
}

func main() {
    // Ginルーターを作成
    router := gin.Default()

    // ページネーションAPIのエンドポイントを定義
    router.GET("/products", getPaginatedProducts)

    // サーバーを開始
    router.Run(":8080")
}

コード解説

  1. クエリパラメータの取得
    c.DefaultQuery を使用して、pagelimit のデフォルト値を設定しつつ取得します。
  2. データの分割
    startend を計算し、スライス操作でデータを分割します。
  3. レスポンスの構築
    総データ数や総ページ数を含むレスポンスをJSON形式で返します。

APIリクエスト例


クライアントが以下のリクエストを送信:

GET /products?page=2&limit=3

サーバーからのレスポンス例:

{
    "data": [
        {"id": 4, "name": "Product 4", "price": 40.0},
        {"id": 5, "name": "Product 5", "price": 50.0},
        {"id": 6, "name": "Product 6", "price": 60.0}
    ],
    "total": 7,
    "page": 2,
    "totalPages": 3
}

APIページネーションの課題と対策

  1. 大量データへの対応
    OFFSETのコストを削減するために、カーソルベースのページネーション(IDやタイムスタンプを利用)を採用する方法があります。
  2. エラーハンドリング
    不正なページ番号やページサイズに対して適切なレスポンスを返すことで、APIの堅牢性を向上させます。

次章では、ページネーションにおける課題とその具体的な解決策についてさらに詳しく解説します。

ページネーションにおける課題と解決策


ページネーションは、大量のデータを扱うアプリケーションで効果的な手法ですが、適切に設計しないとさまざまな課題に直面します。本節では、一般的な課題とその解決策を解説します。

課題1: OFFSETのパフォーマンス問題


問題点:
SQLクエリでOFFSETを使用すると、データベースが指定した数だけスキップする必要があり、大量データではクエリの実行速度が低下します。

解決策:
カーソルベースのページネーションを使用します。WHERE句で特定のIDやタイムスタンプを利用してページを切り替える方法です。

:

SELECT * 
FROM products 
WHERE id > ? 
ORDER BY id 
LIMIT 10;

Goでの実装例:

func paginateWithCursor(db *gorm.DB, lastID, limit int) ([]Product, error) {
    var products []Product
    if err := db.Where("id > ?", lastID).Order("id").Limit(limit).Find(&products).Error; err != nil {
        return nil, err
    }
    return products, nil
}

課題2: 不正なページ番号やサイズ


問題点:
ユーザーが不正なページ番号やページサイズを指定すると、無効なレスポンスやサーバー負荷が発生する可能性があります。

解決策:

  1. ページ番号とサイズのデフォルト値を設定します。
  2. 許容範囲外の値を制限します。

:

page := 1
limit := 10
if userPage > 0 {
    page = userPage
}
if userLimit > 0 && userLimit <= 100 {
    limit = userLimit
}

課題3: データの一貫性


問題点:
データの更新が頻繁に発生する場合、同じページ番号でも取得するデータが異なる可能性があります。

解決策:

  • ページネーション時にデータのスナップショットを取得する。
    例: クエリにタイムスタンプやバージョン番号を含める。
  • カーソルベースのページネーションを使用し、特定のソート順を維持する。

課題4: フロントエンドでのデータ同期


問題点:
フロントエンドがページネーションをサポートしない場合、スムーズなデータ表示が難しくなります。

解決策:

  • ページネーション情報(総データ数、総ページ数など)をAPIレスポンスに含める。
  • クライアントがデータをキャッシュする仕組みを導入する。

課題5: ユーザーエクスペリエンスの低下


問題点:
ページ切り替えが多すぎると、ユーザーの操作性が悪化します。

解決策:

  • 無限スクロール(Infinite Scroll)を実装する。
  • 「次へ」「前へ」のような簡単なナビゲーションを提供する。

まとめ


ページネーションは、ユーザー体験とシステム効率を向上させるための重要な機能ですが、適切に設計しないと課題が生じます。本節で述べた解決策を活用することで、パフォーマンスとユーザー体験を両立させたページネーションを実現できます。次章では、Goでのページネーションの応用例を具体的に紹介します。

応用例:大規模アプリケーションでの利用


Go言語を用いたページネーションの基本的な実装を学んだら、次は大規模なアプリケーションに応用する方法を理解することが重要です。本節では、実際のシナリオを想定した応用例を解説します。

応用例1: eコマースサイトの商品リスト


シナリオ:
ECサイトでは、商品が多数登録されており、ユーザーはカテゴリや価格、評価などでフィルタリングした商品を確認します。

実装例:

func fetchFilteredProducts(db *gorm.DB, category string, minPrice, maxPrice float64, page, limit int) ([]Product, error) {
    var products []Product
    offset := (page - 1) * limit

    query := db.Where("category = ? AND price BETWEEN ? AND ?", category, minPrice, maxPrice).
        Order("price DESC").
        Limit(limit).
        Offset(offset)

    if err := query.Find(&products).Error; err != nil {
        return nil, err
    }
    return products, nil
}

ポイント:

  • フィルタリング: カテゴリや価格帯などの条件を指定します。
  • ソート: ユーザーが最も興味を持つ順序で商品を並べます。
  • ページネーション: 一度に表示する商品数を制限します。

応用結果例:
リクエスト:

GET /products?category=electronics&minPrice=100&maxPrice=500&page=2&limit=10

レスポンス:

{
    "data": [
        {"id": 11, "name": "Smartphone X", "price": 450.00},
        {"id": 12, "name": "Bluetooth Speaker", "price": 350.00}
    ],
    "total": 120,
    "page": 2,
    "totalPages": 12
}

応用例2: ソーシャルメディアのフィード


シナリオ:
ソーシャルメディアアプリで、ユーザーの投稿やフォロー中のユーザーの投稿をフィードに表示します。

カーソルベースのページネーション実装例:

func fetchUserFeed(db *gorm.DB, userID uint, lastPostID uint, limit int) ([]Post, error) {
    var posts []Post

    query := db.Where("user_id = ? AND id < ?", userID, lastPostID).
        Order("id DESC").
        Limit(limit)

    if err := query.Find(&posts).Error; err != nil {
        return nil, err
    }
    return posts, nil
}

ポイント:

  • カーソルベースページネーション: lastPostIDを使用して次のデータを効率的に取得します。
  • 逆順取得: 新しい投稿を優先して取得します。

応用結果例:
リクエスト:

GET /feed?userID=123&lastPostID=50&limit=5

レスポンス:

{
    "data": [
        {"id": 49, "content": "Just had a great coffee!", "timestamp": "2024-11-17T09:15:00Z"},
        {"id": 48, "content": "Enjoying the weekend vibes.", "timestamp": "2024-11-16T18:45:00Z"}
    ],
    "nextCursor": 47
}

応用例3: ビジネスアナリティクスダッシュボード


シナリオ:
ダッシュボードで売上データやユーザーアクティビティを分割して表示します。

実装例:

func fetchSalesData(db *gorm.DB, startDate, endDate string, page, limit int) ([]SalesRecord, error) {
    var records []SalesRecord
    offset := (page - 1) * limit

    query := db.Where("date BETWEEN ? AND ?", startDate, endDate).
        Order("date DESC").
        Limit(limit).
        Offset(offset)

    if err := query.Find(&records).Error; err != nil {
        return nil, err
    }
    return records, nil
}

ポイント:

  • 期間指定: 売上データの範囲を日付で指定します。
  • 統計データの表示: ページネーションでデータを分割しながら統計を効率的に表示します。

応用結果の活用方法

  • フィードのスクロール機能や「さらに読み込む」ボタンの実装。
  • ダッシュボードでの統計表示の効率化。
  • データベース負荷の軽減とユーザー体験の向上。

次章では、本記事の内容を総括し、Goでページネーションを実装する際の重要なポイントを振り返ります。

まとめ


本記事では、Go言語を用いたページネーションの基本から応用までを解説しました。ページネーションは、大量のデータを効率的に分割・提供し、ユーザー体験とシステムパフォーマンスを向上させる重要な技術です。

以下が要点です:

  • ページネーションの基本概念とGo言語での基礎的な実装。
  • SQLを活用したデータベースとの統合方法とその効率化。
  • GORMやGinなどの便利なライブラリを活用した簡潔な実装。
  • 大規模アプリケーションにおける応用例として、eコマース、ソーシャルメディア、ダッシュボードでの利用を紹介。
  • OFFSETのパフォーマンス問題やデータの一貫性の課題に対する解決策。

適切なページネーションを設計・実装することで、Goを使ったアプリケーション開発がさらに効果的になります。本記事を参考に、実際のプロジェクトで役立つページネーション機能を構築してください。

コメント

コメントする

目次