Techtouch Developers Blog

テックタッチ株式会社の開発チームによるテックブログです

GORM v2 触ってみた Major Features 編

adventCalendar-day3

この記事はテックタッチアドベントカレンダー3日目の記事です。2日目は国定による「TypeScript 4.1 に更新しました」でした。

SREチームの taisa です。「愛の不時着」をきっかけに韓国ドラマにハマっています。最近は「スタートアップ」と「梨泰院クラス」をみています。Netflix な毎日です。

今回は「GORM v2 Major Features 編」ということで、今年8月にリリースされた GORM v2 の新機能をリリースノートに従って簡単に動かしてみたので紹介します。テックタッチのバックエンドは Go で実装していて ORM には GORM v1 を利用しています。v2 の Major Features と Breaking Changes をチェックして v2 へのアップデートも検討したいところです。

公式ドキュメント

GORM は Golang の ORM ライブラリです。公式ドキュメントは、メインが v2 になり v1 のドキュメントはサブドメインになりました。

gorm.io

v1.gorm.io

GORM v2 リリースノート

GORM v2 のリリースノートはこちらです。これを見れば大体想像できる人も多いと思いますが、本記事は実際に動かして出力されるSQLを確認することを一番の目的としています。

gorm.io

v1 のファイル構成

Major Features を確認する前に v1 と v2 のファイル構成を比べてみました。確認した v1 のタグバージョンは v1.9.16 です。_test.go とテスト系ファイル除くファイル数は31ファイルでした。

.
├── association.go
├── callback.go
├── callback_create.go
├── callback_delete.go
├── callback_query.go
├── callback_query_preload.go
├── callback_row_query.go
├── callback_save.go
├── callback_update.go
├── dialect.go
├── dialect_common.go
├── dialect_mysql.go
├── dialect_postgres.go
├── dialect_sqlite3.go
├── dialects
│   ├── mssql
│   │   └── mssql.go
│   ├── mysql
│   │   └── mysql.go
│   ├── postgres
│   │   └── postgres.go
│   └── sqlite
│       └── sqlite.go
├── errors.go
├── field.go
├── interface.go
├── join_table_handler.go
├── logger.go
├── main.go
├── model.go
├── model_struct.go
├── naming.go
├── scope.go
├── search.go
└── utils.go

v2 のファイル構成

v2 で確認したタグバージョンは v1.20.5 です。v1 と比べてパッケージが多く切られているのがわかります。_test.go とテスト系ファイル除いたファイル数も56ファイルと増えました。スクラッチで作り直したようで、ファイル一覧を見ただけでも大分整理された印象があります。

.
├── association.go
├── callbacks
│   ├── associations.go
│   ├── callbacks.go
│   ├── callmethod.go
│   ├── create.go
│   ├── delete.go
│   ├── helper.go
│   ├── interfaces.go
│   ├── preload.go
│   ├── query.go
│   ├── raw.go
│   ├── row.go
│   ├── transaction.go
│   └── update.go
├── callbacks.go
├── chainable_api.go
├── clause
│   ├── clause.go
│   ├── delete.go
│   ├── expression.go
│   ├── from.go
│   ├── group_by.go
│   ├── insert.go
│   ├── joins.go
│   ├── limit.go
│   ├── locking.go
│   ├── on_conflict.go
│   ├── order_by.go
│   ├── returning.go
│   ├── select.go
│   ├── set.go
│   ├── update.go
│   ├── values.go
│   ├── where.go
│   └── with.go
├── errors.go
├── finisher_api.go
├── gorm.go
├── interfaces.go
├── logger
│   ├── logger.go
│   ├── sql.go
├── migrator
│   └── migrator.go
├── migrator.go
├── model.go
├── prepare_stmt.go
├── scan.go
├── schema
│   ├── check.go
│   ├── field.go
│   ├── index.go
│   ├── interfaces.go
│   ├── naming.go
│   ├── relationship.go
│   ├── schema.go
│   ├── utils.go
├── soft_delete.go
├── statement.go
└── utils
    └── utils.go

Goland の設定

v1 ではインポートパスが github.com/jinzhu/gorm でしたが、v2 では gorm.io/gorm に変わりました。それに伴い Goland の設定を変更する必要がありました。Preferences > Go > Go Modules を選択し「Enable Go Modules integration」にチェックが入っていればOKです。

赤くなってしまう。

goland

Preferences > Go > Go Modules を選択し「Enable Go Modules integration」にチェック

goland

動作確認用のモデル定義

動作確認する為に以下のモデルを定義しました。今回 SQLite と MySQL で動作確認をしましたが、本記事に記載している SQLは MySQL のものです。

type Company struct {
    ID        int       `gorm:"primaryKey"`
    Name      string
    // 新機能の動作確認用カラム
    // Track creating/updating time/unix (milli/nano) seconds for multiple fields
    CreatedAt time.Time // Set to current time if it is zero on creating
    UpdatedAt int       // Set to current unix seconds on updating or if it is zero on creating
    Updated   int64     `gorm:"autoUpdateTime:nano"`  // Use unix Nano seconds as updating time
    Updated2  int64     `gorm:"autoUpdateTime:milli"` // Use unix Milli seconds as updating time
    Created   int64     `gorm:"autoCreateTime"`       // Use unix seconds as creating time
}

type User struct {
    ID         int `gorm:"primaryKey"`
    CompanyID  int
    Company    Company
    Name       string
    Address    string
    Age        int
    CreditCard CreditCard
    // 新機能の動作確認用カラム
    Attributes datatypes.JSON
}

type CreditCard struct {
    ID     int `gorm:"primaryKey"`
    UserID int
    Number string
}

Major Features

前置きが長くなりましたが、Major Features を確認していきます。 数が多いのでさらっといきます。

Context Support

WithContextでコンテキストが渡せるようになりました。

var user model.User
db.WithContext(ctx).Find(&user)
// SELECT * FROM `users`

Batch Insert

バッチインサートが可能になりました。v1 だと別のライブラリを使うか SQL を書くしかなかったのでうれしいアップデートです。これは v1 にも入れられないのか?と思ったりします。

var users = []model.User{
    {Name: "test", Address: "address", Age: 20, CompanyID: 1},
    {Name: "test2", Address: "address2", Age: 30, CompanyID: 1},
    {Name: "test3", Address: "address3", Age: 40, CompanyID: 1},
}
db.Create(&users)
//  INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES (1,'test','address',20),(1,'test2','address2',30),(1,'test3','address3',40)

Prepared Statement Mode

Prepared Statement モードが使えるようになりました。DB Open 時のコンフィグでも ON にできます。

var user model.User
tx := db.Session(&gorm.Session{
    PrepareStmt: true,
})
tx.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

DryRun Mode

DryRun モードで SQL が確認可能になりました。SQL ログにも出力されますがstmt.SQL.String()で出力することも可能です。

var user model.User
stmt := db.Session(&gorm.Session{DryRun: true}).Find(&user, 1).Statement
fmt.Println(stmt.SQL.String())
// SELECT * FROM `users` WHERE `users`.`id` = ?

Join Preload

db.Joins("Company").Findのように書くことで Join Preload が可能になりました。

Preload associations using INNER JOIN, and will handle null data to avoid failing to scan

リリースノートをみるとINNER JOINと書いてありますが、書き方が悪いのか出力された SQL は LEFT JOINになっています。(詳細追えられてません)

var users []model.User
db.Joins("Company").Find(&users, "users.id IN ?", []int{1, 2})
b, _ := json.Marshal(&users)
fmt.Println(string(b))
// SELECT `users`.`id`,`users`.`company_id`,`users`.`name`,`users`.`address`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` `Company` ON `users`.`company_id` = `Company`.`id` WHERE users.id IN (1,2)

ちなみに json で出力すると下記となります。

[
    {
        "ID": 1,
        "CompanyID": 1,
        "Company": {
            "ID": 1,
            "Name": "company name"
        },
        "Name": "test",
        "Address": "address",
        "Age": 20
    },
    {
        "ID": 2,
        "CompanyID": 1,
        "Company": {
            "ID": 1,
            "Name": "company name"
        },
        "Name": "test2",
        "Address": "address2",
        "Age": 30
    }
]

Find To Map ※ 実行できず

結果を Map に入れられるとのことでしたが、これについては SQLite と MySQL 両方ともエラーが発生して実行できませんでした。

var result map[string]interface{}
db.Model(&model.User{}).First(&result, "id = ?", 1)

エラー内容

panic: assignment to entry in nil map

goroutine 1 [running]:
gorm.io/gorm.scanIntoMap(0x0, 0xc000090900, 0x6, 0x6, 0xc0000908a0, 0x6, 0x6)
        /Users/tt-dev/go/pkg/mod/gorm.io/gorm@v1.20.5/scan.go:40 +0x285
gorm.io/gorm.Scan(0xc0000ae300, 0xc0000b0b40, 0xc00001a000)
        /Users/tt-dev/go/pkg/mod/gorm.io/gorm@v1.20.5/scan.go:72 +0x445
gorm.io/gorm/callbacks.Query(0xc0000b0b40)
        /Users/tt-dev/go/pkg/mod/gorm.io/gorm@v1.20.5/callbacks/query.go:34 +0x4ae
gorm.io/gorm.(*processor).Execute(0xc0001aaa80, 0xc0000b0b40)
        /Users/tt-dev/go/pkg/mod/gorm.io/gorm@v1.20.5/callbacks.go:105 +0x5d4
gorm.io/gorm.(*DB).First(0xc0000b0b40, 0x45c6c60, 0xc0000b81d8, 0xc0000928c0, 0x2, 0x2, 0xc0000b0b40)
        /Users/tt-dev/go/pkg/mod/gorm.io/gorm@v1.20.5/finisher_api.go:75 +0x358
main.FindToMap()
        /Users/tt-dev/go/src/github.com/taisa831/sandbox-gorm2/main.go:138 +0x1cb
main.main()
        /Users/tt-dev/go/src/github.com/taisa831/sandbox-gorm2/main.go:51 +0x25e
Exiting.

Create From Map

Map からインサートが可能になりました。

db.Model(&model.User{}).Create(map[string]interface{}{"Name": "test", "Address": "address", "Age": 50, "CompanyID": 1})
// INSERT INTO `users` (`address`,`age`,`company_id`,`name`) VALUES ('address',50,1,'test')

datas := []map[string]interface{}{
    {"Name": "test", "Address": "address1", "Age": 60, "CompanyID": 1},
    {"name": "test", "Address": "address2", "Age": 70, "CompanyID": 1},
}
db.Model(&model.User{}).Create(datas)
// INSERT INTO `users` (`address`,`age`,`company_id`,`name`) VALUES ('address1',60,1,'test'),('address2',70,1,'test')

FindInBatches

FindInBatchesで逐次処理が可能になりました。件数が膨大なデータを処理するのに便利。

var users []model.User
err := db.Where("id >= ?", 1).FindInBatches(&users, 2, func(tx *gorm.DB, batch int) error {
    for _, user := range users {
        fmt.Println(user.ID)
    }
    return nil
})
if err.Error != nil {
    fmt.Println(err.Error.Error())
}
// SELECT * FROM `users` WHERE id >= 1 LIMIT 2
// SELECT * FROM `users` WHERE id >= 1 LIMIT 2 OFFSET 2
// ・・・

Nested Transaction

トランザクションがネスト可能になりました。サンプルでは、user1 作成後 user2 の作成時に失敗しますが、user2 だけロールバックされ、次の処理に進み、user3 が作成されて処理が終わります。

db.Transaction(func(tx *gorm.DB) error {
    user1 := model.User{
        Name: "name",
        Address: "address",
    }
    tx.Create(&user1)
    // INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES (1,'name','address',10)

    // SAVEPOINT sp0x4493200
    tx.Transaction(func(tx2 *gorm.DB) error {
        user2 := model.User{
            Name: "name",
            Address: "address",
        }
        tx.Create(&user2)
        // INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES (1,'name','address',10)
        return errors.New("rollback user2") // rollback user2
    })

    // ROLLBACK TO SAVEPOINT sp0x4493200
    // SAVEPOINT sp0x4493340
    tx.Transaction(func(tx2 *gorm.DB) error {
        user3 := model.User{
            Name: "name",
            Address: "address",
        }
        // INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES (1,'name','address',10)
        tx.Create(&user3)
        return nil
    })
    return nil // commit user1 and user3
})

SavePoint, RollbackTo

トランザクション中でSavePointを置くことで途中で処理が失敗しても特定のSavePointまでロールバックできます。サンプルでは、user1 と user2 を作成後ロールバックしていますが、user1 作成後のSavePointまでロールバックされているので user1 は作成されます。

tx := db.Begin()

user1 := model.User{
    Name: "name1",
    Address: "address1",
}
tx.Create(&user1)
//  INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES (1,'name1','address1',10)

// SAVEPOINT sp1
tx.SavePoint("sp1")

user2 := model.User{
    Name: "name2",
    Address: "address2",
}
tx.Create(&user2)
// INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES (1,'name2','address2',10)

// ROLLBACK TO SAVEPOINT sp1
tx.RollbackTo("sp1") // rollback user2

tx.Commit()

Named Argument

Where句の値に名前をつけることが可能になりました。同じ値を沢山セットする必要があるときに使えそう。

var user model.User
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "test")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "test" OR name2 = "test"

var user2 model.User
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "test"}).First(&user2)
// SELECT * FROM `users` WHERE name1 = "test" OR name2 = "test" ORDER BY `users`.`id` LIMIT 1

db.Raw(
    "SELECT * FROM users WHERE name1 = @name OR name2 = @name2 OR name3 = @name",
    sql.Named("name", "test"), sql.Named("name2", "test"),
).Find(&model.User{})
// SELECT * FROM users WHERE name1 = "test" OR name2 = "test" OR name3 = "test"

db.Exec(
    "UPDATE users SET name1 = @name, name2 = @name2, name3 = @name",
    map[string]interface{}{"name": "test", "name2": "test"},
// UPDATE users SET name1 = "test", name2 = "test", name3 = "test"

Group Conditions

db.WhereをネストしたりOrを使って複雑な Where句の組み立てが可能になりました。

var user model.User
db.Where(
    db.Where("name = ?", "test").Where(db.Where("address = ?", "address").Or("company = ?", 1)),
).Or(
    db.Where("name = ?", "test2").Where("address = ?", "address"),
).Find(&user)
// SELECT * FROM `users` WHERE (name = 'test' AND (address = 'address' OR company = 1)) OR (name = 'test2' AND address = 'address')

SubQuery

様々なタイプのサブクエリが書けるようになりました。

// Where SubQuery
var user model.User
db.Where("age > (?)", db.Table("users").Select("AVG(age)")).Find(&user)
// SELECT * FROM `users` WHERE age > (SELECT AVG(age) FROM `users`)

// From SubQuery
db.Table("(?) as u", db.Model(&model.User{}).Select("name", "age")).Where("age = ?", 18).Find(&user)
// SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE age = 20

// Update SubQuery
db.Model(&model.User{}).Update(
    "age", db.Model(&model.Company{}).Select("age").Where("companies.id = users.company_id"),
)
// UPDATE `users` SET `name`=(SELECT `name` FROM `companies` WHERE companies.id = users.company_id)

Upsert

Upsert が可能になりました。便利。

var users []model.User
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&users)
// INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES  ON DUPLICATE KEY UPDATE `id`=`id`

db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "id"}},
    DoUpdates: clause.Assignments(map[string]interface{}{"name": "test", "address": "address", "age": 20, "company_id": 1}),
}).Create(&users)
// INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES  ON DUPLICATE KEY UPDATE `address`='address',`age`=20,`company_id`=1,`name`='test'

db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "id"}},
    DoUpdates: clause.AssignmentColumns([]string{"name", "address", "age", "company_id"}),
}).Create(&users)
// INSERT INTO `users` (`company_id`,`name`,`address`,`age`) VALUES  ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`address`=VALUES(`address`),`age`=VALUES(`age`),`company_id`=VALUES(`company_id`)

Locking

FOR UPDATEが実行可能になりました。

db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&model.User{})
// SELECT * FROM `users` FOR UPDATE

db.Clauses(clause.Locking{
    Strength: "SHARE",
    Table: clause.Table{Name: clause.CurrentTable},
}).Find(&model.User{})
// SELECT * FROM `users` FOR SHARE OF `users`

Optimizer/Index/Comment Hints

様々な Hints が書けるようになりました。(自分は使ったことがありません。)

import "gorm.io/hints"

// Optimizer Hints
db.Clauses(hints.New("hint")).Find(&model.User{})
// SELECT /*+ hint */ * FROM `users`

// Index Hints
db.Clauses(hints.UseIndex("idx_user_name")).Find(&model.User{})
// SELECT * FROM `users` USE INDEX (`idx_user_name`)

// Comment Hints
db.Clauses(hints.Comment("select", "master")).Find(&model.User{})
// SELECT /* master */ * FROM `users`

Field permissions

カラム別に読み取り専用、作成専用などの指定が可能になりました。すごいけど使いどころが難しそう。

type User struct {
  Name string `gorm:"<-:create"` // allow read and create
  Name string `gorm:"<-:update"` // allow read and update
  Name string `gorm:"<-"`        // allow read and write (create and update)
  Name string `gorm:"->:false;<-:create"` // createonly
  Name string `gorm:"->"` // readonly
  Name string `gorm:"-"`  // ignored
}

Track creating/updating time/unix (milli/nano) seconds for multiple fields

日付で unix time が使えるようになりました。

type Company struct {
    ID        int       `gorm:"primaryKey"`
    Name      string    `gorm:""`
    CreatedAt time.Time // Set to current time if it is zero on creating
    UpdatedAt int       // Set to current unix seconds on updating or if it is zero on creating
    Updated   int64     `gorm:"autoUpdateTime:nano"`  // Use unix Nano seconds as updating time
    Updated2  int64     `gorm:"autoUpdateTime:milli"` // Use unix Milli seconds as updating time
    Created   int64     `gorm:"autoCreateTime"`       // Use unix seconds as creating time
}

実際にインサートしたときの値はこちらです。

        id: 2
      name: company name
created_at: 2020-12-02 20:58:28.746
updated_at: 1606910308
   updated: 1606910308746117000
  updated2: 1606910308746
   created: 1606910308

Naming Strategy

テーブル名やカラム名などの名前に命名規則が指定可能になりました。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
    NamingStrategy: schema.NamingStrategy{TablePrefix: "t_", SingularTable: true},
    Logger: newLogger,
})

下記は SQLite で実行してみた結果です。

sqlite> .tables
t_company      t_credit_card  t_user

Logger

ロガーの更新内容です。サイトの内容をそのまま引用しました。

  • Context support
  • Customize/turn off the colors in the log
  • Slow SQL log, default slow SQL time is 100ms
  • Optimized the SQL log format so that it can be copied and executed in a database console

https://gorm.io/docs/v2_release_note.html#Logger

Transaction Mode

デフォルトで GORM 書き込みはトランザクション内で実行されるようです。初期化時にトランザクションモードを無効にしておくことで、トランザクションが必要なときだけトランザクションを利用することになるので高速化がはかれます。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

DataTypes (JSON as example)

データタイプに json が使えるようになりました(SQLite, MySQL, Postgres)。json の検索もできて便利。

import "gorm.io/datatypes"

db.Create(&model.User{
    Name:       "user",
    CompanyID:  1,
    Address:    "address",
    Age:        30,
    Attributes: datatypes.JSON([]byte(`{"name": "user", "age": 20, "tags": ["tag1", "tag2"], "orgs": {"orga": "orga"}}`)),
})
// INSERT INTO `users` (`company_id`,`name`,`address`,`age`,`attributes`) VALUES (1,'user','address',30,'{"name": "user", "age": 20, "tags": ["tag1", "tag2"], "orgs": {"orga": "orga"}}')

var user model.User
db.First(&user, datatypes.JSONQuery("attributes").HasKey("name"))
//  SELECT * FROM `users` WHERE JSON_EXTRACT(`attributes`, '$.name') IS NOT NULL ORDER BY `users`.`id` LIMIT 1

db.First(&user, datatypes.JSONQuery("attributes").HasKey("orgs", "orga"))
// SELECT * FROM `users` WHERE JSON_EXTRACT(`attributes`, '$.orgs.orga') IS NOT NULL AND `users`.`id` = 27 ORDER BY `users`.`id` LIMIT 1

Smart Select

structで select するカラムが指定可能になりました。

type APIUser struct {
    ID   int
    Name string
}

db.Model(&model.User{}).Limit(10).Find(&APIUser{})
// SELECT `id`,`name` FROM `users` LIMIT 10

Associations Batch Mode

Association モードで一括処理が可能になりました。

userA := model.User{ID: 1}
userB := model.User{ID: 3}
users := []model.User{userA, userB}
var creditCard model.CreditCard

db.Model(&users).Association("CreditCard").Find(&creditCard)
// SELECT * FROM `credit_cards` WHERE `credit_cards`.`user_id` IN (1,3)

db.Model(&users).Association("CreditCard").Delete(&userA)
// UPDATE `credit_cards` SET `user_id`=NULL WHERE `credit_cards`.`user_id` IN (1,3) AND `credit_cards`.`id` = 1

db.Model(&users).Association("CreditCard").Count()
// SELECT count(1) FROM `credit_cards` WHERE `credit_cards`.`user_id` IN (1,3)

今回検証できませんでしたが、下記のような処理も可能になったようです。

// For `Append`, `Replace` with batch data, argument's length need to equal to data's length or will returns error
var users = []User{user1, user2, user3}
// e.g: we have 3 users, Append userA to user1's team, append userB to user2's team, append userA, userB and userC to user3's team
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})
// Reset user1's team to userA,reset user2's team to userB, reset user3's team to userA, userB and userC
db.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})

Delete Associations when deleting

削除時に関連するレコードを削除してから対象レコードが削除可能になりました。サンプルでは user の credit card レコードを削除してから user レコードを削除しています。cascade でない場合、関連情報削除して回るのは面倒なのでかなり使えそう。

// delete user's credit card when deleting user
user := model.User{
    ID: 3,
}
db.Select("CreditCard").Delete(&user)
// DELETE FROM `credit_cards` WHERE `credit_cards`.`user_id` = 3
// DELETE FROM `users` WHERE `users`.`id` = 3

今回動作確認できなかった新機能

以下の新機能は動かしてみるところまでいけなかったので、またのタイミングに個別に取り上げたいと思います。

CRUD From SQL Expr/Context Valuer

gorm.io

Multiple Databases, Read/Write Splitting

gorm.io

Prometheus

gorm.io

まとめ

長くなりましたが、GORM v2 の Major Features の動作確認紹介でした。本記事で作成したサンプルは GitHub にあげているので簡単に動かしてみたい方はみてみてください。ちなみに全く同じ実装を v1 で動かそうとしましたが、一つも動きませんでした。

自分は ORM の機能を使わず SQL を書いてしまいたくなることもあるのですが、ORM の使い方に慣れてしまえばメリットも大きいので、詳しくなっていきたいところです。また、折をみてコードリーディングをして内部理解もしたいなと思っています。

github.com

明日は terunuma による「プロダクト開発と事業へ成果を出せる SET になるために読んだ書籍・スライド」です!よろしく!