- 投稿日:2019-02-27T19:45:52+09:00
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
- 投稿日:2019-02-27T10:14:56+09:00
PHPでMySQLに接続できない場合の対応
MySQLに接続しようとしたらPDOExceptionが発生した。
PHP Fatal error: Uncaught PDOException: could not find driver in hoge.php:1ドライバが見つからないと言っているので、この場合は
php-mysql
を入れます。sudo apt install php-mysqlおわり
- 投稿日:2019-02-27T09:06:38+09:00
外部サーバーの 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 -pMySQL サーバーの接続ホスト名を
0.0.0.0
や172.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_config
のAllowTcpForwarding
ディレクティブが影響するよう。grep ^AllowTcpForwarding /etc/ssh/sshd_config
AllowTcpForwarding noDocker が動作しているホスト OS の
sshd_config
のAllowTcpForwarding
の値がno
になっていた。
AllowTcpForwarding
は TCP 転送を許可するかどうかを指定する。 1sed -i '/^AllowTcpForwarding/ s/no/yes/' /etc/ssh/sshd_config systemctl restart sshd
AllowTcpForwarding
の値をyes
に変更して sshd を再起動したところ、無事に Sequel Pro で Docker コンテナの MySQL サーバーに接続することができた。
- 投稿日:2019-02-27T00:51:35+09:00
Docker + Rails + MySQL + Vue で Todo アプリ作成 〜その1〜
はじめに
前回: Docker + Rails で開発環境を作った。
せっかく作ったのに開発しないのはもったいないよね。
でもできることって限られてる。
ということで、TODOアプリを作ってみる目標
- trello
- 今回はとりあえず、seed データを投入して、その一覧を表示させるとこまで。
- タイトル詐欺してますね、「その1」では Vue は使いません(そこまでいけない)
1.準備
Docker を起動し、コンテナに接続していきます。
$ cd myapp $ docker-compose up $ docker ps
myapp_web
とmysql
のCONTAINER ID
を控える
そぞれのコンテナに接続をする。web$ docker exec -it CONTAINER_ID bash root@CONTAINER_ID:/myapp# これでOKdbdocker exec -it CONTAINER_ID bash root@CONTAINER_ID:/# mysql -u root -p Enter password: # database.yml に記載したパスワード mysql> これでOK2. Rails の操作
2.1 モデルを作る
Task モデルを作る。
web# rails g model Task title:string context:string level:integer # rails db:migrate2.2 コントローラーも作る
こっちは Tasks
複数形であることに注目、Rails Tutorial で確かそう習った# rails g controller Tasks
とりあえず一覧を見たい
tasks_controller.rbclass 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.rbTask.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 で実際にデータが入っているか確認する。dbmysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | myapp_development | # こいつを使うよ | myapp_test | | mysql | | performance_schema | | sys | +--------------------+終わったらコンテナごと
exit
しちゃう。3. View とその準備
さっき
rails db:seed
で作成したデータを表示させたい。
表示させるページは/tasks
と/
(ルートページ)3.1 ルーティング
routes.rbRails.application.routes.draw do root to: 'tasks#index' resources :tasks end3.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.erbindex.html.erb<h1>Your Tasks</h1> <ul> <% @tasks.each do |task| %> <li><%= task.title %><%= task.context %></li> <% end %> </ul>
Task.all
の数だけ Task の持っている title と context を表示...なんかだんだんモデルの作りがイケてない気がしてきた。
そしてローカルホストに接続して確認。
参考
Railsでタスク管理ができるWebアプリを作成してみた(Rails入門)
【Ruby on Rails】ToDoアプリを簡単に作ってみるありがとうございます。
- 投稿日:2019-02-27T00:36:39+09:00
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で現状使えるバージョンに近しいものを選択した。
検証モデル
次のようなプロジェクトごとにタスクを登録し、それぞれのタスクにメンバーをアサインするモデルを考える。
次のような関連を持っている。
Project
とTask
は 一対多Task
とMember
は 多対多(タスクには複数人をアサイン可能)PostgreSQLの場合
PostgreSQLはUUID型があるのでプライマリキーの型をUUIDにする。
マイグレーションファイル
各モデルともIDはUUIDとする。
最初のテーブルprojects
を作成する前にenable_extension 'pgcrypto'
を実行してPostgreSQLの拡張を有効化する。db/migrate/xxxxxxxxxxxxxx_create_projects.rbclass 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.rbclass 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_tasks
はid: false
としている。db/migrate/xxxxxxxxxxxxxx_create_member.rbclass 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.rbclass Project < ApplicationRecord has_many :tasks endapp/models/task.rbclass Task < ApplicationRecord belongs_to :project has_and_belongs_to_many :members enum status: { waiting: 0, doing: 1, done: 2, cancel: -1 } endapp/models/member.rbclass 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
型なんて存在しないので、id
をstring
にして、UUIDを格納する。
UUIDは36文字なのでlimit: 36
を設定するために一旦、id: false
として、primary_key: true
となるカラムを定義している。
varchar(255)
になるのを気にしないなら、create_table
にid: :string
を指定してもいいかと思う。rubyclass 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
を指定している。rubyclass 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
members
とmembers_tasks
も同様。rubyclass 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.rbclass Project < ApplicationRecord include IdGenerator has_many :tasks endapp/models/task.rbclass Task < ApplicationRecord include IdGenerator belongs_to :project has_and_belongs_to_many :members enum status: { waiting: 0, doing: 1, done: 2, cancel: -1 } endapp/models/member.rbclass Member < ApplicationRecord include IdGenerator has_and_belongs_to_many :tasks end各モデルクラスの実装はPostgreSQLの場合とほぼ変わらない。
include IdGenerator
を除いては。何をインクルードしているかというと次のモジュール。
app/models/concerns/id_generator.rbmodule 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 endPostgreSQLではテーブル定義で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動作確認
Task
、Member
も同様に実装して動作を確認してみた。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"]
Project
をcreate
してみると、確かに登録されているのだが、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側で生成される値をsave
やcreate
後のオブジェクトに反映する方法はないのだろうか…。UUIDプライマリキーの使いどころ
プライマリキーにUUIDを使うことのデメリットは次が考えられる
- データサイズが大きくなる
- リソースのIDとしてパスにUUIDを使った場合にURLが長くなる
- 階層的なリソースの場合、辛い長さになると思う
- 調査等で直接SQLを叩く場合に面倒くさい
では、UUIDプライマリキーはどういうときに使うとよいのだろうか?
- データの登録数を推測されたくないケース
- ECサイトなどで購入IDなどに連番を使うと購入件数などが推測されてしまう。それを避けたい場合。
- サイトをクロールされたくないケース
- URLで連番を晒していると、順にクロールしやすい
- 整数型の最大値以上のレコード数が見込まれるケース
- まあ、そんなにたくさんのレコードが1テーブルに入ってしまうこと自体を避けるべきかと思うが1。
参考
- Active Record と PostgreSQL - Rails ガイド
- DockerでRails5.2.1とMySQL8を動かすgitプロジェクトを作る方法 : 試行錯誤な日々
- Railsでuuidを取り扱う時のTips - Qiita
- 日々の覚書: MySQL 8.0.13でカラム定義のDEFAULTに関数が指定できるようになった
たとえ古いレコードを削除してもIDの使い回しはちょっと気持ち悪いしなぁ ↩