$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

Rails勉強し直している - DB操作編

基本に戻ろうと思い、Railsも勉強し直している。勉強し直しの時、Hatena-Textbookの課題を使うことが多いので、今回もそのようにした。

今回はデータベースの課題Railsを使ってやってみた。

作ったもの

この辺がdiff。かなり探索的に作ったのでcommitはごちゃっとしている。

簡易的にlist / add / deleteができるように作った。

bin/rails runner diary.rb add shibayu36 title body
bin/rails runner diary.rb list shibayu36 title body
bin/rails runner diary.rb delete shibayu36 3

あとは今回学んだことを雑多に書いていく。

Railsの初期セットアップ

何もわからず初期セットアップをしたので右往左往してしまったが、MySQL使いつつ、RSpec使ってみたいにするなら、こんな感じでセットアップした方が良さそうだった。

# databaseを切り替えて、testは入れないでおく
$ rails new project_name --database=mysql --skip-test

# それ以外色々不要なものを外すのであれば、オプションを色々つける
$ rails new project_name --database=mysql --skip-test --skip-action-mailer --skip-action-mailbox --skip-action-text --skip-active-storage --skip-action-cable

その後rspec-railsを入れて初期セットアップ

group :development, :test do
  gem "rspec-rails"
end
$ bundle install
$ bin/rails generate rspec:install

最終的には、Rails + Docker + MySQL + RuboCop + RSpec + factory_botというような構成にしてみたのだけど、この辺りはまた別の記事で書きたいと思う。

docker composeで起動するときに2回目以降サーバが立ち上がらない問題

A server is already running. Check /app/tmp/pids/server.pid.というエラーになって立ち上がらなくなる。こちらについては、ENTRYPOINTを設定してスクリプトで毎回消す、起動コマンドで削除を入れる、pidファイルの作成先を/dev/nullにするなどいろんな方法で回避できそうだった。

参考

モデルの作成

rails generate modelで結構いい感じにやってくれる。たとえば

bin/rails generate model User name:string:uniq

とすると、id/created_at/updated_atは勝手にできた上でuniqueなnameカラムを作ってくれる。NOT NULLとかを付けたいと思ったなら、migrationファイルにnull: falseとか書いておけば良い。

リファレンスを付けたいときはこんな感じ。

bin/rails generate model Diary user:references name:string

一瞬でできて便利。

参考: Active Record マイグレーション - Railsガイド

utf8mb4_0900_ai_ci

開発していたらMySQLのcollationがutf8mb4_0900_ai_ciというものになっていた。見慣れないなーと思って調べたら、MySQL 8系からcollationのデフォルトがこれになったようだ。

今回はこのままでも全然問題なかったのだけど、変える時はどうするのかなと思い調べた。database.ymlにcollationを設定し、DBを作り直すと良いようだった。

collation: utf8mb4_general_ci
docker compose exec web bin/rails db:drop db:create

参考

app/以下に別ディレクトリを作るときのautoload

今回diary.rbにはできる限り実装を書かずに別のところに実装を書く方針とした。app/models/配下に書いても良いのだけど、試しに別のディレクトリを作るということをやってみたかった。そこでapp/commands/というディレクトリを切ったところ、autoload周りでハマってしまった。

最初はapp/commands/add.rbというのを作り、中にCommands::Addクラスを作っていた。これがRailsのautoloadのルールに則っていなかったため、エラーとなってしまった。

本来のルールは、app/commands/add_command.rbにAddCommandというクラスを作る必要があった。つまり

  • app/以下の1段目はnamespaceとして解釈されない
  • 2段目以降が解釈される。そのため、commands/add_command.rbのように、suffixにそのディレクトリ固有の名称を付けるのが一般的
    • controllerもcontrollers/users/diaries_controller.rb みたいになるのと同様

参考: https://railsguides.jp/autoloading_and_reloading_constants.html#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E6%A7%8B%E9%80%A0

GitHub Copilot便利すぎ

色々理由があって、ちゃんとGitHub Copilotを使ってなかったのだけど、今回は使ってみようという気持ちになった。便利すぎた。

具体的には

  • 実装書くときに、やりたいことに対して使ったら良さそうなメソッドを推薦してくれる感覚があった
  • テストを書き始める前に、ひとまず補完を試すと、テスト全体が補完されるという便利さ
    • テストケースの場合分けはそれっぽい感じになっている感覚だった。それを自分のイメージに合わせて細かく調整していく形
    • テスト自体も色々書いていると、have_attributes使ったらもっと簡単に書けるとかを推薦してくれて楽
  • テストを書いている時に繰り返しで入力する項目とかは、改行入れるたびに勝手に補完された
  • 英語のコメント書いている時に、英語自体が補完される...

という感じで、学習効率が異常に上がる感覚だった。今回はChatGPTを使ったり、GitHub Copilot使ったりと、AIを使った学習の高速化がすごいと実感できた。

まとめ

今回はRailsのDB周りの操作を勉強し直してみた。ちゃんと課題をやってみると自分が何もわかっていないことが実感できて便利。次はこれをWebからアクセスできるようにしてみたい。

幼児期からの性教育を学ぶ - 「おうち性教育はじめます」を読んだ

性教育は幼児くらいから始めた方が良いと聞いたことがあり、「おうち性教育はじめます」という本を読んだ。

性教育について家で簡単にできること、幼児期には何をしておいたら良いか、実際の伝え方例など漫画で非常にわかりやすく解説されていた。これを読んでいると、自分達が子供の頃に学校で学んだ性教育はひどいものだったなと思ってしまった。

すぐ使える内容になっていたため、読んで良かったと思う。2~3歳以降の子供がいる家庭なら全員読んでおくと良いと思った。

個人的に印象に残ったことは

  • プライベートパーツ(口、胸、性器、おしり)は大切に扱わなくてはいけないと伝える。親であっても勝手に触ったりみようとしてはいけない
    • 気づき: よく親が子供がかわいくておしりを触っているのを見るが、あまり良くない
  • おうちで性教育をするにはまずはここから
    • プライベートパーツの話をする
    • NO・GO・TELLを日常的に伝える
    • 思春期まではタッチングとリスニングを大切に
  • 子供に伝えるときは、「淡々と事実を伝える」「価値観をくっつけない」「子供が興味を示した時に答える。親は準備だけはしておく」を意識する

ちなみに最近思春期編も出たので、思春期の子供がいる人はそっちを買うでも良さそうです。

読書メモ

* プライベートパーツに関する部分で怖い、痛い、悔しい思いをした時は、NO・GO・TELL 39
    * はっきり拒否、逃げる、信頼できる大人に話す
* 親の愛情を伝えるには、タッチングとリスニング 45
    * 思春期まではタッチングを多め、それ以降はタッチングではなくリスニングを主体に
* おうちで性教育をするにはまずはここから 52
    * プライベートパーツの話をする
    * NO・GO・TELLを日常的に伝える
    * 思春期まではタッチングとリスニングを大切に
* 子供に伝えるときに気をつけたいこと 58
    * 淡々と事実を伝える
    * 価値観をくっつけない
    * 子供が興味を示した時に答える。親は準備だけはしておく
    * 幼い異性兄弟の場合、本人たちが嫌がらなければ同時に伝えたり答えてもいい
    * 高学年以上は同性の親が伝える
* 男性器の正しい洗い方 64
* 射精の伝え方例 72
    * 精通の時の対処でも、素晴らしい・汚れたのような価値観をくっつけない
* 体つきの変化が出たら、親や異性の兄弟とはお風呂や寝室を分ける 83
    * 体の変化がある前に親の方から言えると良い
* 女性器の説明 92、99
* 初経の時、父親はそっと席を外すと良い 100
* 受精の仕組みの伝え方例 118、120
* 性に関することを聞かれたら、非難っぽくならないように注意しつつ、なぜ聞いてみたかったかを聞き返すといい 123
* 答えるだけの準備ができていない時は、「今答える準備ができていないから準備しておく、また聞いてね」と正直に言う 128
* マスターベーションをみてしまった時は、禁止ではなくマナーを教えることが大事 140
* 避妊についての知識 155

グラフアルゴリズムの理解のためにBellman–Ford法をRubyで実装してみる

優先度付きキューにも使われる二分ヒープ構造をRubyで実装してみる - $shibayu36->blog;に引き続き。アルゴリズム図鑑を眺めていて、グラフ系のアルゴリズムを全く知らないことに気づいた。そこでこの本に載っていたBellman-Ford法についてRubyで実装してみたのでメモ。

重み付きグラフ

Bellman-Ford法の前に重み付きのグラフの構造を作っておく必要がある。以下のように実装をした。無向グラフも有向グラフも扱えるように作った。

class WeightedGraph
  def initialize
    @graph = Hash.new { |h, k| h[k] = [] }
  end

  def add_edge(from, to, weight)
    @graph[from].push(Edge.new(from, to, weight))
    @graph[to].push(Edge.new(to, from, weight))
  end

  def add_directed_edge(from, to, weight)
    @graph[from].push(Edge.new(from, to, weight))
  end

  def points
    @graph.keys
  end

  def edges
    @graph.values.flat_map do |edges|
      edges.map do |edge|
        { from: edge.from, to: edge.to, weight: edge.weight }
      end
    end
  end

  class Edge
    attr_reader :from, :to, :weight

    def initialize(from, to, weight)
      @from = from
      @to = to
      @weight = weight
    end
  end
end

Bellman-Fordアルゴリズム

Bellman-Ford法はグラフの最短経路問題を解くアルゴリズムだ。ダイクストラ法の方が高速だが、Bellman-Ford法は負の経路がある場合にも使える。

実装については以下を参考にした。 * ベルマン–フォード法 - Wikipedia * Bellman-Ford法の解説 - Qiita * 【アルゴリズム】ベルマンフォード法(BellmanFord)

実装

class BellmanFord
  def initialize(graph, from, to)
    @graph = graph
    @from = from
    @to = to
  end

  def shortest_cost
    @last_updated = Hash.new(nil)

    @cost = Hash.new(Float::INFINITY)
    @cost[@from] = 0

    (0..@graph.points.length - 1).each do |i|
      is_changed = false

      @graph.edges.each do |edge|
        candidate_cost = @cost[edge[:from]] + edge[:weight]
        if candidate_cost < @cost[edge[:to]]
          @cost[edge[:to]] = candidate_cost
          @last_updated[edge[:to]] = edge[:from]
          is_changed = true

          # negative loop detection.
          # This algorithm must finish within n - 1 iterations.
          # If the N-th iteration change cost, there must be a negative loop.
          if i == @graph.points.length - 1
            raise 'negative loop detected'
          end
        end
      end

      break unless is_changed
    end

    @cost[@to]
  end

  def shortest_path
    shortest_cost
    path = [@to]
    loop do
      break if @last_updated[path[0]].nil?

      path.unshift(@last_updated[path[0]])
    end
    path
  end
end

テスト

describe WeightedGraph do
  it 'calculates shortest cost' do
    graph = WeightedGraph.new
    graph.add_edge('A', 'B', 9)
    graph.add_edge('A', 'C', 2)
    graph.add_edge('B', 'C', 6)
    graph.add_edge('B', 'D', 3)
    graph.add_edge('B', 'E', 1)
    graph.add_edge('C', 'D', 2)
    graph.add_edge('C', 'F', 9)
    graph.add_edge('D', 'E', 5)
    graph.add_edge('D', 'F', 6)
    graph.add_edge('E', 'F', 3)
    graph.add_edge('E', 'G', 7)
    graph.add_edge('F', 'G', 4)
    expect(BellmanFord.new(graph, 'A', 'G').shortest_cost).to eq(14)
    expect(BellmanFord.new(graph, 'A', 'G').shortest_path).to eq(%w[A C D F G])
  end

  it 'also calculates shortest cost when graph is directed' do
    graph = WeightedGraph.new
    graph.add_directed_edge(0, 2, 1)
    graph.add_directed_edge(0, 3, 4)
    graph.add_directed_edge(0, 4, 5)
    graph.add_directed_edge(2, 1, 1)
    graph.add_directed_edge(3, 6, 4)
    graph.add_directed_edge(4, 5, 2)
    graph.add_directed_edge(4, 6, 2)
    graph.add_directed_edge(1, 5, 4)
    graph.add_directed_edge(1, 7, 8)
    graph.add_directed_edge(5, 7, 2)
    graph.add_directed_edge(6, 7, 5)
    expect(BellmanFord.new(graph, 0, 7).shortest_cost).to eq(8)
    expect(BellmanFord.new(graph, 0, 7).shortest_path).to eq([0, 2, 1, 5, 7])
  end

  it 'can detect negative loop' do
    graph = WeightedGraph.new
    graph.add_edge('A', 'B', 1)
    graph.add_edge('B', 'C', 3)
    graph.add_edge('C', 'D', -5)
    graph.add_edge('D', 'B', 1)
    graph.add_edge('C', 'E', 2)
    expect { BellmanFord.new(graph, 'A', 'E').shortest_cost }.to raise_error('negative loop detected')
  end
end

まとめ

今回は自分の中でグラフのアルゴリズムの理解を深めるため、Rubyで実装をしてみた。計算を繰り返すと最短経路を求められるというのは面白いと感じた。