Ruby on Rails ~レールの路線図~ Part 3 [Ruby3, Rails6]

2006.1.21 に Rubyist九州 Meeting 第1回にて発表した, Ruby on Rails のスライドです。2005年12月に Rails 1.0 が出ました。その前後, 同月に初めて Railsに触ったので、まだほんの初歩的、序の口のところです。

[2021.5] ざっと現代の状況に更新。Ruby 3.0 + Ruby on Rails v6.1. サンプルコード netsphere / rails-examples · GitLab

  1. ページ1 アプリケィションひな形の起動まで。
  2. ページ2 最初のDBアプリケィションづくり。
  3. このページ. 多対多リレーションシップ, トランザクション、ほか。

モデル 2周目

1:多、多:多リレィションシプ

テーブルとテーブルとの関係性をリレーションシップという。よく書き間違いがあるが, 「リレーション」とはテーブルの定義を指し、関係性ではない。リレーションの関係性とか関連付けをリレーションシップという名前で呼ぶ。

Rails では, モデルクラスに, 関係性を書く。

  1. 1:1
    • has_one :相手テーブル名の単数形
  2. 1:多
    • 1の側: has_many :相手テーブル名
      • 相手テーブルに自エンティティを指すFK
      • いくつかのメソッドを生成
    • 多の側: belongs_to :相手テーブル名の単数形
      • 自テーブルにあるFKの操作メソッドを生成
  3. 多:多
    • has_and_belongs_to_many :相手テーブル名
    • has_many through:

多対多をやってみよう。tag モデルを作る。

$ rails g scaffold tag name:string
Running via Spring preloader in process 5206
      invoke  active_record
      create    db/migrate/20210719132128_create_tags.rb
      create    app/models/tag.rb
      invoke    test_unit
      create      test/models/tag_test.rb
      create      test/fixtures/tags.yml
      invoke  resource_route
       route    resources :tags
      invoke  scaffold_controller
      create    app/controllers/tags_controller.rb
      invoke    erb
      create      app/views/tags
      create      app/views/tags/index.html.erb
      create      app/views/tags/edit.html.erb
      create      app/views/tags/show.html.erb
      create      app/views/tags/new.html.erb
      create      app/views/tags/_form.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/tags_controller_test.rb
      create      test/system/tags_test.rb
      invoke    helper
      create      app/helpers/tags_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/tags/index.json.jbuilder
      create      app/views/tags/show.json.jbuilder
      create      app/views/tags/_tag.json.jbuilder
      invoke  assets
      invoke    css
      create      app/assets/stylesheets/tags.css
      invoke  css
      create    app/assets/stylesheets/scaffold.css

ひな形の migration ファイルを修正。中間テーブルは、意味のある名前が推奨される。今回の例ではこのテーブルの意味が乏しいので、そういうときは 複数形_複数形 が標準。

Ruby
[RAW]
  1. # -*- coding:utf-8 -*-
  2. # 多対多サンプル
  3. class CreateTags < ActiveRecord::Migration[6.1]
  4. def change
  5. create_table :tags do |t|
  6. t.string :name, null:false, index:{unique:true}
  7. t.timestamps
  8. end
  9. # 中間テーブル
  10. create_table :articles_tags do |t|
  11. # belongs_to は単数形. article_id 列が生成される.
  12. t.belongs_to :article, null:false, foreign_key:true
  13. t.belongs_to :tag, null:false, foreign_key:true
  14. t.integer :order, null:false
  15. end
  16. add_index :articles_tags, [:article_id, :tag_id], unique:true
  17. end
  18. end

モデルクラスと、中間テーブルのクラスを作る。

Ruby
[RAW]
  1. # -*- coding:utf-8 -*-
  2. # タグ
  3. class Tag < ApplicationRecord
  4. # 中間テーブルが独自の列を一切持たない場合は, has_and_belongs_to_many を使う
  5. # が、そのような状況は稀。通常は has_many :through を使う.
  6. has_many :articles_tags
  7. has_many :articles, through: :articles_tags
  8. validates :name, presence:true
  9. # Callback として, before_save, before_validation などがある.
  10. before_save :normalize_text
  11. private
  12. # for before_save
  13. def normalize_text
  14. self.name = name.unicode_normalize :nfkc
  15. end
  16. end

外部キーを作るテーブルのモデルクラスに belongs_to を書く。

Ruby
[RAW]
  1. # -*- coding:utf-8 -*-
  2. # 中間テーブル
  3. class ArticlesTag < ApplicationRecord
  4. belongs_to :article
  5. belongs_to :tag
  6. end

トランザクション

複数のテーブルを更新するとき、途中で中断してはならないことが多い。例えば預金の間の資金移動では, 出金口座からの引き去りと、入金口座への加算の二つの処理は、all or nothing でなければならない。

こういう場合, データベースでトランザクションを掛ける。トランザクション処理 (OLTP) は, データベースへの変更を全部反映する (commit) か, すべてを取り消す (rollback) かのいずれかになることが保証される。

Rails では簡単に, ActiveRecord::Base.transaction ブロックで囲めばよい。ブロック内で例外が発生すれば rollback され, そうでなければ commit される。なので、データベースの更新は失敗時に例外を発生させるほうのメソッドを使うようにしなければならない。

app/controllers/articles_controller.rb

Ruby
[RAW]
  1. # POST /articles or /articles.json
  2. def create
  3. @article = Article.new(article_params)
  4. @tags_str = params[:tags_str]
  5. begin
  6. ActiveRecord::Base.transaction do
  7. @article.save!
  8. update_tags! @tags_str
  9. end
  10. rescue ActiveRecord::RecordInvalid
  11. render :new, status: :unprocessable_entity
  12. return
  13. end
  14. redirect_to @article, notice: "Article was successfully created."
  15. end
Ruby
[RAW]
  1. private
  2. def update_tags! tags_str
  3. # create() は無効な場合は単にそのオブジェクトを返す。無用メソッド。
  4. tags_str = tags_str.unicode_normalize(:nfkc).strip
  5. tags = tags_str.split(/[ \t]+/).uniq.map do |tag_name|
  6. Tag.where(name:tag_name).first || Tag.create!(name:tag_name)
  7. end
  8. # 全部消して、作り直す.
  9. ArticlesTag.where(article_id:@article.id).delete_all
  10. tags.each_with_index do |tag, idx|
  11. ArticlesTag.create!(article_id:@article.id, tag_id:tag.id, order: idx + 1)
  12. end
  13. end

トランザクションの途中の状態が, 並行して行われる別の処理から透けて見えるかどうかは、DBMS ごとに異なる。SQL 標準では, 4つのトランザクション隔離性水準 (transaction isolation level) が定義されている。PostgreSQL 13 のデフォルト隔離性水準は READ COMMITTED. MySQL 8.0 InnoDB のデフォルトは REPEATABLE READ.

国際化・多言語化

今どきは、多言語化するWebアプリケーションも普通になっている。View ファイルを言語の数だけ作ればいい。これは力技。

それに加えて、submit ボタンのラベルなど、プログラムから生成するパーツについて, 短いテキストの差し替えををおこなう。

言語ごとのファイルを用意する。config/locales/ja.yml ファイル:

ja:
  helpers:
    submit:
      create: "登録する"
      update: "更新する"

URLから locale を判定して、設定する。

app/controllers/application_controller.rb:

Ruby
[RAW]
  1. # -*- coding:utf-8 -*-
  2. class ApplicationController < ActionController::Base
  3. around_action :switch_locale
  4. # 国際化対応 (手抜き) ... http://localhost:3000/articles/3/edit?locale=ja
  5. # 本当は、URL に埋め込むようにする。https://hostname/ja-jp/resources/x/edit
  6. # See https://guides.rubyonrails.org/i18n.html
  7. def switch_locale(&action)
  8. locale = params[:locale] || I18n.default_locale
  9. I18n.with_locale(locale, &action)
  10. end
  11. end

それから

  • セッション管理

    クッキーを利用。[2021-09] 認証については, セキュリティホールを避けるため、適切なライブラリを使うべき. Devise は複雑すぎるので, Sorcery がおすすめ。サンプルプログラムを置いておく; LoginSample-sorcery-1 · main · netsphere / rails-examples · GitLab

  • セキュリティ
    • ローカル資源へのアクセス
  • viewstate

Rails5 から APIモードが導入された。フロントエンドは React などの JavaScript フレームワークに任せて、バックエンドのみを提供する。Express などより Rails のほうが開発生産性が高いので、Ruby 開発者を確保できる場合、いい選択肢。

$ rails new myhelloapi --api

しばしば --api --webpack=react のようなコマンド例を見かけるが、誤り。--webpack オプションは, フロントエンド用で, APIモードでは単に無視される。

だいぶ様子の違う Gemfile になる。

Ruby
[RAW]
  1. source 'https://rubygems.org'
  2. git_source(:github) { |repo| "https://github.com/#{repo}.git" }
  3. ruby '3.0.1'
  4. # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
  5. gem 'rails', '~> 6.1.3', '>= 6.1.3.2'
  6. # Use sqlite3 as the database for Active Record
  7. gem 'sqlite3', '~> 1.4'
  8. # Use Puma as the app server
  9. #gem 'puma', '~> 5.0'
  10. # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
  11. # gem 'jbuilder', '~> 2.7'
  12. # Use Redis adapter to run Action Cable in production
  13. # gem 'redis', '~> 4.0'
  14. # Use Active Model has_secure_password
  15. # gem 'bcrypt', '~> 3.1.7'
  16. # Use Active Storage variant
  17. # gem 'image_processing', '~> 1.2'
  18. # Reduces boot times through caching; required in config/boot.rb
  19. gem 'bootsnap', '>= 1.4.4', require: false
  20. # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
  21. # gem 'rack-cors'
  22. group :development, :test do
  23. # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  24. gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  25. end
  26. group :development do
  27. gem 'listen', '~> 3.3'
  28. # Spring speeds up development by keeping your application running in the
  29. # background. Read more: https://github.com/rails/spring
  30. gem 'spring'
  31. end
  32. # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
  33. gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Rails7に向けて

[2021-07追加] Basecamp 社が Hotwire をリリースした。Hotwire is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire.

何でもJavaScriptで開発する風潮に対する対抗馬。

複雑なテーブル / レガシーシステム

  • set_table_name 'テーブル名'
  • set_primary_key '主キー名'

    ※複数主キーのときは?

  • find?

fin.

fin.