Ruby on RailsでMySQL

Ruby on RailsでMySQL

Ruby on Railsでデータベースを指定する場合についてです。
プロジェクト作成時に指定できます。

プロジェクト作成時に指定

> rails new アプケーション名 -d データベース

データベースに指定できるのは以下です。
mysql
oracle
postgresql
sqlite3
frontbase
ibm_db
sqlserver
jdbcmysql
jdbcsqlite3
jdbcpostgresql
jdbc

これで作成した場合はdatabase.ymlに接続先情報を書き込めば終了です。
以降は-dオプションなしでデフォルトのsqlite3になっているものをMySQLに書き換える場合です。

Gemfile

Gemfileの以下の部分を、

gem  "sqlite3", "~> 1.4"

以下に書き換え。

gem "mysql2", "~> 0.5"

bundle install します。

database.yml

データベースの接続設定を ./config/database.yml に追加します。
接続先情報が以下の場合です。

項目 情報
ホスト localhost
ポート 3306
データベース testdb
ユーザー root
パスワード passw0rd

sqlite3の設定を全て削除して以下に書き換え。

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: localhost
  port: 3306
  username: root
  password: passw0rd

development:
  <<: *default
  database: testdb

test:
  <<: *default
  database: testdb

production:
  <<: *default
  database: testdb

テーブル作成と利用

db:migrateは使わずにテーブル作成して利用します。
Rails側でDBテーブルの作成などもできるのですが今一使い難いので、ドロ臭くなりますが普通にDB作ってRailsで利用するまでの手順です。

データベース作成

A5:SQL なんかでER図作って、DDL作成してデータベースに反映します。

自分は普段SpringBoot + DBFluteで開発してたりするので、A5:SQLが出力したDDLを最初にペタっと張ってマイグレートするのが手順として染み付いているので、Railsのように generate modelで1テーブルずつカラム指定して作成するのがどうしても慣れないんです。

簡単に以下のようなテーブルを作成しました。

ユーザーとユーザー属性は 1:1 です。
DDLは以下のようになります。

-- ユーザー属性
-- * RestoreFromTempTable
create table user_attributes (
  id bigint not null auto_increment comment 'ID'
  , user_id bigint not null comment 'ユーザーID'
  , phone varchar(11) comment '電話番号'
  , memo text comment 'メモ'
  , created_at datetime(6) not null comment '作成日'
  , updated_at datetime(6) not null comment '更新日'
  , constraint user_attributes_PKC primary key (id)
) comment 'ユーザー属性' ;

-- ユーザー
-- * RestoreFromTempTable
create table users (
  id bigint not null auto_increment comment 'ID:user_id'
  , name varchar(255) not null comment '名前'
  , created_at datetime(6) not null comment '作成日'
  , updated_at datetime(6) not null comment '更新日'
  , constraint users_PKC primary key (id)
) comment 'ユーザー' ;

alter table user_attributes
  add constraint user_attributes_FK1 foreign key (user_id) references users(id)
  on delete cascade
  on update no action;

この既に出来上がってるDBをRailsから利用します。
Railsの場合テーブルの物理名は必ず複数形にするのが推奨されています。
以下でDBアクセス用のモデルを作成した場合、

> rails g model User

Railsは User = users に関連付けされるためです。
ただmodelとテーブルの関連付けは self.table_name を定義すれば指定できます。
テーブル名が userxxxxx になっている場合なら、

class User < ApplicationRecord
  self.table_name = "userxxxxx"
end

これで関連付けできます。
他にもRailsではテーブルに細かく規約的なものがあります。
詳細は Active Record を参照。
大まかには以下な感じです。

  • テーブル名は小文字スネークケースで複数形。line_itemsテーブルならLineItemモデルになります。
  • 主キーは"id"のbigintで統一。
  • 外部キー(foreign key)はテーブル名の単数形_id。line_itemsならline_item_idがfkになります。
  • created_atカラムがある場合はレコード作成日時が自動的に設定される。
  • updated_atカラムがある場合はレコード更新日時が自動的に設定される。

モデル作成

以下のコマンドでmodelを作成し、テーブルとの関連付けを行う。

> rails g model user --no-migration
> rails g model user_attribute --no-migration

テーブル名は users と user_attributes の複数形ですが、modelをgenerateする場合は単数形で指定します。
–no-migrationはRailsのマイグレーション機能をオミットするオプションです。これでrails db:migrateしなくてもよくなります。というかこれがないとアプリ実行中にdbのチェックが行われてエラーになります。

テーブル間の関係付け

usersとuser_attributesは1:1の親子関係です。
これをRailsで表現する場合はmodelに手を加えます。
テーブル間の関係付け (forreign key)は以下で表します。詳細

キーワード 関係
belongs_to 親子関係の子側に設定
has_one 親子関係(1:1)の親側に設定
has_many 親子関係(1:N)の親側に設定
has_many :through 親子関係の子を介して別テーブルを参照(N:N)
has_one :through 親子関係で孫テーブルを参照(1:1)
has_and_belongs_to_many N:N、A,Bという二つのテーブルがある場合A,B両方の主キーを持ったA_Bというテーブルがないと成り立たないです

belongs_to, has_one, has_manyは2テーブル間、has_many :through, has_one :through, has_and_belongs_to_manyは3テーブル間の関係を示します。

usersとuser_attributesの関係ではbelongs_toとhas_oneを使います。

./models/user.rb

class  User < ApplicationRecord
  has_one :user_attribute
end

./models/user_attribute.rb

class UserAttribute < ApplicationRecord
  belongs_to :user
end

コントローラ作成

適当にInfosというコントローラを作成します。
g scaffoldで作るとmodelまで作られてしまうのでg controllerで作成します。

> rails g controller Infos index show create update destroy

以下のパスが作成されます。
./config/routes.rbを変更します。

  get 'infos', to: 'infos#index'
  get 'infos/show/:id', to: 'infos#show'
  post 'infos/create'
  put 'infos/update/:id', to: 'infos/update'
  delete 'infos/destroy/:id', to: 'infos#destroy'

アクションの実装

usersとuser_attributesテーブルを両方使用するような形で実装します。

index

usersとuser_attributesをjoinして一覧を出力します。
出力は以下のようなjsonにします。

[
  {
    "id": 1,
    "name": "テスト01",
    "user_attributes_id": 1,
    "phone": "07011112222",
    "memo": "テスト"
  }
]

Active Recodeでjoinして必要なカラムだけ取得するには以下のようなコードになります。

@Users = User.joins(:user_attribute).select("users.id, users.name, user_attributes.id as user_attributes_id, user_attributes.phone, user_attributes.memo")
puts @Users.to_json()

上記で実行されるSQLが以下になります。

SELECT users.id, users.name, user_attributes.id as user_attributes_id, user_attributes.phone, user_attributes.memo FROM `users` INNER JOIN `user_attributes` ON `user_attributes`.`user_id` = `users`.`id`

joinsやselectなどの詳細は Active Record クエリインターフェイス が詳しいです。

このまま戻り値として使用できるので、indexの実装は以下のようになります。

def index
  @Users = User
    .joins(:user_attribute)
    .select("users.id, users.name, user_attributes.id as user_attributes_id, user_attributes.phone, user_attributes.memo")
  render json: @Users
end

show

indexと似た形で引数で渡されたidのレコードのみ返します。

def show
  @User = User
    .joins(:user_attribute)
    .select("users.id, users.name, user_attributes.id as user_attributes_id, user_attributes.phone, user_attributes.memo")
    .where("users.id = :id", { id: params[:id] }).first()
  render json: @User
end

selectの所もうちょっとうまく書けないものか。

create

createでは以下のjsonを受け付けてusersとuser_attributesに新規レコードを作成ですね。
パラメータは以下にします。

{
  "name": "テスト01",
  "phone": "07011112222",
  "memo": "テスト"
}

createメソッドの中身は以下です。

def create
  form = params.permit(:name, :phone, :memo)
  user = User.new()
  user.transaction do
    # usersテーブル追加
    user.name = form[:name]
    user.save!
    # users_attributesテーブル追加
    userAttr = UserAttribute.new()
    userAttr.user_id = user.id
    userAttr.phone = form[:phone]
    userAttr.memo = form[:memo]
    userAttr.save!
    # 応答用のハッシュ作成
    result = {
      :user_id => user.id,
      :name => user.name,
      :user_attribute_id => userAttr.id,
      :phone => userAttr.phone,
      :memo => userAttr.memo
    }
    render json: result, status: :created
  end
rescue => e
  render json: { "message": e.message }, status: :internal_server_error
end

冗長ですが二つのテーブルをinsertする関係上途中でなんだかのエラーが発生した場合はロールバックする必要がありますのでこんな感じになってます。
細かく解説すると、

form = params.permit(:name, :phone, :memo)

上記の部分でリクエストのbody部分をハッシュで取得します。
Railsのルールとしてparams[:name]とやって取得するのはセキュリティ上問題があるので必ずpermitを使うようにする必要があります。

user = User.new()
user.transaction do
end

トランザクションはApplicationRecordのtransactionを使用します。
上記のdoブロックの中がトランザクションの有効範囲になります。
ここから例外(raise)が発生した場合はロールバックされます。
例外がなかった場合はコミットです。
手動でロールバック、コミットする方法は・・・見つけられませんでした。

render json: result, status: :created

上記でJSONの応答を生成します。
JSONの内容は以下のようになります。

{
  "id": 1,
  "name": "テスト01",
  "user_attribute_id": 1,
  "phone": "07011112222",
  "memo": "テスト"
}

statusの部分でHTTPステータスを指定します。
:createdは 201です。他のステータスは HTTPステータスコード に一覧があります。

update

ほぼcreateと同じ内容です。

def update
  form = params.permit(:id, :name, :phone, :memo)
  user = User.find(form[:id])
  userAttr = UserAttribute.where(["user_id = :id", { id: form[:id] }]).first()
  user.transaction do
    # usersテーブル更新
    user.name = form[:name]
    user.save!
    # users_attributesテーブル更新
    userAttr.phone = form[:phone]
    userAttr.memo = form[:memo]
    userAttr.save!
    # 応答用のハッシュ作成
    result = {
      :id => user.id,
      :name => user.name,
      :user_attribute_id => userAttr.id,
      :phone => userAttr.phone,
      :memo => userAttr.memo
    }
    render json: result, status: :created
  end
rescue => e
  render json: { "message": e.message }, status: :internal_server_error
end

destroy

usersテーブルだけ削除します。
user_attributesの方はFKの設定で “on delete cascade” を設定しているので親のusersを削除すれば勝手に削除されます。

def destroy
  user = User.find(params.permit(:id)[:id])
  user.destroy
  render status: :ok
end

随分長いなったけど基本はこんな感じになりますか。
もう少し簡潔に記述できる気がします。でもドロ臭い方が分かり易い。

コメント

このブログの人気の投稿

(真)ESP32の開発環境をVSCodeで作る

ESP32にmicroSD接続

ESP32でデバッグ