20190227のMySQLに関する記事は5件です。

macにPHPとapacheをbrewから入れてみた

大変すぎた。元々入ってるphpとapacheが邪魔しまくってきたのでログを取っておく
CakePHPで作ったプロジェクトが動くところはまでは行くかと思います。

何はともあれ、まずhomebrewの最新化

$ brew update

PHPインストール

PHPの検索

$ brew search php

php7.1(お好きなバージョン)をインストール

$ brew install php@7.1

インストールしたPHPを使うように設定変更

$ brew link php@7.1
上記で出てきた以下のコマンドを実行する
$ echo 'export PATH="/usr/local/opt/php@7.1/bin:$PATH"' >> ~/.bash_profile
$ echo 'export PATH="/usr/local/opt/php@7.1/sbin:$PATH"' >> ~/.bash_profile

設定変わったか確認

$ which php

/usr/local/opt/php@7.1/bin/php

apacheをインスール

元から入ってるapacheの自動起動を止める

$ sudo apachectl stop
$ sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist

apacheを検索

$ brew search httpd

apacheをインストール

$ brew install httpd

設定変更

$ vi /usr/local/etc/httpd/httpd.conf

コメントを外す

#LoadModule userdir_module lib/httpd/modules/mod_userdir.so
#Include /usr/local/etc/httpd/extra/httpd-userdir.conf
#LoadModule rewrite_module modules/mod_rewrite.so

LoadModuleしてる辺りに追記
LoadModule php7_module /usr/local/opt/php@7.1/lib/httpd/modules/libphp7.so
<IfModule php7_module>
  AddType application/x-httpd-php .php
</IfModule>
書き換え

#ServerName www.example.com:8080

ServerName localhost

DirectoryIndex index.html

DirectoryIndex index.php index.html

DocumentRoot "/usr/local/var/www"
<Directory "/usr/local/var/www">

DocumentRoot "作成しているプロジェクトのディレクトリ"
<Directory "作成しているプロジェクトのディレクトリ">
上で書き換えたの中を変更

AllowOverride None

AllowOverride All

mysqlをインストール

$ brew search mysql
$ brew install mysql@5.6
$ brew link mysql@5.6
$ echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.bash_profile

mysql起動

$ source ~/.bash_profile
$ mysql.server start

mysql.sockの設定

$ mysqladmin version
UNIX socket っという項目の後に書いてあるmysqld.sockファイルの場所を記録
/usr/local/etc/php/7.1/php.iniに記述する
pdo_mysql.default_socket=[sockファイル]

$ apachectl start

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでMySQLに接続できない場合の対応

MySQLに接続しようとしたらPDOExceptionが発生した。

PHP Fatal error:  Uncaught PDOException: could not find driver in hoge.php:1

ドライバが見つからないと言っているので、この場合はphp-mysqlを入れます。

sudo apt install php-mysql

おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

外部サーバーの Docker の MySQL に Sequel Pro から接続する

Docker で運用している Redmine の MySQL サーバーに Sequel Pro からアクセスしようとしたら、以下のエラーメッセージが表示されて接続できない。

Lost connection to MySQL server at 'reading initial communication packet', system error: 0

外部サーバーに SSH で接続してコンソールから mysql コマンドで接続することは成功している。

mysql -h 127.0.0.1 -u root -p

MySQL サーバーの接続ホスト名を 0.0.0.0172.18.0.2 (コンテナの IP アドレス) などにしてみるも変わらず。

grep refused /var/log/secure
Feb 27 05:49:29 rm sshd[23858]: refused local port forward: originator 127.0.0.1 port 54698, target 172.18.0.2 port 3306
Feb 27 06:34:16 rm sshd[25509]: refused local port forward: originator 127.0.0.1 port 55548, target 0.0.0.0 port 3306
Feb 27 06:34:40 rm sshd[25530]: refused local port forward: originator 127.0.0.1 port 55565, target 127.0.0.1 port 3306

/var/log/secure を覗くと refused local port forward というエラーが出力されているので検索すると /etc/ssh/sshd_configAllowTcpForwarding ディレクティブが影響するよう。

grep ^AllowTcpForwarding /etc/ssh/sshd_config
AllowTcpForwarding no

Docker が動作しているホスト OS の sshd_configAllowTcpForwarding の値が no になっていた。

AllowTcpForwarding は TCP 転送を許可するかどうかを指定する。 1

sed -i '/^AllowTcpForwarding/ s/no/yes/' /etc/ssh/sshd_config
systemctl restart sshd

AllowTcpForwarding の値を yes に変更して sshd を再起動したところ、無事に Sequel Pro で Docker コンテナの MySQL サーバーに接続することができた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker + Rails + MySQL + Vue で Todo アプリ作成 〜その1〜

はじめに

前回: Docker + Rails で開発環境を作った。
せっかく作ったのに開発しないのはもったいないよね。
でもできることって限られてる。
ということで、TODOアプリを作ってみる

目標

  • trello
  • 今回はとりあえず、seed データを投入して、その一覧を表示させるとこまで。
  • タイトル詐欺してますね、「その1」では Vue は使いません(そこまでいけない)

1.準備

Docker を起動し、コンテナに接続していきます。

$ cd myapp
$ docker-compose up
$ docker ps

myapp_webmysqlCONTAINER ID を控える
そぞれのコンテナに接続をする。

web
$ docker exec -it CONTAINER_ID bash
root@CONTAINER_ID:/myapp#    これでOK
db
docker exec -it CONTAINER_ID bash
root@CONTAINER_ID:/# mysql -u root -p
Enter password:  # database.yml に記載したパスワード
mysql>     これでOK

2. Rails の操作

2.1 モデルを作る

Task モデルを作る。

web
# rails g model Task title:string context:string level:integer
# rails db:migrate
2.2 コントローラーも作る

こっちは Tasks
複数形であることに注目、Rails Tutorial で確かそう習った

# rails g controller Tasks

とりあえず一覧を見たい

tasks_controller.rb
class TasksController < ApplicationController
  def index
    @tasks = Task.all
  end

  def show
  end

  def new
  end

  def create
  end

  def edit
  end

  def update
  end

  def destroy
  end
end

ついでに他のも CRUD の7つも枠だけ用意しておく。

2.3 index で表示させる seed データを作る
seeds.rb
Task.create(
  [
    {
      title: 'task 001',
      context: 'hogehoge',
      level: '2',
    },
    {
      title: 'task 002',
      context: 'fugafuga',
      level: '3',
    },
    {
      title: 'task 003',
      context: 'piyopiyo',
      level: '1',
    },
  ],
)

web
# rails db:seed
2.4 seed データを作成したら db を覗く

MySQL 側でデータベース名を確認し
sequel pro で実際にデータが入っているか確認する。

db
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| myapp_development  | # こいつを使うよ
| myapp_test         |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

終わったらコンテナごと exit しちゃう。

image.png
接続をしたら
image.png
やったぜ
次はこのデータを画面に表示させる。

3. View とその準備

さっき rails db:seed で作成したデータを表示させたい。
表示させるページは /tasks/(ルートページ)

3.1 ルーティング
routes.rb
Rails.application.routes.draw do
  root to: 'tasks#index'

  resources :tasks
end
3.2 View ファイルの作成

model, controller は rails g があるけど
view にはないらしい。
ので、手動で作っていく、

$ touch app/views/tasks/index.html.erb
$ touch app/views/tasks/show.html.erb
$ touch app/views/tasks/new.html.erb
$ touch app/views/tasks/edit.html.erb
index.html.erb
<h1>Your Tasks</h1>

<ul>
  <% @tasks.each do |task| %>
  <li><%= task.title %><%= task.context %></li>
  <% end %>
</ul>

Task.all の数だけ Task の持っている title と context を表示

...なんかだんだんモデルの作りがイケてない気がしてきた。

そしてローカルホストに接続して確認。

image.png
できた。
とりあえずこれでおしまい。
続きは次回。

参考

Railsでタスク管理ができるWebアプリを作成してみた(Rails入門)
【Ruby on Rails】ToDoアプリを簡単に作ってみる

ありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails の UUIDプライマリキーを試す

RailsでUUIDプライマリキーを試してみた。

UUIDプライマリキーとは、オートインクリメントされる連番の整数ではなく、
077cacaa-3913-11e9-aacc-0242ac1c0002の様なランダムな36文字のUUIDのをIDとして、プライマリキーとして使うことである。

検証環境

  • Ruby 2.6.1
  • Rails 5.2.2
  • PostgreSQL 10.7
  • MySQL 5.7.25 と 8.0.15

データベースのバージョンは Amazon Auroraで現状使えるバージョンに近しいものを選択した。

検証モデル

次のようなプロジェクトごとにタスクを登録し、それぞれのタスクにメンバーをアサインするモデルを考える。

model.png

次のような関連を持っている。

  • ProjectTask は 一対多
  • TaskMember は 多対多(タスクには複数人をアサイン可能)

PostgreSQLの場合

PostgreSQLはUUID型があるのでプライマリキーの型をUUIDにする。

マイグレーションファイル

各モデルともIDはUUIDとする。
最初のテーブルprojectsを作成する前にenable_extension 'pgcrypto'を実行してPostgreSQLの拡張を有効化する。

db/migrate/xxxxxxxxxxxxxx_create_projects.rb
class CreateProjects < ActiveRecord::Migration[5.2]
  def change
    enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
    create_table :projects, id: :uuid, comment: 'プロジェクト' do |t|
      t.string :name, null: false, comment: 'プロジェクト名'
      t.text :description, comment: '概要'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end

続くtasksテーブルでは、projectsテーブルにt.referencesで関連を定義する。
関連先のIDの型を反映するのかと思ったのだが、bigintとなり、エラーとなるので type: :uuidでわざわざ指定している。

db/migrate/xxxxxxxxxxxxxx_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[5.2]
  def change
    create_table :tasks, id: :uuid, command: 'タスク' do |t|
      t.references :project, type: :uuid, foreign_key: true
      t.string :name, null: false, command: 'タスク名'
      t.text :description, comment: '概要'
      t.integer :status, default: 0, comment: 'ステータス'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end

最後のmembersテーブルはtasksと多対多の関係としたいので、関連テーブルmembers_tasksも追加する。
has_and_belongs_to_manyでリレーションを作るので、members_tasksid: falseとしている。

db/migrate/xxxxxxxxxxxxxx_create_member.rb
class CreateMembers < ActiveRecord::Migration[5.2]
  def change
    create_table :members, id: :uuid, comment: 'メンバー' do |t|
      t.string :name, null: false
      t.text :description

      t.timestamps
    end

    create_table :members_tasks, id: false, comment: 'タスクとメンバーの関連テーブル' do |t|
      t.references :member, type: :uuid, foreign_key: true
      t.references :task, type: :uuid, foreign_key: true

      t.timestamps
    end
  end
end

これらの結果、生成されるテーブル群は次の通り。

                               Table "public.projects"
   Column    |            Type             | Collation | Nullable |      Default      
-------------+-----------------------------+-----------+----------+-------------------
 id          | uuid                        |           | not null | gen_random_uuid()
 name        | character varying           |           | not null | 
 description | text                        |           |          | 
 start_on    | date                        |           |          | 
 end_on      | date                        |           |          | 
 created_at  | timestamp without time zone |           | not null | 
 updated_at  | timestamp without time zone |           | not null | 
Indexes:
    "projects_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "tasks" CONSTRAINT "fk_rails_02e851e3b7" FOREIGN KEY (project_id) REFERENCES projects(id)

                                 Table "public.tasks"
   Column    |            Type             | Collation | Nullable |      Default      
-------------+-----------------------------+-----------+----------+-------------------
 id          | uuid                        |           | not null | gen_random_uuid()
 project_id  | uuid                        |           |          | 
 name        | character varying           |           | not null | 
 description | text                        |           |          | 
 status      | integer                     |           |          | 0
 start_on    | date                        |           |          | 
 end_on      | date                        |           |          | 
 created_at  | timestamp without time zone |           | not null | 
 updated_at  | timestamp without time zone |           | not null | 
Indexes:
    "tasks_pkey" PRIMARY KEY, btree (id)
    "index_tasks_on_project_id" btree (project_id)
Foreign-key constraints:
    "fk_rails_02e851e3b7" FOREIGN KEY (project_id) REFERENCES projects(id)
Referenced by:
    TABLE "members_tasks" CONSTRAINT "fk_rails_61255d7478" FOREIGN KEY (task_id) REFERENCES tasks(id)

                                Table "public.members"
   Column    |            Type             | Collation | Nullable |      Default      
-------------+-----------------------------+-----------+----------+-------------------
 id          | uuid                        |           | not null | gen_random_uuid()
 name        | character varying           |           | not null | 
 description | text                        |           |          | 
 created_at  | timestamp without time zone |           | not null | 
 updated_at  | timestamp without time zone |           | not null | 
Indexes:
    "members_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "members_tasks" CONSTRAINT "fk_rails_7c97f456df" FOREIGN KEY (member_id) REFERENCES members(id)

                       Table "public.members_tasks"
   Column   |            Type             | Collation | Nullable | Default 
------------+-----------------------------+-----------+----------+---------
 member_id  | uuid                        |           |          | 
 task_id    | uuid                        |           |          | 
 created_at | timestamp without time zone |           | not null | 
 updated_at | timestamp without time zone |           | not null | 
Indexes:
    "index_members_tasks_on_member_id" btree (member_id)
    "index_members_tasks_on_task_id" btree (task_id)
Foreign-key constraints:
    "fk_rails_61255d7478" FOREIGN KEY (task_id) REFERENCES tasks(id)
    "fk_rails_7c97f456df" FOREIGN KEY (member_id) REFERENCES members(id)

モデルクラス

モデルの実装は次の通り。
UUIDをプライマリキーにしているからといって変わる部分はなく、
通常通り、リレーションを定義していけば良い。

app/models/project.rb
class Project < ApplicationRecord
  has_many :tasks
end
app/models/task.rb
class Task < ApplicationRecord
  belongs_to :project
  has_and_belongs_to_many :members

  enum status: { waiting: 0, doing: 1, done: 2, cancel: -1 }
end
app/models/member.rb
class Member < ApplicationRecord
  has_and_belongs_to_many :tasks
end

動作確認

次の様なサンプルコードで動作確認してみる。

サンプルコード
def sample
  # 予め登録されていると思われるオブジェクト
  project = Project.create(name: 'プロジェクト①');
  member1 = Member.create(name: 'メンバー①')

  task1 = Task.new(name: 'タスク-A', members: [member1])
  # アサインされるメンバーと同時登録
  task2 = Task.new(name: 'タスク-B', members: [member1, Member.new(name: 'メンバー②')]) 

  project.tasks = [task1, task2] # 複数タスクの同時の追加

  project.save
  project.reload

  pp project
  pp project.tasks
  pp project.tasks.last.members
end

結果は次の通り。
コードの通り各オブジェクトが保存できている。

irb(main):220:0> sample
#<Project:0x00005610d4c8c450
 id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
 name: "プロジェクト①",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
 updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>
  Task Load (0.4ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = $1  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"]]
[#<Task:0x00005610d4d4a6f8
  id: "7dcc7181-5286-4912-b25c-f2fcaf362eac",
  project_id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
  name: "タスク-A",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>,
 #<Task:0x00005610d4d49f28
  id: "e8325765-053b-40c1-a909-be679a5fe739",
  project_id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
  name: "タスク-B",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>]
  Member Load (0.4ms)  SELECT "members".* FROM "members" INNER JOIN "members_tasks" ON "members"."id" = "members_tasks"."member_id" WHERE "members_tasks"."task_id" = $1  [["task_id", "e8325765-053b-40c1-a909-be679a5fe739"]]
[#<Member:0x00005610d4cadd58
  id: "b63a0b87-657c-424a-93e1-c7c8c6e5e6f1",
  name: "メンバー①",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>,
 #<Member:0x00005610d4cadbc8
  id: "06555612-3a12-43a0-95d0-f0d18eff8e8f",
  name: "メンバー②",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>]
=> nil


SQLも含む結果
irb(main):220:0> sample
   (0.7ms)  BEGIN
  Project Create (0.7ms)  INSERT INTO "projects" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "プロジェクト①"], ["created_at", "2019-02-26 13:52:30.849742"], ["updated_at", "2019-02-26 13:52:30.849742"]]
   (1.8ms)  COMMIT
   (0.4ms)  BEGIN
  Member Create (2.2ms)  INSERT INTO "members" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "メンバー①"], ["created_at", "2019-02-26 13:52:30.856421"], ["updated_at", "2019-02-26 13:52:30.856421"]]
   (1.1ms)  COMMIT
  Task Load (0.5ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = $1  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"]]
   (0.3ms)  BEGIN
  Task Create (0.6ms)  INSERT INTO "tasks" ("project_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"], ["name", "タスク-A"], ["created_at", "2019-02-26 13:52:30.870659"], ["updated_at", "2019-02-26 13:52:30.870659"]]
  Task::HABTM_Members Create (0.5ms)  INSERT INTO "members_tasks" ("member_id", "task_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4)  [["member_id", "b63a0b87-657c-424a-93e1-c7c8c6e5e6f1"], ["task_id", "7dcc7181-5286-4912-b25c-f2fcaf362eac"], ["created_at", "2019-02-26 13:52:30.872595"], ["updated_at", "2019-02-26 13:52:30.872595"]]
  Task Create (0.5ms)  INSERT INTO "tasks" ("project_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"], ["name", "タスク-B"], ["created_at", "2019-02-26 13:52:30.874447"], ["updated_at", "2019-02-26 13:52:30.874447"]]
  Task::HABTM_Members Create (0.5ms)  INSERT INTO "members_tasks" ("member_id", "task_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4)  [["member_id", "b63a0b87-657c-424a-93e1-c7c8c6e5e6f1"], ["task_id", "e8325765-053b-40c1-a909-be679a5fe739"], ["created_at", "2019-02-26 13:52:30.876188"], ["updated_at", "2019-02-26 13:52:30.876188"]]
  Member Create (0.4ms)  INSERT INTO "members" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "メンバー②"], ["created_at", "2019-02-26 13:52:30.877893"], ["updated_at", "2019-02-26 13:52:30.877893"]]
  Task::HABTM_Members Create (0.4ms)  INSERT INTO "members_tasks" ("member_id", "task_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4)  [["member_id", "06555612-3a12-43a0-95d0-f0d18eff8e8f"], ["task_id", "e8325765-053b-40c1-a909-be679a5fe739"], ["created_at", "2019-02-26 13:52:30.879402"], ["updated_at", "2019-02-26 13:52:30.879402"]]
   (0.9ms)  COMMIT
   (0.4ms)  BEGIN
   (0.4ms)  COMMIT
  Project Load (1.2ms)  SELECT  "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2  [["id", "492e5a1f-4899-4f00-94be-02afb3f46be2"], ["LIMIT", 1]]
#<Project:0x00005610d4c8c450
 id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
 name: "プロジェクト①",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
 updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>
  Task Load (0.4ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = $1  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"]]
[#<Task:0x00005610d4d4a6f8
  id: "7dcc7181-5286-4912-b25c-f2fcaf362eac",
  project_id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
  name: "タスク-A",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>,
 #<Task:0x00005610d4d49f28
  id: "e8325765-053b-40c1-a909-be679a5fe739",
  project_id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
  name: "タスク-B",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>]
  Member Load (0.4ms)  SELECT "members".* FROM "members" INNER JOIN "members_tasks" ON "members"."id" = "members_tasks"."member_id" WHERE "members_tasks"."task_id" = $1  [["task_id", "e8325765-053b-40c1-a909-be679a5fe739"]]
[#<Member:0x00005610d4cadd58
  id: "b63a0b87-657c-424a-93e1-c7c8c6e5e6f1",
  name: "メンバー①",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>,
 #<Member:0x00005610d4cadbc8
  id: "06555612-3a12-43a0-95d0-f0d18eff8e8f",
  name: "メンバー②",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>]
=> nil


MySQLの場合

今度はMySQLでUUIDプライマリキーを試してみる。

activeuuidなどを使う方法もあるが、ここではgemには頼らず、文字列としてUUIDをプライマリキーにしてみる。

マイグレーションファイル

最初はprojectsテーブルから。
MySQLにはuuid型なんて存在しないので、idstringにして、UUIDを格納する。
UUIDは36文字なのでlimit: 36を設定するために一旦、id: falseとして、primary_key: trueとなるカラムを定義している。
varchar(255)になるのを気にしないなら、create_tableid: :stringを指定してもいいかと思う。

ruby
class CreateProjects < ActiveRecord::Migration[5.2]
  def change
    create_table :projects, id: false, comment: 'プロジェクト' do |t|
      t.string :id, limit: 36, null: false, primary_key: true, comment: 'プライマリキー'
      t.string :name, null: false, comment: 'プロジェクト名'
      t.text :description, comment: '概要'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end

続いて tasksテーブルもIDの定義についてはprojectsと同様。
projectsを参照するreferencesについては、指定しない場合、bigint
型で作ろうとして、型の不一致のエラーが発生するため、type: :stringを指定している。

ruby
class CreateTasks < ActiveRecord::Migration[5.2]
  def change
    create_table :tasks, id: false, command: 'タスク' do |t|
      t.string :id, limit: 36, null: false, primary_key: true, comment: 'プライマリキー'
      t.references :project, type: :string, foreign_key: true
      t.string :name, null: false, command: 'タスク名'
      t.text :description, comment: '概要'
      t.integer :status, default: 0, comment: 'ステータス'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end

membersmembers_tasksも同様。

ruby
class CreateMembers < ActiveRecord::Migration[5.2]
  def change
    create_table :members, id: false, comment: 'メンバー' do |t|
      t.string :id, limit: 36, null: false, primary_key: true, comment: 'プライマリキー'
      t.string :name, null: false
      t.text :description

      t.timestamps
    end

    create_table :members_tasks, id: false, comment: 'タスクとメンバーの関連テーブル' do |t|
      t.references :member, type: :string, foreign_key: true
      t.references :task, type: :string, foreign_key: true

      t.timestamps
    end
  end
end

これらのマイグレーションから生成されるテーブルは次の通り。

mysql> desc projects;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | varchar(36)  | NO   | PRI | NULL    |       |
| name        | varchar(255) | NO   |     | NULL    |       |
| description | text         | YES  |     | NULL    |       |
| start_on    | date         | YES  |     | NULL    |       |
| end_on      | date         | YES  |     | NULL    |       |
| created_at  | datetime     | NO   |     | NULL    |       |
| updated_at  | datetime     | NO   |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
7 rows in set (0.00 sec)

mysql> desc tasks;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | varchar(36)  | NO   | PRI | NULL    |       |
| project_id  | varchar(255) | YES  | MUL | NULL    |       |
| name        | varchar(255) | NO   |     | NULL    |       |
| description | text         | YES  |     | NULL    |       |
| status      | int(11)      | YES  |     | 0       |       |
| start_on    | date         | YES  |     | NULL    |       |
| end_on      | date         | YES  |     | NULL    |       |
| created_at  | datetime     | NO   |     | NULL    |       |
| updated_at  | datetime     | NO   |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
9 rows in set (0.00 sec)

mysql> desc members;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | varchar(36)  | NO   | PRI | NULL    |       |
| name        | varchar(255) | NO   |     | NULL    |       |
| description | text         | YES  |     | NULL    |       |
| created_at  | datetime     | NO   |     | NULL    |       |
| updated_at  | datetime     | NO   |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
5 rows in set (0.00 sec)

mysql> desc members_tasks;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| member_id  | varchar(255) | YES  | MUL | NULL    |       |
| task_id    | varchar(255) | YES  | MUL | NULL    |       |
| created_at | datetime     | NO   |     | NULL    |       |
| updated_at | datetime     | NO   |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+
4 rows in set (0.00 sec)

モデルクラス

app/models/project.rb
class Project < ApplicationRecord
  include IdGenerator

  has_many :tasks
end
app/models/task.rb
class Task < ApplicationRecord
  include IdGenerator

  belongs_to :project
  has_and_belongs_to_many :members

  enum status: { waiting: 0, doing: 1, done: 2, cancel: -1 }
end
app/models/member.rb
class Member < ApplicationRecord
  include IdGenerator

  has_and_belongs_to_many :tasks
end

各モデルクラスの実装はPostgreSQLの場合とほぼ変わらない。
include IdGenerator を除いては。

何をインクルードしているかというと次のモジュール。

app/models/concerns/id_generator.rb
module IdGenerator
  def self.included(klass)
    klass.before_create :fill_id
  end

  def fill_id
    self.id = loop do
      uuid = SecureRandom.uuid
      break uuid unless self.class.exists?(id: uuid)
    end
  end
end

PostgreSQLではテーブル定義でIDのデフォルト値にgen_random_uuid()という関数が設定されてた。
なので、データベース側でIDにUUIDを設定していた。

MySQL5.7では、関数をカラムのデフォルト値にできないので、Ruby側で作ってIDに設定する。
このモジュールをインクルードするとbefore_createフックでそれを行う。

動作確認

先程のサンプルコードを同じく実行してみた。

次の様に、コードの通り登録されている。
ただ、reload後のproject.tasksの順序が異なっている。

irb(main):015:0> sample
#<Project:0x000056398a8b82a0
 id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
 name: "プロジェクト①",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00,
 updated_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00>
  Task Load (0.5ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`project_id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0'
[#<Task:0x0000563989984fe0
  id: "9b133316-1d82-4489-8eb2-e1db7611c842",
  project_id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
  name: "タスク-B",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00>,
 #<Task:0x0000563989984630
  id: "f6c996b1-6523-4bf8-8fc3-2d4aa5110c42",
  project_id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
  name: "タスク-A",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00>]
  Member Load (0.5ms)  SELECT `members`.* FROM `members` INNER JOIN `members_tasks` ON `members`.`id` = `members_tasks`.`member_id` WHERE `members_tasks`.`task_id` = 'f6c996b1-6523-4bf8-8fc3-2d4aa5110c42'
[#<Member:0x0000563989a28ed8
  id: "ba74e512-6924-42c4-8bd5-c69bdb0b837f",
  name: "メンバー①",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00>]
=> nil


SQLも含む結果
irb(main):015:0> sample
   (0.6ms)  BEGIN
  Project Exists (0.6ms)  SELECT  1 AS one FROM `projects` WHERE `projects`.`id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0' LIMIT 1
  Project Create (0.7ms)  INSERT INTO `projects` (`id`, `name`, `created_at`, `updated_at`) VALUES ('cb3cd510-b631-49df-bb5b-a8984b6496e0', 'プロジェクト①', '2019-02-26 13:53:46', '2019-02-26 13:53:46')
   (2.4ms)  COMMIT
   (0.5ms)  BEGIN
  Member Exists (0.7ms)  SELECT  1 AS one FROM `members` WHERE `members`.`id` = 'ba74e512-6924-42c4-8bd5-c69bdb0b837f' LIMIT 1
  Member Create (0.5ms)  INSERT INTO `members` (`id`, `name`, `created_at`, `updated_at`) VALUES ('ba74e512-6924-42c4-8bd5-c69bdb0b837f', 'メンバー①', '2019-02-26 13:53:46', '2019-02-26 13:53:46')
   (4.0ms)  COMMIT
  Task Load (0.6ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`project_id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0'
   (0.5ms)  BEGIN
  Task Exists (0.9ms)  SELECT  1 AS one FROM `tasks` WHERE `tasks`.`id` = 'f6c996b1-6523-4bf8-8fc3-2d4aa5110c42' LIMIT 1
  Task Create (0.7ms)  INSERT INTO `tasks` (`id`, `project_id`, `name`, `created_at`, `updated_at`) VALUES ('f6c996b1-6523-4bf8-8fc3-2d4aa5110c42', 'cb3cd510-b631-49df-bb5b-a8984b6496e0', 'タスク-A', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Task::HABTM_Members Create (0.6ms)  INSERT INTO `members_tasks` (`member_id`, `task_id`, `created_at`, `updated_at`) VALUES ('ba74e512-6924-42c4-8bd5-c69bdb0b837f', 'f6c996b1-6523-4bf8-8fc3-2d4aa5110c42', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Task Exists (1.0ms)  SELECT  1 AS one FROM `tasks` WHERE `tasks`.`id` = '9b133316-1d82-4489-8eb2-e1db7611c842' LIMIT 1
  Task Create (0.9ms)  INSERT INTO `tasks` (`id`, `project_id`, `name`, `created_at`, `updated_at`) VALUES ('9b133316-1d82-4489-8eb2-e1db7611c842', 'cb3cd510-b631-49df-bb5b-a8984b6496e0', 'タスク-B', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Task::HABTM_Members Create (0.6ms)  INSERT INTO `members_tasks` (`member_id`, `task_id`, `created_at`, `updated_at`) VALUES ('ba74e512-6924-42c4-8bd5-c69bdb0b837f', '9b133316-1d82-4489-8eb2-e1db7611c842', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Member Exists (0.5ms)  SELECT  1 AS one FROM `members` WHERE `members`.`id` = '3db54957-bcf9-44b9-b033-aaa15a79cb8c' LIMIT 1
  Member Create (0.7ms)  INSERT INTO `members` (`id`, `name`, `created_at`, `updated_at`) VALUES ('3db54957-bcf9-44b9-b033-aaa15a79cb8c', 'メンバー②', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Task::HABTM_Members Create (0.6ms)  INSERT INTO `members_tasks` (`member_id`, `task_id`, `created_at`, `updated_at`) VALUES ('3db54957-bcf9-44b9-b033-aaa15a79cb8c', '9b133316-1d82-4489-8eb2-e1db7611c842', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
   (1.0ms)  COMMIT
   (0.3ms)  BEGIN
   (0.2ms)  COMMIT
  Project Load (0.5ms)  SELECT  `projects`.* FROM `projects` WHERE `projects`.`id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0' LIMIT 1
#<Project:0x000056398a8b82a0
 id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
 name: "プロジェクト①",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00,
 updated_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00>
  Task Load (0.5ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`project_id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0'
[#<Task:0x0000563989984fe0
  id: "9b133316-1d82-4489-8eb2-e1db7611c842",
  project_id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
  name: "タスク-B",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00>,
 #<Task:0x0000563989984630
  id: "f6c996b1-6523-4bf8-8fc3-2d4aa5110c42",
  project_id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
  name: "タスク-A",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00>]
  Member Load (0.5ms)  SELECT `members`.* FROM `members` INNER JOIN `members_tasks` ON `members`.`id` = `members_tasks`.`member_id` WHERE `members_tasks`.`task_id` = 'f6c996b1-6523-4bf8-8fc3-2d4aa5110c42'
[#<Member:0x0000563989a28ed8
  id: "ba74e512-6924-42c4-8bd5-c69bdb0b837f",
  name: "メンバー①",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00>]
=> nil


では、MySQL 8.1.15 なら?

MySQLでもデフォルト値に関数を設定して、データベース側でUUIDを生成できれば、Ruby側でゴニョらなくてもいいのにと思っていたら、MySQL 8.0.13で、カラム定義のDEFAULTに関数が指定できるようになったようだ。

で、8系の最新バージョンのMySQL 8.0.15でも試してみた。

マイグレーションは次の通り。

class CreateProjects < ActiveRecord::Migration[5.2]
  def change
    create_table :projects, id: false, comment: 'プロジェクト' do |t|
      t.string :id, limit: 36, null: false, primary_key: true, default: ->{"(uuid())"}, comment: 'プライマリキー'
      t.string :name, null: false, comment: 'プロジェクト名'
      t.text :description, comment: '概要'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end

default: ->{"(uuid())"}でカラムのデフォルトにuuid()関数を設定している。
これで作られるテーブルは次の通り。ExtraがDEFAULT_GENERATEDとなっていてよくわからないが、uuid()関数が設定されている。

mysql> desc projects;
+-------------+--------------+------+-----+---------+-------------------+
| Field       | Type         | Null | Key | Default | Extra             |
+-------------+--------------+------+-----+---------+-------------------+
| id          | varchar(36)  | NO   | PRI | NULL    | DEFAULT_GENERATED |
| name        | varchar(255) | NO   |     | NULL    |                   |
| description | text         | YES  |     | NULL    |                   |
| start_on    | date         | YES  |     | NULL    |                   |
| end_on      | date         | YES  |     | NULL    |                   |
| created_at  | datetime     | NO   |     | NULL    |                   |
| updated_at  | datetime     | NO   |     | NULL    |                   |
+-------------+--------------+------+-----+---------+-------------------+
7 rows in set (0.00 sec)

モデルクラスの実装は次の様にidの生成をRuby側では行わないようにした。

class Project < ApplicationRecord
end

動作確認

TaskMemberも同様に実装して動作を確認してみた。

irb(main):001:0> project = Project.create(name: 'プロジェクト')
   (0.8ms)  SET NAMES utf8mb4 COLLATE utf8mb4_general_ci,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (0.5ms)  BEGIN
  Project Create (1.6ms)  INSERT INTO `projects` (`name`, `created_at`, `updated_at`) VALUES ('プロジェクト', '2019-02-25 15:35:50', '2019-02-25 15:35:50')
   (4.1ms)  COMMIT
=> #<Project id: "0", name: "プロジェクト", description: nil, start_on: nil, end_on: nil, created_at: "2019-02-25 15:35:50", updated_at: "2019-02-25 15:35:50">
irb(main):002:0> pp project
#<Project:0x00005644fe35f860
 id: "0",
 name: "プロジェクト",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Mon, 25 Feb 2019 15:35:50 UTC +00:00,
 updated_at: Mon, 25 Feb 2019 15:35:50 UTC +00:00>
=> #<Project id: "0", name: "プロジェクト", description: nil, start_on: nil, end_on: nil, created_at: "2019-02-25 15:35:50", updated_at: "2019-02-25 15:35:50">
irb(main):003:0> Project.all.ids
   (0.8ms)  SELECT `projects`.`id` FROM `projects`
=> ["077cacaa-3913-11e9-aacc-0242ac1c0002"]

Projectcreateしてみると、確かに登録されているのだが、id: "0"となっている。
しかし、DBにはちゃんとUUIDで保存されている。

先程のサンプルコードも実行してみると次の様にエラーとなる。

irb(main):016:0> sample
  Task Destroy (0.8ms)  DELETE FROM `tasks`
  Member Destroy (0.8ms)  DELETE FROM `members`
  Project Destroy (4.9ms)  DELETE FROM `projects`
   (0.4ms)  BEGIN
  Project Create (0.7ms)  INSERT INTO `projects` (`name`, `created_at`, `updated_at`) VALUES ('プロジェクト①', '2019-02-26 13:17:54', '2019-02-26 13:17:54')
   (3.2ms)  COMMIT
   (0.4ms)  BEGIN
  Member Create (0.8ms)  INSERT INTO `members` (`name`, `created_at`, `updated_at`) VALUES ('メンバー①', '2019-02-26 13:17:54', '2019-02-26 13:17:54')
  Member Load (0.5ms)  SELECT  `members`.* FROM `members` WHERE `members`.`id` = '0' LIMIT 1
   (1.2ms)  ROLLBACK
Traceback (most recent call last):
        2: from (irb):16
        1: from app/models/member.rb:2:in `block in <class:Member>'
ActiveRecord::RecordNotFound (Couldn't find Member with 'id'=0)

どうもcreateした後のインスタンスにIDがセットされない。
そのため、そのインスタンスをviewでそのまま使おうとするとおかしくなると思う。
上記のようなnewしただけのオブジェクトで関連させて、いっぺんにsaveするケースやパスヘルパー(edit_project_pathなど)の引数に使うなど。
そして、idがわからないのでreloadもできない。

この様な場合でも、create時にDB側で生成される値をsavecreate後のオブジェクトに反映する方法はないのだろうか…。

UUIDプライマリキーの使いどころ

プライマリキーにUUIDを使うことのデメリットは次が考えられる

  • データサイズが大きくなる
  • リソースのIDとしてパスにUUIDを使った場合にURLが長くなる
    • 階層的なリソースの場合、辛い長さになると思う
  • 調査等で直接SQLを叩く場合に面倒くさい

では、UUIDプライマリキーはどういうときに使うとよいのだろうか?

  • データの登録数を推測されたくないケース
    • ECサイトなどで購入IDなどに連番を使うと購入件数などが推測されてしまう。それを避けたい場合。
  • サイトをクロールされたくないケース
    • URLで連番を晒していると、順にクロールしやすい
  • 整数型の最大値以上のレコード数が見込まれるケース
    • まあ、そんなにたくさんのレコードが1テーブルに入ってしまうこと自体を避けるべきかと思うが1

参考


  1. たとえ古いレコードを削除してもIDの使い回しはちょっと気持ち悪いしなぁ 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む