サカナ未遂

プログラミング、筋トレ、子育て

スターティングGo言語 4章を読む

前回の続きを読んでいく

blog.tera-chan3700.com

Chapter 4 参照型

参照型はslice、map、channelの3つが定義されていて、make関数を仕様して生成する。

スライス

可変長配列を表現する型 以下の場合、要素数が10のint型のスライスが生成される

s := make([]int, 10)

// 代入はインデックスを指定
s[0] = 100

// 要素数を超えた場合はランタイムパニックが起こる
s[10] = 101  // これはエラー

// 現在の要素数を調べるにはlenを使う
len(s)

rubyだとsはオブジェクトになり、s.lengthと書けるけどGoだとlenが関数と定義されてるので、関数に渡す必要がある。

cap

capは容量

s := make([]int, 10, 15)
fmt.Println(cap(s)) // => 15

容量は予め確保する容量。上記の場合、インデックスで参照できるのはs[9]までだけど、要素数を拡張するときに、capの範囲内なら新たにメモリを拡張する必要がない。 容量が10の場合、容量を拡張すると、Goのランタイムはもとの容量の10より大きいメモリ領域を確保して、拡張前のデータを丸ごと確保した領域へコピーする。この動作は処理コストが高いため、予めわかってる範囲で容量を取っておくとパフォーマンスの劣化を防ぐことができる。

スライスは配列型と同じようなリテラルで生成できる

// スライス
s := []int{1, 2, 3, 4, 5}

// こっちは配列
a := [5]int{1, 2, 3, 4, 5}

// 配列からスライスを生成
s2 := a[0:2]

配列との書き方も似ているし違いがわかりにくい。 スライスは可変長なので上の配列でいう5を指定しない場合はスライスになる。

配列とスライスの違いは「拡張性」である。 Goの配列は要素数を含めて1つの型を表すので、[10]intと[11]intでは別の型として扱われる。

スライスの要素を拡張するにはappned関数を仕様する。appendを使用する場合は:=か=による代入を行う必要がある。変数の代入がないとコンパイルエラーになる。

s := []int{1, 2, 3, 4, 5}
s = append(s, 6) // 代入しないとエラーになる

appendで要素を追加するときcapを超えるとスライスが拡張されるが、どの程度確証されるかはGoのランタイムに依存する。

copy

スライスにスライスの値を一括でコピーするcopy関数 以下のように記述する

// s1とs2はスライス
n := copy(s1, s2)

s1の中身をs2で塗りつぶす。nにはコピーが成功した要素数が変える。 nにスライスが返りそうに見えるが、s1に対して破壊的変更が発生している。

簡易スライスと完全スライス式

簡易スライス式と完全スライス式の違い

a := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// 簡易スライス式   
// この場合、aのインデックス2〜4の値をとって、容量は2から最後まで
s1 := a[2:4] // 10 - 2 でcapが8になる。

// 完全スライス式
s2 := a[2:4:4] // aのインデックス2 〜 4までが容量になるので capが2になる。
s3 := a[2:4:6] // aのインデックス2 〜 6までが容量になるので capが4になる

スライスのfor

スライスの全要素をループするのはforとrangeを使う

s := []string{"alice", "bob", "carol"}

for i, v := range s {
    fmt.Printf("%d回目、%sです\n", i, v)
}

スライスと可変長引数

関数の引数に、以下のようにs ...intと定義することで、パラメータが[]int型のスライスにまとめることができる。

func sum(s ...int) int {
    ...
    return n
}

この関数sumに引数を渡さない場合は0が渡される。 可変長引数の定義は引数の末尾に1つだけ定義できるので、可変長引数のあとに別の引数を定義することはできない

スライスは参照型

配列を関数に渡すときは値渡しになるが、スライスを渡すときは参照渡しになる。 配列からスライスを生成した場合、配列の値を書き換えるとスライスの値も変わる(参照元が変わるため)

ただ、配列からスライスを生成した場合でもappendを使って拡張した場合、スライスは新たにメモリを確保するため、もとの配列からの参照は外れる。

マップ

mapはGoでの連想配列に類するデータ構造、キーと値をペアで保存できる配列。

m := make(map[int]string)

m["1"] = "US"
m["81"] = "Japan"
m["86"] = "China"

// マップをリテラルでの書き方
m2 := map[int]string{1: "US", 81: "Japan", 86: "China"}

キーが重複すると要素の値は上書きされる。 mapの参照で、存在しないキーを指定して参照した場合、型の初期値が表示される。 mapの値の型がintの場合、0となる。

mapのキーを指定したとき、2つ目の戻り値にbool型が代入される。 キーがあればtrue、なければfalseが入る。

s, ok := m[10]

// ifと絡めて書くことがおおい

if _, ok := m[10]; ok {
    //キーがある場合の処理を書く
}

マップの要素型がスライスなどの参照型である場合は初期値がnilになる。

マップのfor

mapもスライスと同様、forとrangeの組み合わせで全ての要素を参照できる。

for k, v := range m {
}

rangeの戻り値にkとvを用意する。 キーの値は不定であることに注意する

len

len関数で、要素数を取得できる

delete

deleteを使うことでマップから要素を取り除くことができる。

delete(m, 2)

該当しないキーを設定した場合は何も処理されない。

チャネル

ゴルーチンとゴルーチンの間でデータの受け渡しを行うデータ構造。 チャネルの型は「chan [データ型]」と書く。

// 双方向チャネル
var ch chan int

// int型の受信専用チャネル
var ch1 <-chan int

// int型の送信専用チャネル
var ch2 chan<- int

受信と送信がわかりにくい、、、<を手と思うようにし、chanが手の中にはいってるので<-chanが受信 chanから手が出てるのが送信chan<- と考えればよいか。

チャネルの生成

チャネルもmakeを使って生成する。

// バッファサイズ0
ch := make(chan int)

// バッファサイズ10
ch := make(chan int, 10)

チャネルはキューの性質を備えるデータ構造であり、バッファはキューを格納する領域。

ch := make(chan int, 2)
ch <- 5
ch <- 6
// ch <- 7    サイズが2なのでこのコメントアウトを外すとエラーになる。


i := <- ch // 5が入る
i := <- ch // 6が入る

チャネルはゴルーチン間でデータを共有するもの。他のゴルーチンがチャネルへデータを送信するのを待ってる。

func main() {
    ch := make(chan int)

    // ch <- 10  ゴルーチンが動く前にい送信すると、受信するゴルーチンがないためエラーになる
    go reciver(ch) //チャネルを受信するゴルーチンを動かす

    i := 0
    for i < 10000 {
        ch <- i  // ゴルーチンが動いたあとはエラーにならない
        i++
    }
}

close

チャネルを生成するとオープンになり、close関数を使うことで閉じることができる。 クローズされたチャネルにデータ送信を行うとランタイムパニックが発生する。 クローズされたチャネルから受信をする場合は、チャネルのバッファ内は問題なく受信でき、バッファが空になったら型の初期値を返す。 クローズされたかどうかは第2の戻り値のbool型を判定する。

i, ok := <-ch

select

selectは複数のチャネルを効率的に処理するための構文

select {
case e1 := <-ch1
    // ch1が受信したときの処理
case e2 := <-ch2
    // ch2が受信したときの処理
default:
    }

各チャネルが受信したタイミングで処理が走る。 引き続き5章を読んでいく。

Go言語を基礎を学ぶ。スターティングGo言語 3章まで読む

Go言語の勉強で書籍を探していて「スターティングGo言語」が良さそうだったので買ってみた。

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

  • 作者:松尾 愛賀
  • 発売日: 2016/04/15
  • メディア: 単行本(ソフトカバー)

少々前の書籍なので環境構築は、最新の情報を参照した。 Chapter2以降で学んだことを書いていく

Chapter2 プログラムの構成と実行

とりあえずいつものコードを書く hello.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Goはコンパイル言語であるが、ビルドプロセスを隠蔽してプログラムを直接事項できるrunコマンドがある

$ go run hello.go
Hello, World!

エントリーポイントはmainパッケージのmain関数になる。 ビルドはbuildコマンドをつかう。ビルドすると実行ファイルが生成される.

$ go build -o hello
$ ./hello
Hello, World!

ソースコードよりビルドされたファイルのほうがサイズが大きくなる。 これはOSの標準ライブラリに依存しないため、パッケージの機能すべてを実行ファイルの中に組み込むためである。

パッケージ

1つのパッケージを複数のファイルに分けて定義することができる。一つにまとめても問題ない。

本ではanimalsパッケージを作成し、mainで読み込んで実行とあるが動かなかった。

package main

import (
    "fmt"

    "./animals"
)

func main() {
.
.

ググったところ、go.modで初期化した場所がルートになるのでそこからのパスを設定しないと動かないっぽい

go mod init terachan.com/sample
package main

import (
    "fmt"

    "terachan.com/sample/animals" // ← こんな感じにする
)

こうすることで動いた。

パッケージは1つのディレクトリには1つのパッケージしか定義できない 同じパッケージの関数(同一フォルダ)は、main.goでimportしなくても使用できる。

テストは標準パッケージでテスト機能testingがついてる。 XXXX_test.goのファイルをつくってテストコードを書く。

package hogehgoe

import "testing"

func TestTargetApp(t *testing.T) {
    expect := "success"
    actual := TargetApp()

    if expect != actual {
        t.Errorf("%s != %s", expect, actual)
    }
}

テスト実行

$ go test -v ./hogehoge/
=== RUN   TestElephantFeed
--- PASS: TestElephantFeed (0.00s)
PASS
ok      terachan.com/sample/hogehoge     0.063s

Chapter3 言語の基本

fmt.Printfでデバッグをするときは%v、%#v、%Tが便利。さまざまな型を見やすい形式で埋め込んでくれる。

  • %v:さまざまな型のデータを埋め込む
  • %#v:Goのリテラル表現でデータを埋め込む
  • %T:データの型情報を埋め込む

明示的に書けるが、:=を使うと代入時に型推論してくれる。

// 明示的に宣言
var n int
n = 10

// 型推論
i := 10

// 型推論。これもいける。複数を定義するときはこれのほうが読みやすい
var {
    n = 1
    s = "hogehoge"
    b = false
}

変数

ローカル変数とパッケージ変数に分かれる。 main関数の外側で定義すればパッケージ変数となり、どの関数からでも呼び出せる。

参照されてない変数がある場合はコンパイルエラーになる。 これは地味に便利で、何に使ってるかわからない変数問題がなくなる。 float32は原則使用しないほうがよい。64のほうが精度が高い。メモリの使用効率野天では32のほうが省メモリではあるが。

配列

配列の定義

a := [5]int{1, 2, 3, 4, 5}

要素のサイズが5のint型の配列の定義となる。 値が4つしかない場合は、5つめの要素は0になる。 要素数を超過した場合はエラーになる。 配列はGoで定義されたすべての型で配列型を定義できる。

素数の省略

a := [...]int{1, 2, 3, 4, 5}

[...]を使うことで初期値の数でサイズが決まる。

配列の要素の型が同じでも、要素数が異なると変数同士の代入はエラーになる

var {
    hoge1 [5]int
    hoge2 [6]int
}
// これはエラーになる
hoge1 = hoge2

素数が同じでも型が異なればもちろんエラーになる。

interface{}

interface{}型は{}も含めて型の名前。 Goのあらゆる型と互換性のある特殊な型

var x interface{}
// なんでも入る
x = "hogehgoe"

関数

Goはオブジェクト指向機能を持っていないので「関数の定義」と「構造体の定義」をすることがプログラミングにおける中心的な作業になる。 関数に戻り値がないvoid型はない(CやJavatとは違う) returnで複数の値を返すことができる。(Rubyみたい) 関数の戻り値を受け取るときに_をすると戻り値を破棄することができる

q, _ := myMethod()

// 2つとも破棄はできない
_, _ := myMethod()

エラー処理の書き方は以下のように書き、よく出てくる表現

result, err := myMethod()
if (err != nil) {
    // エラー処理
    ...
}

一見引数を返さないように見える関数

func myMethod() (a int) {
    a = 10
    return
}

この場合、関数名のところに(a int)とあるが、これが戻り地として暗黙的に定義されている。 明示的にreturn aとしなくてもreturnと書くだけでaの値を返してくれるので、この場合、myMethodを呼び出すと10が帰ってくる。

関数の引数の無視

func ignoreArgs(_, _ int) int {
    return 1
}

引数を無視することに意味はなさそうだけど、特定のインターフェースに属する場合、定義上引数が必要になるが実装上不要な場合がある。 そのようなときに引数を無視する実装にする必要がある。

無名関数

関数を値として表現したもの。関数を関数の引数にすることも、戻り地にすることもできる。

f := func(x, y int) int { return x + y }

この場合、int型の引数を2つもらってint型の値を返す型になる。

クロージャ

クロージャは関数と関数の処理に関係する「関数外」の環境をセットにして閉じ込めたもの。 自分が作られた環境の外の変数への参照を保持した関数である。 クロージャを生成し直すと関数外の変数が新しく生成される。

スコープ

1文字目が大文字であれば他のパッケージから参照可能 publicとprivateみたいなものか。 別パッケージの小文字で定義されたものを呼び出すとコンパイルエラーとなる。

ファイルのスコープ

1つのパッケージで複数のファイルを定義することができるが、importするパッケージは、別ファイルのものを参照することはできない。

簡易文付きif

if [簡易文]; [条件式] {

}

エラー処理の場合とかに有効。

if _., err := doSometiong(); err != nil
{
    // エラー処理
}

関数を実行して、エラーかどうか判定し、エラーだったらエラー処理を実行する例。

goのインデントについて

fmtを適用するとインデントがすべてタブ文字になる。これによってインデントは2から4かで争う必要がない。 標準でリンターがあるのが嬉しいし悩まなくてすむ。JavascriptRubyはあとからリンターいれるのきつい。

for文

配列に対するループの書き方

member := [3]string{"Alice", "Bob", "Ken"}

for i, s := range member {
    fmt.Printf("member[%d] = %s\n", i, s)
}

range memberの戻り値iにインデックス、sに配列の要素が入り、配列の最後までループする。

switch文

明示的にfallthroughを書かない限り、case節の実行がおわるとswitch文が終了する。 fallthroughをつけると、次の条件も判定するので、defaultがある場合は必ず通る。 case節に式を書くこともできる。

型によるswitch文

interface型に適当な値を入れた場合、switchで.(type)を使うことで型を判定できる。

var str interface{}
str =  1

switch str.(type) {
case string:
    fmt.Println("文字列型")
default:
    fmt.Println("ヒットなし")
}

defer

関数の終了時に実行される式を登録することができる。 deferが複数ある場合、あとから定義されたほうから先に実行される。後入れ先出しなのね。 どういう場合につかうかっていうと、ファイルオープン処理時にdeferでクローズ書いておくなどに使う。

panicとrecover

panicを実行するとランタイムパニックが発生して実行中の関数は中断される。 recoverを使うとパニックで中断したプログラムを回復させることができる。panicはrecoverと組み合わせて使うのが原則

go文

並列処理、go methodnameで使う。 簡単に並列処理を書くことができる。

init

初期化する。 複数かくことができ、その場合ソースコードに出現した順序で実行される。

所感

以前簡単にGoを勉強したがまったく忘れていたので良い復習になった。 引き続きよんでいく。

macでGO言語の開発環境を作る

仕事でGoを使う必要性がでてきたので、Goを勉強することにした。

環境

  • macOS Catalina 10.15.7
  • fish, version 3.1.2
  • goenv 2.0.0beta11

goenvのインストール

$ brew install goenv

環境変数の設定

~/.config/fish/cnfig.fishに以下を追加

set -x GOENV_ROOT $HOME/.goenv
set -x PATH $GOENV_ROOT/bin $PATH
eval (goenv init - | source)
set -x PATH $GOPATH/bin $PATH

GOPATHは設定しない場合、デフォルトで$HOME/goが設定される。

github.com

goenvの確認

$ goenv -v
goenv 2.0.0beta11

goのインストール

$ goenv install -l
Available versions:
  1.2.2
  1.3.0
  .
  .
  .
  1.15.6
  1.15.7

一番新しいのを入れる

$ goenv install 1.15.7

Go Moduels環境に設定する

$ go env -w GO111MODULE=on

go modulesについて github.com goのバージョン管理ツールでRubyでいうGemみたいなものだと理解

wikiにそって動かしてみる。 GOPATHの外側にディレクトリを作成してGitHubと連携

$ mkdir -p /tmp/scratchpad/repo
$ cd /tmp/scratchpad/repo
$ git init -q
$ git remote add origin https://github.com/my/repo

新しいモジュールを初期化する

$ go mod init github.com/my/repo

go: creating new go.mod: module github.com/my/repo

コードを追加する (fishだとwikiどおりのヒアドキュメントが使えないので、bashにして実行)

$ cat <<EOF > hello.go
package main

import (
    "fmt"
    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
}
EOF

ビルドして実行する

$ go build -o hello
$ ./hello

こんにちは世界。

go.modファイルをみるとrsc.io/quote v1.5.2が定義されていることが確認できる

module github.com/my/repo

go 1.15

require rsc.io/quote v1.5.2

GOPATHを確認するとバージョンができてることが確認できる

$ cd ~/go
$ ls -l
total 0
drwxr-xr-x   3 username  staff   96  1 29 23:48 1.15.7/      # これができてる
drwxr-xr-x  18 username  staff  576  6  4  2020 bin/
drwxr-xr-x   4 username  staff  128 10  2  2019 pkg/
drwxr-xr-x   6 username  staff  192  9  6 15:36 src/

これでやっとコードが書ける。

パーフェクトRuby on Rails 増補改訂版 4章、webpackerについて

パーフェクトRuby on Rails 増補改訂版 4章のwebpackerについてまとめ

webpacker

webpackerはwebpackをRailsから扱いやすくするラッパー

app/javascript/packs/application.jsがエントリーファイルとなる。 以下のコマンドでエントリーファイルをもとにビルドを実行

$ bin/rails webpacker:compile

public/packs/js配下にファイルが出力される。 出力したファイルはapp/views/layout/application.html.erbで読み込まれている

config/webpacker.ymlにて環境ごとの設定を行える。

Webpackが提供してないloaderやpluginを追加したい場合や、config/webpacker.ymlで設定できる範囲外のことをしたい場合はconfig/webpack/*.jsを修正する。 例 config/webpack/development.js の場合

process.env.NODE_ENV = process.env.NODE_ENV || 'development' // ①

const environment = require('./environment') // ②

module.exports = environment.toWebpackConfig() // ③

①でdevelopment環境を設定し
②で、config/webpacker.ymlに定義された設定などを読み込んでいる。
③でwebpackの設定用JSONを出力する。
なので、config/webpacker.ymlの範囲外は、③の前に設定すれば、webpack設定用JSONを出力してくれる。

webpack-dev-serverでのビルド

webpack-dev-serverコマンドを使うとwebpack管理下のファイルの更新を検知してすぐにビルドしてくれる。

$ bin/webpack-dev-server

assets:precompile実行時にもwebpackerのビルドが行われる。 RAILS_ENVで環境をproductionしてからassets:precompileを行わないとファイル内のプログラムは縮小されない。

production環境でのpublicディレクト

Railsではproduction環境ではpublicディレクトリ以下のファイルは読み込まない。 nginxなどでpublic以下を配信するのが正しい構成。環境変数RAILS_SERVE_STATIC_FILESに値が何かしら入っていればRails自身で配信する。

SQLアンチパターン4章、5章のまとめ

会社の輪読会でSQLアンチパターンの4章、5章を読んだのでそのまとめ。

SQLアンチパターン

SQLアンチパターン

  • 作者:Bill Karwin
  • 発売日: 2013/01/26
  • メディア: 大型本

4章 キーレスエントリ(外部キー嫌い)

先に結論 - 外部キーつけろ!!!

アンチパターン:外部キーを使用しない

外部キー制約を省略するとDB設計がシンプルになり、柔軟性が高まり、実行速度が早くなると思ってる人もいるかも知れないが、そこには代償があり代償は別の形で支払う必要がある。

外部キーを使用しない場合の大変さ

完璧なコードを前提にする必要がある

  • テーブルの関連付けを常に維持するコードを書くこと
  • 行の挿入時には外部キー列の値が参照先のテーブルの既存の値を参照してることを確認すること
  • 行の削除は関連する子テーブルも適切に更新されること

つまるところ「ミスをしないようにすること」

ミスを調べないといけない

  • 破損したデータを調べるスクリプトをたくさん作成する必要がある。似たようなクエリをすべての参照に対して書かないといけない
  • チェックする頻度も検討しないといけない

すべてのアプリケーションからのアクセスを把握する

  • DBに関するコードが完璧だとしても、直接SQLを実行する場合などは、データが破損する可能性がある。
  • クエリーを変更する場合、DB操作するすべてのアプリとスクリプトの変更を把握しないといけない。

外部キーを書きたくない理由

外部キーを書きたくない理由は、複数のテーブルの関連し合う列を更新するときに外部キー制約が邪魔になるから(キャッチ =22)。

アンチパターンを利用していい場合

外部キーをサポートしてないDBを使う場合

4章まとめ

外部キーつかえ!! (最初に書いたけど)

5章 EAV

(エンティティ・アトリビュート・バリュー)

可変項目のサポート

オブジェクト指向をもとにしたテーブル設計をする場合に、基底クラスでサブクラスでテーブルを分けるとする。

その場合、サブクラスに対応するテーブル毎に異なる項目を設定する必要がある。 そんなとき、汎用的な属性テーブルがあれば解決すると考えてしまう。 (これがアンチパターンのはじまり)

EAVの例 Issueテーブル

issue_id
1234

IssueAttributesテーブル

issue_id attr_name attr_value
1234 product 1
1234 data_reported 2009-06-01
1234 status NEW
1234 description 保存処理に失敗する

EAV

名前/値ペアともよばれる。

メリットは - 両方のテーブルの列数を減らせる - 新しい属性が増えても列数を増やさなくていい - 属性が存在しないエンティティの該当列にNULLが入ってるNULLだらけのテーブルになるのを防げる。(使わない項目が多すぎるテーブルとか)

EAVのデメリット

  • テーブルから報告日の項目を取得したい場合、クエリが冗長になる。
  • 必須属性を設定できない
  • SQLのデータ型を使えない。
  • 参照整合性を強制できない (外部キーをつけるとすべての行に適用されてしまう)
  • 属性名補わないといけない (attr_nameに入る名前がバラバラになる可能ある)
  • 行を再構築しないといけない

クエリで各属性の行をJOINする必要があるし、クエリ作成時には属性の名前をすべて指定する必要がある。

issue_id data_reported status priority description
1234 2009-06-01 NEW HIGHT 保存処理に失敗する

アンチパターンを使用していい場合

RDBでEAVを使う理由は簡単に見つからない。 RDBの長所が失われるから。 非リレーショナルなデータ管理が必要なら非リレーショナルな技術を使うべき。 (dynamoDB、Cloud DataStore、Cassandra、MongoDB、Redisとか)

EAVを使わずにEAVが扱うようなデータを格納する方法

シングルテーブル継承

すべてのタイプの属性を個別の列に格納して、関連するすべてのサブタイプを1つのテーブルにまとめる。

デメリット
  • NULLと非NULLの列がバラバラに点在してしまう。
  • 新しいオブジェクトタイプが増えるとその分の属性が増えるので列数の実際的な上限に達する可能性もある。

具象テーブル継承

サブタイプ毎にテーブルを作成する。 (基底の属性 + サブ固有属性のテーブル) サブタイプに存在しない属性列を格納する必要がないことがメリット

デメリット
  • すべてのサブタイプに共通する属性とサブタイプ固有の属性の区別が簡単につかない
  • 共通属性に新しい属性を追加する場合、すべてのサブタイプの属性の変更が必要になる。
  • すべてのオブジェクトを取得したいときもテーブルがわかれるので面倒

クラステーブル継承

オブジェクト指向の継承を模倣する、 基底型のテーブルを1つ作り、サブタイプ毎にテーブルをつくる。 サブタイプテーブルは基底型テーブルに対する外部キーの役割を持つ主キーを設定する。 基底型テーブルと1対1の関連が強制される。

半構造化データ

XMLJSONなどの構造化されたデータをそのまま打ち込む (ジェイウォーク大丈夫?)

デメリット
  • SQLが特定の属性にアクセスする手段を殆ど持ってない。

新しい属性を随時定義するための高い柔軟性が必要な場合に適している

パーフェクトRuby on Rails 増補改訂版 2章〜3章を読んだ

と続けて読んだので、そのままパーフェクトRuby on Rails増補改訂版を2章〜3章読んだ。

Rails本2冊読んだばかりなので、知ってる箇所を飛ばして、知らないところについてまとめた。

2章

ActiveRecord::Relation

ActiveRecord::Relationは内部でどのようなSQLを発行するか、という情報だけを保持する。実際にSQLの実行結果が必要になるまではDBへのアクセスは発生しない。 メソッドチェインによるクエリ構築を行うためこのように設計されている。 明示的に任意の箇所でSQLを発行したい場合はto_aメソッドを呼び出して即座にSQLを発行できる。

Scopeの定義

Scopeで定義した条件の結果がnilの場合、該当Scopeの検索条件を除外したクエリを発行するので意図しない結果になることがある nilを返す必要がある場合はクラスメソッドとして定義する方が良い。

ActiveRecord::Enum

sales_statusカラムに対して以下のようにenumを設定する。

class Book < ApplicationRecord
  enum sales_status: {
    reservation: 0,
    now_on_sale: 1,
    end_of_print: 2,
  }
end

ステータス毎に述語メソッドで状態を問い合わせることが可能

pry(main)> book = Book.last
pry(main)> book.reservation?
=> true

# !をつけるとそのenum値へ更新することができる。
pry(main)>  book.now_on_sale!
pry(main)>  book.now_on_sale?
=> true

# カラム名_before_type_castで実際の値を確認することができる。
 pry(main)> book.sales_status_before_type_cast
=> 1

enum型の名称で検索するためのscopeも追加される。

 pry(main)> Book.reservation
  Book Load (0.1ms)  SELECT "books".* FROM "books" WHERE "books"."sales_status" = ?  [["sales_status", 0]]
=> [#<Book:0x00007fbf8eb082b8
  id: 3,
  name: "enum Book 1",
  published_on: nil,
  price: 100,
  created_at: Mon, 11 Jan 2021 02:35:23.632974000 UTC +00:00,
  updated_at: Mon, 11 Jan 2021 02:38:22.399098000 UTC +00:00,
  publisher_id: 1,
  sales_status: "reservation">]

その他

  • デフォルトスコープはモデルクラスの操作全てに影響を与えるので注意深く検討してから使用すべき。
  • コールバックが実行されないメソッドの実行は気を付ける(update_系とか)

3章

Rackとは?

RackはWebアプリケーションサーバーとRailsの間に存在する。 Rackを経ることでアプリケーションサーバーとRails疎結合になるので、それぞれの組み合わせを変更することが容易になる。 Rackからフレームワーク層に到達するまでに処理を挟むことができるRackミドルウェアと呼ばれる機構があって、独自の処理やよく使う機能を差し込むこともできる。 Rackが利用するエントリーポイント用ファイルは一般的にはconfig.ruというファイルを利用する。

感想

Rails本3冊目なので、もっと早く読み終わると思ってたが、思った以上に内容が充実していて、3章まででも結構時間がかかった。 さすがのパーフェクトシリーズで、もはやRuby自体の説明はなく、いきなりRailsの説明から入る初級者をぶった切った内容、渋い。
Rubyの経験がない人はチェリー本Ruby自体を勉強してから望んだ方が良いと思う。 Ruby覚えて、これからRailsを覚えたい人は、まずRailsの教科書を読んでから挑戦した方が理解が進むと思われる。 4章以降もかなり濃い内容なのでまとめていくには時間がかかりそう。ゆっくり読んでいきます。

独習Ruby on Railsを読んだ

こないだ現場Railsを読んだら、Railsに知らないことや忘れてるところが結構あった。 blog.tera-chan3700.com

一応業務でRails触ってるけど、既存コードの改修だったりでなんとなくこなせていたけど、業務で使ってる以上のことが全然成長してないので、まだまだ基礎的なとこ固めようと思い、今度は独習Ruby on Railsを読むことにした。

独習Ruby on Rails

独習Ruby on Rails

  • 作者:小餅 良介
  • 発売日: 2019/06/19
  • メディア: 単行本(ソフトカバー)

3章までは、軽く読んだのでまとめてない。 4章以降で初めて知ったことなどメモしておく。

4章

rails dbconsole(rails db)

実装されてるDBのコマンドラインツールを起動してくれる。 SQLite3/MySQL/PostgreSQLの3種類が対象。 --sandboxオプションは使用できない。

rails stats

現在のアプリケーションのRubyコード統計情報を表示するコマンド。

rails notes

ソースコードに埋め込んだコメントの中からTODO、FIXME、OPTIMIZEのキーワードがあるものを抽出し内容と場所を一覧で表示してくれる。

6章

フォームオブジェクト

現場Railsでもちょっとだけ記載があったフォームオブジェクト。 特定のモデルに限定されないか、DBの登録に直接関係ない入力のバリデーションは入力専用のオブジェクト(フォームオブジェクト)を使うことで整理ができる。 利用承諾など、チェックボックスだけがあり、対応するモデルがない場合などに使える。 app/forms/acceptance.rb みたいな感じでファイルを作成し、ActiveModelを継承させることでバリデーションの機能を実装することができる。

Validateクラスで共通の独自ヘルパーを実装

app/validators配下にActionModel::EachValidatorを継承したクラスを作り、validate_eachを実装することでモデル共通のバリデーションを作成することができる。 バリデーションをまとめることができるのは知らなかった。

コールバッククラスの共通化

バリデーションと同様コールバックも共通化できる。 app/callbacks配下にクラスを作成し、中にバリデーションの処理を実装する。

class MessageOut
  def self.before_validation(obj)
    # バリデーション処理
  end
end

呼び出し側

class Book < ApplicationREcord
  before_validation MessageOut

  ....
end

こんな感じ。

7章

ポリモーフィック関連

子モデルで複数の親モデルを持つ時にポリモーフィック関連を使う。 userモデル、bookモデルを親に持つpictureモデルを作る場合、以下のようにmigrationファイルを作成

class CreatePictures < ActiveRecord::Migration[6.1]
  def change
    create_table :pictures do |t|
      t.references :imageable, polymorphic: true
      t.string :path_name

      t.timestamps
    end
  end
end

db:migrationするとPictureモデルは以下のようになる

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

各親モデルにPictureモデルへの関連と行う。 bookモデル

class Book < ApplicationRecord
  has_many :pictures, as: :imageable
end

userモデル

class User < ApplicationRecord
  has_many :pictures, as: :imageable
end

こうすることによって、PictureはUserとBookを親に持つことができる。

仮想属性

テーブルカラムを持たない仮想的な属性(attributes API)を作ることができる。 Addressモデルに対してattributeを追加することで使用できる。

class Address < ApplicationRecord
  attribute :location, :string, default: '東京都葛飾区'
end

8章

コレクションルートとメンバールート

  • コレクションルート 複数のリソースを対象とするルートかidを持っていない新規リソースに対するルート

  • メンバールート idパラメータで特定できるルート

shallowルート

親子関係にあるリソースで子リソースをidで一意に特定できる場合、子のIDだけでアクセスできるようにするのにshallowを使う。

shallow do
  resources :user do
    resources :hobbies
  end
end

または

resources :users, shallow: true do
  resources :hobbies
end

のように設定する。

リソースフルルートのグループ化

名前空間によるリソースルートのグループ化を行った場合、名前空間はコントローラのファイルが配置されるディレクトリ名に相当する。 なのでコントローラは「名前空間名」ディレクトリに移動させる必要がある。

リソースルートだけをグループ化したい場合はscopeメソッドを使う。 scopeの場合だとコントローラの配置移動の必要はない。

ルートの共通化

同じ目的のルートパス名を複数で使用する場合は、concernを使うことで共通化できる。

# 共通ルート
concern :searchable do
  get 'search', on: :collection
end

# 各リソースフルルートで共通ルートを紐付ける
resources :user,s concerns: :searchable
resources :books, concerns: :searchable

9章

respond_toメソッド

リクエストされるフォーマットによてレスポンスを柔軟に変更することができる。

def show
  @user = User.find(params[:id])
  respond_to do |format|
    format.html
    format.json {render json: @user}
  end
end

上記のコードの場合

  1. http://localhost:3000/users/1
  2. http://localhost:3000/users/1.json

で1の場合html、2の場合jsonでレスポンスを返してくれる。

10章以降

viewに関するとことか、とりあえず直近の業務では触ることないのでサラッと読んで終わり。 必要な時が来たら読み直そうと思う。

所感

現場Railsを読んだばかりだし同じような内容の箇所は飛ばして読んだが、 一つ一つの機能がより細かく説明されている。特に章末の練習問題がためになる。 写経だけしてプログラム動かすとできた気になるが、練習問題で自分で考えてコード書くとやっぱり身に付く。 scaffoldでさえ本見ないと、どうやるんだっけ?となってるので良い機会だった。 同じ技術の本を続けて読むと、1冊目は結構時間かかるけど、2冊目以降はある程度知ってる箇所飛ばして知らないとこにフォーカスできるので、この流れで次はパーフェクトRuby on Raisを読んでみようかと思う。