20210226のMySQLに関する記事は11件です。

MacbookにRuby導入

環境

MacOS:Catalina
MacbookPro初期の状態からRubyRailsMySQLをインストールします。

手順は以下です。
・Zshをデフォルトに
・Command line tools導入
・Homebrew導入
・Rubyインストール
・MySQLを導入
・Railsを導入
・Node.jsの導入
・yarnの導入

Zshをデフォルトに

# zshをデフォルトに設定
% chsh -s /bin/zsh

# ログインシェルを表示
% echo $SHELL
# 以下のように表示されれば成功
/bin/zsh

パスワード求められたらPCのパスワードを入力します。

Command line tools導入

以下をターミナルで

% xcode-select --install

インストールをクリックして進める。

Homebrew導入

Homebrewというソフトウェア管理ツールを導入します。
以下をターミナルで。

% cd  # ホームディレクトリに移動
% pwd  # ホームディレクトリにいるかどうか確認
% /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" # コマンドを実行

PCパスワードを求められます。
途中エンターキー入力あります。

以下でインストールできたか確認。

% brew -v

最新の状態にします。

% brew update

Homebrewの権限変更

%  sudo chown -R `whoami`:admin /usr/local/bin

Rubyインストール

Rubyの土台となるrbenvとruby-buildを、Homebrewを用いてインストール

% brew install rbenv ruby-build

rbenvをどこからも使用できるようにする。

% echo 'eval "$(rbenv init -)"' >> ~/.zshrc

zshrcの変更を反映

% source ~/.zshrc

ターミナルのirb上で日本語入力を可能にする設定を行うために、以下のコマンドでインストール

% brew install readline

すでにインストール済と出る場合あります。

readlineをどこからも使用できるようにします。

% brew link readline --force

rbenvを利用してRubyをインストール。

% RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)"
% rbenv install 2.6.5

時間がかかるコマンドです。

rbenvを読み込んで変更を反映。

% rbenv rehash

Rubyのバーションを確認。

% ruby -v

MySQLを導入

% brew install mysql@5.6

MySQLの自動起動設定。
MySQLは本来であればPC再起動のたびに起動し直す必要がありますが、それは面倒であるため、自動で起動するように。

% mkdir ~/Library/LaunchAgents 
% ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents
% launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plist 

mysqlコマンドをどこからでも実行できるように。

% echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.zshrc # mysqlのコマンドを実行できるようにする設定
% source ~/.zshrc #  設定を読み込むコマンド
% which mysql # mysqlのコマンドが打てるか確認する

# 以下のように表示されれば成功
/usr/local/opt/mysql@5.6/bin/mysql

MySQLの起動を確認。

% mysql.server status # MySQLの状態を確認するコマンド

# 以下のように表示されれば成功
 SUCCESS! MySQL running

Railsを導入

Rubyの拡張機能(gem)を管理するためのbundler(バンドラー)をインストールします。

% gem install bundler --version='2.1.4'

Railsをインストール。

% gem install rails --version='6.0.0'

処理に時間かかります。

rbenvを再読み込み。

% rbenv rehash

Railsが導入できたか確認。

% rails -v
Rails 6.0.0  # 「Rails」のあとに続く数字は変わる可能性があります

Node.jsの導入

Railsを動かすためにはNode.jsが必要となり、それをHomebrewを用いてインストールします。

% brew install node@14

Warningでても問題ないです。

Node.jsへのパスを設定。

% echo 'export PATH="/usr/local/opt/node@14/bin:$PATH"' >> ~/.zshrc
% source ~/.zshrc

Node.jsが導入できたか確認。

% node -v
v14.15.3 # 数値は異なる場合があります

yarnの導入

yarnのインストール。

% brew install yarn

yarnが導入できたか確認。

% yarn -v

以上で、Railsでアプリ作成ができる環境が整いました。

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

[Node.js] MySQLのIN句をプレースホルダーで表す

概要

Node.jsでMySQLを扱うにあたり、
IN句のリスト部分をプレースホルダーで表すには、どのようにすれば良いかを以下に記していきます。

本題の前に...

(本題しか興味ない人は、すっ飛ばしてください)

Node.jsでMySQLを扱うためにインストール

npm i -S mysql promise-mysql

Node.jsでMySQLプレースホルダーを扱うときの基本

例えば、プレースホルダーなしで書こうとすると以下のようになります。(良くない例です)

【NG例】プレースホルダーなし
const userid = 1 ;
const sql = `SELECT * FROM users WHERE userid = ${userid} ;`

このままだと、SQLインジェクションの格好の餌食となってしまうため、
プレースホルダーを使って書き直します。

【OK例】プレースホルダーあり
const userid = 1;
const sql = `SELECT * FROM users WHERE userid = ? ;`
const res = await conn.query(sql, userid);
//この場合、
//SELECT * FROM users WHERE userid = 1;
//というクエリを投げたことになる

変数が2つ以上になる場合は、

【OK例】変数が2つ以上の場合
const userid = 1;
const classname = 'G';
const sql = `SELECT * FROM users WHERE userid = ? AND classname = ? ;`
const res = await conn.query(sql, [userid, classname] );
//この場合、
//SELECT * FROM users WHERE userid = 1 AND classname = 'G';
//というクエリを投げたことになる

のように、配列に変数を格納することで表すことができます。

本題「IN句のリストをプレースホルダで表したい」

以下のように書くことで、IN句もプレースホルダーを用いて表すことができます。

IN句でプレースホルダーを使う
const sql = `SELECT * FROM users WHERE userid NOT IN (?) ;`
const excludedId = [1, 3, 4];
const res = await conn.query(sql, [excludedId] );

これで、userid = 1, 3, 4 以外の行を抽出することができました。

ちょっとだけ解説

複数の変数がある場合、その変数の個数分「?」を用意する必要があると上述しました。
今回の場合、

リストを?で表す
const excludedId = [1, 3, 4];
const sql = `SELECT * FROM users WHERE userid NOT IN (?,?,?) ;`

のように書かれていてほしいわけです。
しかしながら、状況によって
?の個数が3つから4つ、5つと変動していくことも考えられ、上記のように書くのは現実的ではありません。

そこで、
「1,3,4をそれぞれ変数として見る」のではなく、
「[1,3,4]という配列を、1つの変数として捉える」ことで解決できます。
リストに渡したい配列さえあれば、その配列を引数に渡すだけで
簡単にクエリを作ることが出来ました。

注意点

ちなみに、以下のやり方ではエラーが出ます。

【NG例】IN句でプレースホルダーを使う
const sql = `SELECT * FROM users WHERE userid NOT IN (?) ;`
const excludedId = [1, 3, 4];
const res = await conn.query(sql, excludedId);

複数の変数が出てくる場合は
[userid, classname]のように配列に変数を格納して、渡しますよね。

しかし今回の場合、[1,3,4]という配列を一つの変数として扱いたいので、
const res = await conn.query(sql, excludedId );
のように配列を渡してしまうと、
その中の要素である、1,3,4という3つの数値が、変数として見られてしまいます。

そのため必ず、配列を配列で括って書くようにしましょう。

余談「Node.jsでクエリを確認するためには」

プレースホルダーで書いていると、
実際、自分の思ったところに正しい変数が入っているか不安になることもあると思います。
そこで、以下のような書き方をすると
実際に投げられているクエリを確認することができ、大変便利です。

クエリ確認
const excludedId = [1, 3, 4];
const sql = mysql.format(`select * from users where userid NOT IN (?) ;`, [excludeId] );
console.log(sql); //select * from users where userid NOT IN (1,3,4) ;

終わりに

IN句のリストをプレースホルダーで表している例があまりなかったので
先輩の助けを借り、今回記事にしてみました。
参考になれば幸いです。

参考

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

rails db:createでエラー解決 Access denied for user 'root'@'localhost' (using password: YES)

rails db:create をすると以下のようなエラーが

% rails db:create
Access denied for user 'root'@'localhost' (using password: NO)
Couldn't create 'TodoApp_development' database. Please check your configuration.
rails aborted!
Mysql2::Error::ConnectionError: Access denied for user 'root'@'localhost' (using password: NO)

database.ymlとmysqlのパスワードが一致していないことが原因らしい
https://qiita.com/naota7118/items/b62d71484e21d6739d68

前回の記事で設定したmysqlのパスワードを
database.ymlに設定し直した

成功

% rails db:create
Created database 'TodoApp_development'
Created database 'TodoApp_test'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mysqlエラー対処忘備録(rootへの権限付与)

db環境設定で最も基本的なアクセス権限の付与について 

~ % mysql -u root
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)

~ % mysql -u root -p
Enter password: 
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

rootにアクセス権限がないようだ...

https://qiita.com/yummy888/items/25621bc1451f218e010a
この記事を参考にしよう。

 % mysql.server stop 
Shutting down MySQL
.. SUCCESS! 

~ % mysqld_safe --skip-grant-tables
2021-02-26T03:02:12.6NZ mysqld_safe Logging to '/usr/local/var/mysql/MacBook-Pro.local.err'.
2021-02-26T03:02:13.6NZ mysqld_safe A mysqld process already exists

...。mysqld_safe A mysqld process already existsとはなんぞや?

この記事を参考にしよう。
mysqld_safe A mysqld process already existsの対処法【MySQL】
https://yaba-blog.com/mysqld_safe-a-mysqld-process-already-exists-mysql/

mysql.server stop だとMySQLが勝手に再起動するらしい
mysql.server statusで確認。

~ % mysql.server stop 
Shutting down MySQL
.. SUCCESS! 
~ % mysql.server status
 SUCCESS! MySQL running (53818)

たしかに、再起動されている。

~ % brew services stop mysql
Stopping `mysql`... (might take a while)
==> Successfully stopped `mysql` (label: homebrew.mxcl.mysql)

再度こちらの記事に戻るhttps://qiita.com/yummy888/items/25621bc1451f218e010a#%E6%A8%A9%E9%99%90%E7%A2%BA%E8%AA%8D

権限なしでアクセス

 ~ % mysqld_safe --skip-grant-tables
2021-02-26T03:22:53.6NZ mysqld_safe Logging to '/usr/local/var/mysql/MacBook-Pro.local.err'.
2021-02-26T03:22:53.6NZ mysqld_safe Starting mysqld daemon with databases from /usr/local/var/mysql

成功

mysqlへ接続

~ %  mysql -u root
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 8.0.22 Homebrew

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 

-以下記事通り

権限確認

Userテーブル削除

rootユーザーを作成し、権限付与

mysqld_sefe プロセス終了
ps
-> 現在実行されているプロセス一覧表示

KILL (PID)
-> プロセス終了

~ % mysql.server start
Starting MySQL
. SUCCESS! 
yuto@MacBook-Pro ~ %  mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.22 Homebrew

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 

またrootアカウントで作業するのは抵抗がある場合MySQLユーザーの作成を行います。
セキュリティ設定(rootのパスワード変更等)

~%mysql_secure_installation

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

Equalumアップデート検証をやってみた。。

今回はEqualumのアップデート検証を行ってみます。

以前検証報告をさせて頂いた、kafka+SPARK環境を独自のノウハウと技術で統合し、先進のCDCエージェント技術を駆使してエンド・ツゥ・エンドでのExactly Onceを提供するEqualumなるソリューションに関して、最近大きなアップデートが有りましたので、取り急ぎ検証と結果共有をさせて頂きます。

何が変わった??

今回のバージョンでは、今までOracleのデータベース対応だけだったレプリケーション機能が、MySQL等の他のCDC対応データベースでの利用が可能になっています。また、以前のバージョンで対応していたOracle環境同様に、オリジナルの選択肢が単に増えただけではなく、レプリケーションの着地側データベースの選択肢も選べる様になっていますので、データシステム全体のコストバランスを考えた、より適切なシステム展開をサポートする事が可能です。

では、レプリケーション機能を使ってみます。

今回の環境としては、

スクリーンショット 2021-02-25 11.51.38.png

の構成を作ります。

まずはデータの生成部分を作ります。

ここは、過去何度も使い回しているPython環境を”得意のバラック改造”してサクッと作ります。今回の環境も常設のPython3+AnacondaをMBP上で使います。

最初に空のテーブルを3種類作成します

* 顧客系
* 取引系
* 決済系
ここも、エイヤー!でパラメータ定義をしていますので、気になられる方は適宜書き換えてください(汗)。今回は、共通キー的なカラムとして、全体を透過的に共通化しているオーダーID(Order_ID)を入れています。

# coding: utf-8
#
# MySQLにオリジナル側のCDCテーブルを作成
#
# Python3版
#

#  初期設定
import sys
stdout = sys.stdout
sys.stdout = stdout

import pymysql.cursors

# テーブル定義 
# ヘッダー系情報
DC0 = "id INT AUTO_INCREMENT, ts TIMESTAMP(6), PRIMARY KEY(id, ts), Order_ID VARCHAR(15), "
# 顧客情報
DC1 = "User VARCHAR(20), Zip VARCHAR(10), Prefecture VARCHAR(10), Address VARCHAR(60), "
DC2 = "Tel VARCHAR(15), Email VARCHAR(40), Point INT, Area VARCHAR(10)"
# 取引系情報
DC3 = "Category VARCHAR(20), Product VARCHAR(20), Price INT, Units INT, Logistics VARCHAR(20) "
# 決済系情報
DC4 = "User VARCHAR(20), Card VARCHAR(40), Number VARCHAR(30), Price INT, Units INT, Payment INT, Tax INT "
# テーブル名
Table_Name = ["Rep_Demo_Usr_Table","Rep_Demo_Biz_Table","Rep_Demo_Pay_Table"]
# 作成するテーブル数
Generate_Table = 3

try:

    print("オリジナル側CDCテーブル作成処理を開始")

    # テーブル定義の初期化
    Table_Create = []
    Create_SQL = "CREATE TABLE IF NOT EXISTS " + Table_Name[0] + " ("+DC0+DC1+DC2+")"  
    Table_Create.append(Create_SQL)    
    Create_SQL = "CREATE TABLE IF NOT EXISTS " + Table_Name[1]+ " ("+DC0+DC3+")"
    Table_Create.append(Create_SQL)
    Create_SQL = "CREATE TABLE IF NOT EXISTS " + Table_Name[2] + " ("+DC0+DC4+")" 
    Table_Create.append(Create_SQL)

    # MySQLとの接続
    db = pymysql.connect(host = 'xxx.xxx.xxx.xxx',
                         port=3306,                
                         user='xxxxxxxx',
                         password='zzzzzzzz',
                         db='xxxxxxxx',        
                         charset='utf8mb4',
                         cursorclass=pymysql.cursors.DictCursor)

    with db.cursor() as cursor:

        Counter = 0

        while Counter < Generate_Table:

            # テーブルの初期化
            cursor.execute("DROP TABLE IF EXISTS " + Table_Name[Counter])
            db.commit()

            # 新規テーブルを作成
            cursor.execute(Table_Create[Counter])    
            db.commit()

            Counter = Counter + 1

except KeyboardInterrupt:

    print('!!!!! 割り込み発生 !!!!!')

finally:

    # データベースコネクションを閉じる
    db.close()

    print("オリジナル側CDCテーブル作成処理が終了")

次にデータを連続挿入する仕組みを作ります。

顧客系に関しては、実際の取引とは関係ない「単純な顧客登録のみ」が時々入ってくる様にしました。またこの割合は

if str(fakegen.boolean(40)) == "True":

の部分で40の値を変える事で可能です(詳しくは、Pythonのマニュアルにて・・(汗))
また、リアル感を追加する為に、ランダムな暇つぶし処理も入っていますので、これも適宜書き換えて頂いて結構です。
(因みに、Equalum的には一番ドSな待ち時間無しの設定でも普通に処理します・・・・・)

# coding: utf-8
#
# MYSQLの各CDCテーブルをランダムに更新する
#
# Python3版
#

#  初期設定
import sys
stdout = sys.stdout
sys.stdout = stdout

import time
import pymysql.cursors
import re

# デモで使うメタデータ定義
Category_Name = ["酒類","家電","書籍","DVD/CD","雑貨"]

Product_Name0 = ["日本酒","バーボン","ビール","芋焼酎","赤ワイン","白ワイン","スコッチ","ブランデー","泡盛","テキーラ"]
Product_Price0 = [1980, 2500, 490, 2000, 3000, 2500, 3500, 5000, 1980, 2000]    

Product_Name1 = ["テレビ","洗濯機","ラジオ","ステレオ","電子レンジ","パソコン","電池","エアコン","乾燥機","掃除機"]
Product_Price1 = [49800, 39800, 2980, 88000, 29800, 64800, 198, 64800, 35800, 24800]    

Product_Name2 = ["週刊誌","歴史","写真集","漫画","参考書","フィクション","経済","自己啓発","月刊誌","新刊"]
Product_Price2 = [280, 1500, 2500, 570, 1480, 1400, 1800, 1540, 980, 1980]    

Product_Name3 = ["洋楽","演歌","Jポップ","洋画","アイドル","クラッシック","邦画","連続ドラマ","企画","アニメ"]
Product_Price3 = [1980, 2200, 2500, 3500, 2980, 1980, 3800, 2690, 1980, 2400]    

Product_Name4 = ["洗剤","電球","贈答品","医薬部外品","ペットフード","乾電池","文房具","男性用品","女性用品","季節用品"]
Product_Price4 = [498, 198, 1980, 398, 980, 248, 398, 2980, 3580, 1980]

Table_Name1 = "Rep_Demo_Usr_Table" # 顧客管理系テーブル
Table_Name2 = "Rep_Demo_Biz_Table" # 販売・物流系テーブル
Table_Name3 = "Rep_Demo_Pay_Table" # 決済系テーブル

# 地域名ルックアップ情報(キーは都道府県名)
Area_Data={'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北','山形県':'東北','福島県':'東北',
           '茨城県':'関東','栃木県':'関東','群馬県':'関東','埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東',
           '新潟県':'中部','富山県':'中部','石川県':'中部','福井県':'中部','山梨県':'中部','長野県':'中部','岐阜県':'中部','静岡県':'中部','愛知県':'中部',
           '三重県':'近畿','滋賀県':'近畿','京都府':'近畿','大阪府':'近畿','兵庫県':'近畿','奈良県':'近畿','和歌山県':'近畿',
           '鳥取県':'中国','島根県':'中国','岡山県':'中国','広島県':'中国','山口県':'中国',
           '徳島県':'四国','香川県':'四国','愛媛県':'四国','高知県':'四国',
           '福岡県':'九州・沖縄','佐賀県':'九州・沖縄','長崎県':'九州・沖縄','熊本県':'九州・沖縄','大分県':'九州・沖縄','宮崎県':'九州・沖縄','鹿児島県':'九州・沖縄','沖縄県':'九州・沖縄'}

# 物流センタールックアップ情報(キーは地域名)
Logi_Data = {'北海道':'道央物流センター','東北':'東北物流センター','関東':'関東中央物流センター',
             '中部':'甲州物流センター','近畿':'伊丹物流センター','中国':'広島臨港物流センター','四国':'讃岐物流センター','九州・沖縄':'平戸物流センター'}

# 購入ポイント情報(カテゴリ名の順番に設定
Point_Data = [0.02, 0.1, 0.03, 0.02, 0.05]

# 消費税率の設定
Tax_Data = 0.1

# 生成するデータの総数
Loop_Count = 10

# タイミング調整フラグ (0:待ち時間無し 1:1秒おき 2:ランダム)
Wait_Flag = 2

# 一定間隔の場合(システム時間で秒単位)
Sleep_Wait = 1

# ランダム間隔の場合(実態に合わせて調整)
#Base_Count = 1600000
Base_Count = 800000

# SQL情報をコンソールに表示するか否かの設定(1:表示する)
Display_SQL = 0

# 書き込み用のデータカラム
DL0 = "Order_ID, "
DL1 = "User, Zip, Prefecture, Address, Tel, Email, Point, Area"
DL2 = "Category, Product, Price, Units, Logistics"
DL3 = "User, Card, Number, Price, Units, Payment, Tax"

def Add2Pre(Address_Data):

    # 都道府県情報の抽出
    pattern = u"東京都|北海道|(?:京都|大阪)府|.{2,3}県"
    m = re.match(pattern , Address_Data)
    if m:
        Prefecture_Name = m.group()

    return(Prefecture_Name)

try:

    print("テーブルへ連続挿入を開始")

    # Fakerの初期化
    from faker import Faker
    fakegen = Faker('ja_JP')    
    Faker.seed(fakegen.random_digit())

    # その他の変数
    Counter = 0
    Work_Count = 1

    # MySQLとの接続
    db = pymysql.connect(host = 'xxx.xxx.xxx.xxx',
                         port=3306,                
                         user='xxxxxxxx',
                         password='zzzzzzzz',
                         db='xxxxxxxx',        
                         charset='utf8mb4',
                         cursorclass=pymysql.cursors.DictCursor)

    with db.cursor() as cursor:

        # 検証データの生成
        while Counter < Loop_Count:

            # ランダムに書き込む商材の種類を選択
            Category_ID = fakegen.random_digit()
            if Category_ID > 4: Category_ID = Category_ID - 5

            # カテゴリ名の設定  
            Category = Category_Name[Category_ID]
            # 商品IDの設定
            Product_ID = fakegen.random_digit()

            # オーダーIDの情報作成
            S_Counter = str(Counter)
            O_Counter = S_Counter.zfill(8)

            if Category_ID == 0:
                Product = Product_Name0[Product_ID]
                Price = Product_Price0[Product_ID]
                Units = fakegen.random_digit() + 1
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C00" + O_Counter

            elif Category_ID == 1:
                Product = Product_Name1[Product_ID]
                Price = Product_Price1[Product_ID]
                Units = 1
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C01" + O_Counter

            elif Category_ID == 2:
                Product = Product_Name2[Product_ID]
                Price = Product_Price2[Product_ID]
                Units = fakegen.random_digit() + 1
                if Units >3: Units = 3  
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C02" + O_Counter

            elif Category_ID == 3:
                Product = Product_Name3[Product_ID]
                Price = Product_Price3[Product_ID]
                Units = fakegen.random_digit() + 1
                if Units >2: Units = 2
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C03" + O_Counter

            else:
                Product = Product_Name4[Product_ID]
                Price = Product_Price4[Product_ID]
                Units = fakegen.random_digit() + 1
                if Units >4: Units = 4
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C04" + O_Counter

            # 顧客名
            User = fakegen.name()

            # 支払い情報
            if str(fakegen.pybool()) == "True":
                Card = "現金"
            else:
                Card = fakegen.credit_card_provider()

            Number = fakegen.credit_card_number()              
            if Card == "現金":    Number = "N/A"

            # 支払い総額と消費税
            Payment = Price * Units
            Tax = Payment * 0.1

            # 顧客情報の設定
            Zip = fakegen.zipcode()
            Address = fakegen.address()            
            Prefecture = Add2Pre(Address)           
            Tel = fakegen.phone_number()
            Email = fakegen.ascii_email()

            # 地域名と物流センター名を取得           
            Area = Area_Data.get(Prefecture)
            Logistics = Logi_Data.get(Area)

            # 購入処理系は随時全て更新し、顧客管理のみ時々顧客登録のみを実施する            
            if str(fakegen.boolean(40)) == "True":

                Point = 0 # 登録だけなのでポイントは無し

                # 顧客登録のみのコードを設定
                O_ID = "W99" + O_Counter

                DV0 = O_ID + "','"                
                DV1 = User + "','" + Zip + "','" + Prefecture + "','" + Address + "','" + Tel + "','" + str(Email) + "','" + str(Point) + "','" + Area
                SQL_Data1 = "INSERT INTO " + Table_Name1 + " (" + DL0 + DL1+ ") VALUES ('"+DV0 + DV1 + "')"

                # 顧客登録のみを処理
                cursor.execute(SQL_Data1)
                db.commit()

                #  コンソールに生成データを表示
                if Display_SQL == 1:
                    print (SQL_Data1) 

            else: # 通常の処理

                # 挿入データ用SQLの準備
                DV0 = O_ID + "','"
                DV1 = User + "','" + Zip + "','" + Prefecture + "','" + Address + "','" + Tel + "','" + str(Email) + "','" + str(Point) + "','" + Area
                DV2 = Category + "','" + Product + "','" + str(Price) + "','" + str(Units) + "','" + Logistics
                DV3 = User + "','" + Card + "','" + Number + "','" + str(Price) + "','" + str(Units) + "','" + str(Payment) + "','" + str(Tax)

                # SQLの作成
                SQL_Data1 = "INSERT INTO " + Table_Name1 + " (" + DL0 + DL1+ ") VALUES ('"+DV0 + DV1 + "')"
                SQL_Data2 = "INSERT INTO " + Table_Name2 + " (" + DL0 + DL2 + ") VALUES ('"+DV0 + DV2 + "')"
                SQL_Data3 = "INSERT INTO " + Table_Name3 + " (" + DL0 + DL3 + ") VALUES ('"+DV0 + DV3 + "')"

                # MySQLへ書き込む          
                cursor.execute(SQL_Data1)
                db.commit()

                cursor.execute(SQL_Data2)
                db.commit()

                cursor.execute(SQL_Data3)
                db.commit()

                #  コンソールに生成データを表示
                if Display_SQL == 1:
                    print (SQL_Data1)
                    print (SQL_Data2)
                    print (SQL_Data3)


            # 生成間隔の調整
            if Wait_Flag == 1:
                time.sleep(Sleep_Wait)
            elif Wait_Flag == 2:
                Wait_Loop = Base_Count * fakegen.random_digit() + 1
                for i in range(Wait_Loop): Work_Count = Work_Count + i

            # ループカウンタの更新
            Counter=Counter+1

            # データの作成状況を表示
            if (Counter % 10) == 0:     print("途中経過: " + str(Counter) + " 個目のデータ作成を終了")

except KeyboardInterrupt:

    print('!!!!! 割り込み発生 !!!!!')

finally:

    # データベースコネクションを閉じる
    db.close()

    print("生成したデータの総数 : " + str(Counter))    
    print("テーブルへ連続挿入を終了")

取り敢えずのテストを実施・・・

では、取り急ぎのテストを実施してみます。今回は仮想環境上にCDC設定済みのMySQLを立ち上げて、その上にレプリケーション検証用のデータベースを作成し、その中に3個のテーブルを作成します。

スクリーンショット 2021-02-25 15.31.53.png

無事にテーブルが出来ましたので、今度は連続挿入の処理を走らせてみます。ここではランダム感覚で10個のデータを生成・挿入します。

「販売・物流系テーブル」
スクリーンショット 2021-02-25 15.35.23.png
「決済系テーブル」
スクリーンショット 2021-02-25 15.36.10.png
「顧客系テーブル」
スクリーンショット 2021-02-25 15.39.53.png

上手くデータが分類されて、3種類のテーブルが出来ている事が確認出来ました。

いよいよEqualumの登場!

この画面は、以前検証報告させて頂いたEqualumと「殆ど同じ」になりますが、今回のレプリケーション機能の対応拡大に伴い、2箇所大きな追加・変更が有ります。
スクリーンショット 2021-02-25 15.49.51.png
まずは右上の部分
スクリーンショット 2021-02-25 15.53.07.png
左側のメニューバー部分
スクリーンショット 2021-02-25 15.56.48.png
になります。

レプリケーションの設定をしてみる。。。

まずは、左側のメニューバーからレプリケーションを選択します。
スクリーンショット 2021-02-25 15.59.59.png
+ADDボタンを選択します。
スクリーンショット 2021-02-25 16.01.34.png
あとは、上から順番に必要項目を選択・設定していけば終了です。
(1)グループ名を設定してソースとターゲット、その他の項目を設定
(2)Add Tablesの下に+ボタンが有るので選択します。
スクリーンショット 2021-02-25 16.04.57.png
(3)必要項目を選択・入力します。今回はRepxxxxxという形でテーブル名を統一していますので、Rep%と設定しました。この部分は、その他の省略形式もサポートしていますので、関連するテーブルを一気に選択・設定する際に便利な機能になります。
(4)Generate Listボタンを選択すると、先程の条件に適合するテーブルが自動的に選択されますので、問題がなければValidate Tableを選択して接続確認します。
スクリーンショット 2021-02-25 16.40.12.png
無事に整合性が取れていると判断された場合には、緑色のチェックマークが付きますのので、右下のCreateボタンを押して登録します。(万が一この様な警告が出た場合は、ターゲット側に同じ名前のテーブルが無いかどうかを確認してください。)
スクリーンショット 2021-02-25 16.38.56.png

では検証開始!

無事に設定・登録が完了すると、管理画面にEqualumが自動生成したFLOWとして3個のストリーミング・レプリケーションが確認出来ます。
スクリーンショット 2021-02-26 9.14.50.png
まずは、DBeaverを使ってスタートの状況を確認します。
スクリーンショット 2021-02-26 9.16.52.png
空っぽのテーブルが、先程のPythonスクリプトで作成され、この段階ではターゲットのSingleStore側には何も変化は有りませんが、先程の連続挿入スクリプトを走らせると、ターゲット側にテーブルが作られてデータがストリーミングでレプリケーションされます。
スクリーンショット 2021-02-26 9.19.09.png

念の為に、DBeaverで確認してみます。
ソース側のテーブル
スクリーンショット 2021-02-26 9.12.35.png
ターゲット側のテーブル
スクリーンショット 2021-02-26 9.11.27.png
無事にレプリケーションが行われている様です。

今回のまとめ

今回は、Equalumのレプリケーション機能についての簡単な動作検証を行いました。以前の記事を読まれた方で”勘の良い方”はピン!ときたかもしれませんが、Equalumの場合途中のFLOW処理を入れなければ、基本的にはレプリケーションと同じ”様な”動作になります。もちろん、上流側のソースDBがCDCストリーミングをサポートしていれば、”ほぼ同じ”様な動作を手組みで作る事も可能なのですが、現実的なレプリケーション環境を考えると、命名規則に則ったテーブル名設定や、それらのリレーションを保持する為にテーブルをグループ化して管理しなければならない・・といった要求仕様が出てきます。
今回Oracle環境以外に横展開を始めたEqualumのレプリケーション環境では、出来るだけ利用者の負担を減らす方向で、レプリケーションに合わせた機能追加などを行い、通常メニューとして利用出来る様になりました。
また、冒頭にも書かせて頂いた通り、同じデータベース同志でなければレプリケーション出来ない!という仕様でも有りませんので、用途やコスト見合いで柔軟に構成を選択する事が可能です。

さて次回は・・・・

次回は、今回検証出来なかった、レプリカ側に対してPythonを使った簡単な即時可視化の仕掛けを入れて、利活用を意識した検証を行いたいと思います。もちろん、シンプルなBCP対応!という事であれば、今回の検証で十分ご検討頂けるかと思いますが、逆に「取り敢えず現場から全部持ってきて、それから考える!(ETL的処理は会議室で考える!系)というアプローチも否定できませんので、(この辺は、Equalumが稼働するサーバ・クラスターの構成にも関係しますが・・・)興味本意半分以上ではありますが、可視化ツールの作成から作業を行ってみたいと思います。

謝辞

本検証は、SIngleStore社の公式Freeバージョン(V7.3)を利用して実施しています。
この貴重な機会を提供して頂いたSingleStore社に対して感謝の意を表すると共に、本内容とSingleStore社の公式ホームページで公開されている内容等が異なる場合は、SingleSTore社の情報が優先する事をご了解ください。

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

Equalumアップデート検証をやってみた:1

今回はEqualumのアップデート検証を行ってみます。

以前検証報告をさせて頂いた、kafka+SPARK環境を独自のノウハウと技術で統合し、先進のCDCエージェント技術を駆使してエンド・ツゥ・エンドでのExactly Onceを提供するEqualumなるソリューションに関して、最近大きなアップデートが有りましたので、取り急ぎ検証と結果共有をさせて頂きます。

何が変わった??

今回のバージョンでは、今までOracleのデータベース対応だけだったレプリケーション機能が、MySQL等の他のCDC対応データベースでの利用が可能になっています。また、以前のバージョンで対応していたOracle環境同様に、オリジナルの選択肢が単に増えただけではなく、レプリケーションの着地側データベースの選択肢も選べる様になっていますので、データシステム全体のコストバランスを考えた、より適切なシステム展開をサポートする事が可能です。

では、レプリケーション機能を使ってみます。

今回の環境としては、

スクリーンショット 2021-02-25 11.51.38.png

の構成を作ります。

まずはデータの生成部分を作ります。

ここは、過去何度も使い回しているPython環境を”得意のバラック改造”してサクッと作ります。今回の環境も常設のPython3+AnacondaをMBP上で使います。

最初に空のテーブルを3種類作成します

* 顧客系
* 取引系
* 決済系
ここも、エイヤー!でパラメータ定義をしていますので、気になられる方は適宜書き換えてください(汗)。今回は、共通キー的なカラムとして、全体を透過的に共通化しているオーダーID(Order_ID)を入れています。

# coding: utf-8
#
# MySQLにオリジナル側のCDCテーブルを作成
#
# Python3版
#

#  初期設定
import sys
stdout = sys.stdout
sys.stdout = stdout

import pymysql.cursors

# テーブル定義 
# ヘッダー系情報
DC0 = "id INT AUTO_INCREMENT, ts TIMESTAMP(6), PRIMARY KEY(id, ts), Order_ID VARCHAR(15), "
# 顧客情報
DC1 = "User VARCHAR(20), Zip VARCHAR(10), Prefecture VARCHAR(10), Address VARCHAR(60), "
DC2 = "Tel VARCHAR(15), Email VARCHAR(40), Point INT, Area VARCHAR(10)"
# 取引系情報
DC3 = "Category VARCHAR(20), Product VARCHAR(20), Price INT, Units INT, Logistics VARCHAR(20) "
# 決済系情報
DC4 = "User VARCHAR(20), Card VARCHAR(40), Number VARCHAR(30), Price INT, Units INT, Payment INT, Tax INT "
# テーブル名
Table_Name = ["Rep_Demo_Usr_Table","Rep_Demo_Biz_Table","Rep_Demo_Pay_Table"]
# 作成するテーブル数
Generate_Table = 3

try:

    print("オリジナル側CDCテーブル作成処理を開始")

    # テーブル定義の初期化
    Table_Create = []
    Create_SQL = "CREATE TABLE IF NOT EXISTS " + Table_Name[0] + " ("+DC0+DC1+DC2+")"  
    Table_Create.append(Create_SQL)    
    Create_SQL = "CREATE TABLE IF NOT EXISTS " + Table_Name[1]+ " ("+DC0+DC3+")"
    Table_Create.append(Create_SQL)
    Create_SQL = "CREATE TABLE IF NOT EXISTS " + Table_Name[2] + " ("+DC0+DC4+")" 
    Table_Create.append(Create_SQL)

    # MySQLとの接続
    db = pymysql.connect(host = 'xxx.xxx.xxx.xxx',
                         port=3306,                
                         user='xxxxxxxx',
                         password='zzzzzzzz',
                         db='xxxxxxxx',        
                         charset='utf8mb4',
                         cursorclass=pymysql.cursors.DictCursor)

    with db.cursor() as cursor:

        Counter = 0

        while Counter < Generate_Table:

            # テーブルの初期化
            cursor.execute("DROP TABLE IF EXISTS " + Table_Name[Counter])
            db.commit()

            # 新規テーブルを作成
            cursor.execute(Table_Create[Counter])    
            db.commit()

            Counter = Counter + 1

except KeyboardInterrupt:

    print('!!!!! 割り込み発生 !!!!!')

finally:

    # データベースコネクションを閉じる
    db.close()

    print("オリジナル側CDCテーブル作成処理が終了")

次にデータを連続挿入する仕組みを作ります。

顧客系に関しては、実際の取引とは関係ない「単純な顧客登録のみ」が時々入ってくる様にしました。またこの割合は

if str(fakegen.boolean(40)) == "True":

の部分で40の値を変える事で可能です(詳しくは、Pythonのマニュアルにて・・(汗))
また、リアル感を追加する為に、ランダムな暇つぶし処理も入っていますので、これも適宜書き換えて頂いて結構です。
(因みに、Equalum的には一番ドSな待ち時間無しの設定でも普通に処理します・・・・・)

# coding: utf-8
#
# MYSQLの各CDCテーブルをランダムに更新する
#
# Python3版
#

#  初期設定
import sys
stdout = sys.stdout
sys.stdout = stdout

import time
import pymysql.cursors
import re

# デモで使うメタデータ定義
Category_Name = ["酒類","家電","書籍","DVD/CD","雑貨"]

Product_Name0 = ["日本酒","バーボン","ビール","芋焼酎","赤ワイン","白ワイン","スコッチ","ブランデー","泡盛","テキーラ"]
Product_Price0 = [1980, 2500, 490, 2000, 3000, 2500, 3500, 5000, 1980, 2000]    

Product_Name1 = ["テレビ","洗濯機","ラジオ","ステレオ","電子レンジ","パソコン","電池","エアコン","乾燥機","掃除機"]
Product_Price1 = [49800, 39800, 2980, 88000, 29800, 64800, 198, 64800, 35800, 24800]    

Product_Name2 = ["週刊誌","歴史","写真集","漫画","参考書","フィクション","経済","自己啓発","月刊誌","新刊"]
Product_Price2 = [280, 1500, 2500, 570, 1480, 1400, 1800, 1540, 980, 1980]    

Product_Name3 = ["洋楽","演歌","Jポップ","洋画","アイドル","クラッシック","邦画","連続ドラマ","企画","アニメ"]
Product_Price3 = [1980, 2200, 2500, 3500, 2980, 1980, 3800, 2690, 1980, 2400]    

Product_Name4 = ["洗剤","電球","贈答品","医薬部外品","ペットフード","乾電池","文房具","男性用品","女性用品","季節用品"]
Product_Price4 = [498, 198, 1980, 398, 980, 248, 398, 2980, 3580, 1980]

Table_Name1 = "Rep_Demo_Usr_Table" # 顧客管理系テーブル
Table_Name2 = "Rep_Demo_Biz_Table" # 販売・物流系テーブル
Table_Name3 = "Rep_Demo_Pay_Table" # 決済系テーブル

# 地域名ルックアップ情報(キーは都道府県名)
Area_Data={'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北','山形県':'東北','福島県':'東北',
           '茨城県':'関東','栃木県':'関東','群馬県':'関東','埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東',
           '新潟県':'中部','富山県':'中部','石川県':'中部','福井県':'中部','山梨県':'中部','長野県':'中部','岐阜県':'中部','静岡県':'中部','愛知県':'中部',
           '三重県':'近畿','滋賀県':'近畿','京都府':'近畿','大阪府':'近畿','兵庫県':'近畿','奈良県':'近畿','和歌山県':'近畿',
           '鳥取県':'中国','島根県':'中国','岡山県':'中国','広島県':'中国','山口県':'中国',
           '徳島県':'四国','香川県':'四国','愛媛県':'四国','高知県':'四国',
           '福岡県':'九州・沖縄','佐賀県':'九州・沖縄','長崎県':'九州・沖縄','熊本県':'九州・沖縄','大分県':'九州・沖縄','宮崎県':'九州・沖縄','鹿児島県':'九州・沖縄','沖縄県':'九州・沖縄'}

# 物流センタールックアップ情報(キーは地域名)
Logi_Data = {'北海道':'道央物流センター','東北':'東北物流センター','関東':'関東中央物流センター',
             '中部':'甲州物流センター','近畿':'伊丹物流センター','中国':'広島臨港物流センター','四国':'讃岐物流センター','九州・沖縄':'平戸物流センター'}

# 購入ポイント情報(カテゴリ名の順番に設定
Point_Data = [0.02, 0.1, 0.03, 0.02, 0.05]

# 消費税率の設定
Tax_Data = 0.1

# 生成するデータの総数
Loop_Count = 10

# タイミング調整フラグ (0:待ち時間無し 1:1秒おき 2:ランダム)
Wait_Flag = 2

# 一定間隔の場合(システム時間で秒単位)
Sleep_Wait = 1

# ランダム間隔の場合(実態に合わせて調整)
#Base_Count = 1600000
Base_Count = 800000

# SQL情報をコンソールに表示するか否かの設定(1:表示する)
Display_SQL = 0

# 書き込み用のデータカラム
DL0 = "Order_ID, "
DL1 = "User, Zip, Prefecture, Address, Tel, Email, Point, Area"
DL2 = "Category, Product, Price, Units, Logistics"
DL3 = "User, Card, Number, Price, Units, Payment, Tax"

def Add2Pre(Address_Data):

    # 都道府県情報の抽出
    pattern = u"東京都|北海道|(?:京都|大阪)府|.{2,3}県"
    m = re.match(pattern , Address_Data)
    if m:
        Prefecture_Name = m.group()

    return(Prefecture_Name)

try:

    print("テーブルへ連続挿入を開始")

    # Fakerの初期化
    from faker import Faker
    fakegen = Faker('ja_JP')    
    Faker.seed(fakegen.random_digit())

    # その他の変数
    Counter = 0
    Work_Count = 1

    # MySQLとの接続
    db = pymysql.connect(host = 'xxx.xxx.xxx.xxx',
                         port=3306,                
                         user='xxxxxxxx',
                         password='zzzzzzzz',
                         db='xxxxxxxx',        
                         charset='utf8mb4',
                         cursorclass=pymysql.cursors.DictCursor)

    with db.cursor() as cursor:

        # 検証データの生成
        while Counter < Loop_Count:

            # ランダムに書き込む商材の種類を選択
            Category_ID = fakegen.random_digit()
            if Category_ID > 4: Category_ID = Category_ID - 5

            # カテゴリ名の設定  
            Category = Category_Name[Category_ID]
            # 商品IDの設定
            Product_ID = fakegen.random_digit()

            # オーダーIDの情報作成
            S_Counter = str(Counter)
            O_Counter = S_Counter.zfill(8)

            if Category_ID == 0:
                Product = Product_Name0[Product_ID]
                Price = Product_Price0[Product_ID]
                Units = fakegen.random_digit() + 1
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C00" + O_Counter

            elif Category_ID == 1:
                Product = Product_Name1[Product_ID]
                Price = Product_Price1[Product_ID]
                Units = 1
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C01" + O_Counter

            elif Category_ID == 2:
                Product = Product_Name2[Product_ID]
                Price = Product_Price2[Product_ID]
                Units = fakegen.random_digit() + 1
                if Units >3: Units = 3  
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C02" + O_Counter

            elif Category_ID == 3:
                Product = Product_Name3[Product_ID]
                Price = Product_Price3[Product_ID]
                Units = fakegen.random_digit() + 1
                if Units >2: Units = 2
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C03" + O_Counter

            else:
                Product = Product_Name4[Product_ID]
                Price = Product_Price4[Product_ID]
                Units = fakegen.random_digit() + 1
                if Units >4: Units = 4
                Point = Price * Units * Point_Data[Category_ID]
                O_ID = "C04" + O_Counter

            # 顧客名
            User = fakegen.name()

            # 支払い情報
            if str(fakegen.pybool()) == "True":
                Card = "現金"
            else:
                Card = fakegen.credit_card_provider()

            Number = fakegen.credit_card_number()              
            if Card == "現金":    Number = "N/A"

            # 支払い総額と消費税
            Payment = Price * Units
            Tax = Payment * 0.1

            # 顧客情報の設定
            Zip = fakegen.zipcode()
            Address = fakegen.address()            
            Prefecture = Add2Pre(Address)           
            Tel = fakegen.phone_number()
            Email = fakegen.ascii_email()

            # 地域名と物流センター名を取得           
            Area = Area_Data.get(Prefecture)
            Logistics = Logi_Data.get(Area)

            # 購入処理系は随時全て更新し、顧客管理のみ時々顧客登録のみを実施する            
            if str(fakegen.boolean(40)) == "True":

                Point = 0 # 登録だけなのでポイントは無し

                # 顧客登録のみのコードを設定
                O_ID = "W99" + O_Counter

                DV0 = O_ID + "','"                
                DV1 = User + "','" + Zip + "','" + Prefecture + "','" + Address + "','" + Tel + "','" + str(Email) + "','" + str(Point) + "','" + Area
                SQL_Data1 = "INSERT INTO " + Table_Name1 + " (" + DL0 + DL1+ ") VALUES ('"+DV0 + DV1 + "')"

                # 顧客登録のみを処理
                cursor.execute(SQL_Data1)
                db.commit()

                #  コンソールに生成データを表示
                if Display_SQL == 1:
                    print (SQL_Data1) 

            else: # 通常の処理

                # 挿入データ用SQLの準備
                DV0 = O_ID + "','"
                DV1 = User + "','" + Zip + "','" + Prefecture + "','" + Address + "','" + Tel + "','" + str(Email) + "','" + str(Point) + "','" + Area
                DV2 = Category + "','" + Product + "','" + str(Price) + "','" + str(Units) + "','" + Logistics
                DV3 = User + "','" + Card + "','" + Number + "','" + str(Price) + "','" + str(Units) + "','" + str(Payment) + "','" + str(Tax)

                # SQLの作成
                SQL_Data1 = "INSERT INTO " + Table_Name1 + " (" + DL0 + DL1+ ") VALUES ('"+DV0 + DV1 + "')"
                SQL_Data2 = "INSERT INTO " + Table_Name2 + " (" + DL0 + DL2 + ") VALUES ('"+DV0 + DV2 + "')"
                SQL_Data3 = "INSERT INTO " + Table_Name3 + " (" + DL0 + DL3 + ") VALUES ('"+DV0 + DV3 + "')"

                # MySQLへ書き込む          
                cursor.execute(SQL_Data1)
                db.commit()

                cursor.execute(SQL_Data2)
                db.commit()

                cursor.execute(SQL_Data3)
                db.commit()

                #  コンソールに生成データを表示
                if Display_SQL == 1:
                    print (SQL_Data1)
                    print (SQL_Data2)
                    print (SQL_Data3)


            # 生成間隔の調整
            if Wait_Flag == 1:
                time.sleep(Sleep_Wait)
            elif Wait_Flag == 2:
                Wait_Loop = Base_Count * fakegen.random_digit() + 1
                for i in range(Wait_Loop): Work_Count = Work_Count + i

            # ループカウンタの更新
            Counter=Counter+1

            # データの作成状況を表示
            if (Counter % 10) == 0:     print("途中経過: " + str(Counter) + " 個目のデータ作成を終了")

except KeyboardInterrupt:

    print('!!!!! 割り込み発生 !!!!!')

finally:

    # データベースコネクションを閉じる
    db.close()

    print("生成したデータの総数 : " + str(Counter))    
    print("テーブルへ連続挿入を終了")

取り敢えずのテストを実施・・・

では、取り急ぎのテストを実施してみます。今回は仮想環境上にCDC設定済みのMySQLを立ち上げて、その上にレプリケーション検証用のデータベースを作成し、その中に3個のテーブルを作成します。

スクリーンショット 2021-02-25 15.31.53.png

無事にテーブルが出来ましたので、今度は連続挿入の処理を走らせてみます。ここではランダム感覚で10個のデータを生成・挿入します。

「販売・物流系テーブル」
スクリーンショット 2021-02-25 15.35.23.png
「決済系テーブル」
スクリーンショット 2021-02-25 15.36.10.png
「顧客系テーブル」
スクリーンショット 2021-02-25 15.39.53.png

上手くデータが分類されて、3種類のテーブルが出来ている事が確認出来ました。

いよいよEqualumの登場!

この画面は、以前検証報告させて頂いたEqualumと「殆ど同じ」になりますが、今回のレプリケーション機能の対応拡大に伴い、2箇所大きな追加・変更が有ります。
スクリーンショット 2021-02-25 15.49.51.png
まずは右上の部分
スクリーンショット 2021-02-25 15.53.07.png
左側のメニューバー部分
スクリーンショット 2021-02-25 15.56.48.png
になります。

レプリケーションの設定をしてみる。。。

まずは、左側のメニューバーからレプリケーションを選択します。
スクリーンショット 2021-02-25 15.59.59.png
+ADDボタンを選択します。
スクリーンショット 2021-02-25 16.01.34.png
あとは、上から順番に必要項目を選択・設定していけば終了です。
(1)グループ名を設定してソースとターゲット、その他の項目を設定
(2)Add Tablesの下に+ボタンが有るので選択します。
スクリーンショット 2021-02-25 16.04.57.png
(3)必要項目を選択・入力します。今回はRepxxxxxという形でテーブル名を統一していますので、Rep%と設定しました。この部分は、その他の省略形式もサポートしていますので、関連するテーブルを一気に選択・設定する際に便利な機能になります。
(4)Generate Listボタンを選択すると、先程の条件に適合するテーブルが自動的に選択されますので、問題がなければValidate Tableを選択して接続確認します。
スクリーンショット 2021-02-25 16.40.12.png
無事に整合性が取れていると判断された場合には、緑色のチェックマークが付きますのので、右下のCreateボタンを押して登録します。(万が一この様な警告が出た場合は、ターゲット側に同じ名前のテーブルが無いかどうかを確認してください。)
スクリーンショット 2021-02-25 16.38.56.png

では検証開始!

無事に設定・登録が完了すると、管理画面にEqualumが自動生成したFLOWとして3個のストリーミング・レプリケーションが確認出来ます。
スクリーンショット 2021-02-26 9.14.50.png
まずは、DBeaverを使ってスタートの状況を確認します。
スクリーンショット 2021-02-26 9.16.52.png
空っぽのテーブルが、先程のPythonスクリプトで作成され、この段階ではターゲットのSingleStore側には何も変化は有りませんが、先程の連続挿入スクリプトを走らせると、ターゲット側にテーブルが作られてデータがストリーミングでレプリケーションされます。
スクリーンショット 2021-02-26 9.19.09.png

念の為に、DBeaverで確認してみます。
ソース側のテーブル
スクリーンショット 2021-02-26 9.12.35.png
ターゲット側のテーブル
スクリーンショット 2021-02-26 9.11.27.png
無事にレプリケーションが行われている様です。

今回のまとめ

今回は、Equalumのレプリケーション機能についての簡単な動作検証を行いました。以前の記事を読まれた方で”勘の良い方”はピン!ときたかもしれませんが、Equalumの場合途中のFLOW処理を入れなければ、基本的にはレプリケーションと同じ”様な”動作になります。もちろん、上流側のソースDBがCDCストリーミングをサポートしていれば、”ほぼ同じ”様な動作を手組みで作る事も可能なのですが、現実的なレプリケーション環境を考えると、命名規則に則ったテーブル名設定や、それらのリレーションを保持する為にテーブルをグループ化して管理しなければならない・・といった要求仕様が出てきます。
今回Oracle環境以外に横展開を始めたEqualumのレプリケーション環境では、出来るだけ利用者の負担を減らす方向で、レプリケーションに合わせた機能追加などを行い、通常メニューとして利用出来る様になりました。
また、冒頭にも書かせて頂いた通り、同じデータベース同志でなければレプリケーション出来ない!という仕様でも有りませんので、用途やコスト見合いで柔軟に構成を選択する事が可能です。

さて次回は・・・・

次回は、今回検証出来なかった、レプリカ側に対してPythonを使った簡単な即時可視化の仕掛けを入れて、利活用を意識した検証を行いたいと思います。もちろん、シンプルなBCP対応!という事であれば、今回の検証で十分ご検討頂けるかと思いますが、逆に「取り敢えず現場から全部持ってきて、それから考える!(ETL的処理は会議室で考える!系)というアプローチも否定できませんので、(この辺は、Equalumが稼働するサーバ・クラスターの構成にも関係しますが・・・)興味本意半分以上ではありますが、可視化ツールの作成から作業を行ってみたいと思います。

謝辞

本検証は、SIngleStore社の公式Freeバージョン(V7.3)を利用して実施しています。
この貴重な機会を提供して頂いたSingleStore社に対して感謝の意を表すると共に、本内容とSingleStore社の公式ホームページで公開されている内容等が異なる場合は、SingleSTore社の情報が優先する事をご了解ください。

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

VBAからMySQLに接続してデータベース操作する方法のまとめ

はじめに

VBAからMySQLに接続する方法と各種の基本操作について、備忘のためにまとめておきます。

<目次>
1. 基本的なコードの記載
2. 接続方法の詳細
 2-1. Driver(ドライバ)の指定
 2-2. Server(サーバ名)の指定
 2-3. Port(ポート番号)の指定
 2-4. Database(データベース名)の指定
 2-5. ユーザー名とパスワードの指定
 2-6. Charset(文字セット)の指定
3. 主なSQL構文によるソースコード例
 3-1. SELECT文(レコードの取得)
 3-2. CREATE TABLE文(テーブルの作成)
 3-3. INSERT文(レコードの追加)
 3-4. UPDATE文(レコードの更新)
 3-5. DELETE文(レコードの削除)
 3-6. DROP TABLE文(テーブルの削除)
 3-7. テーブルが存在するか否かを確認する

1. 基本的なコードの記載

前提として、MySQLに作成した「test_db」というデータベース(ローカルホストに作成)に、次のような「商品リスト」というテーブルを作成している状態から始めます。
(MySQLの操作に慣れない方は、MySQLの使い方というサイトがお勧めです。)

コマンドプロンプト(MySQLのテーブル)
mysql> select * from 商品リスト;
+--------+--------+------+
| 商品ID | 商品名 | 単価 |
+--------+--------+------+
|      1 | ビール | 5000 |
|      2 | 焼き鳥 | 7000 |
|      3 | 柿ピー |  500 |
|      4 | ポテチ | 3000 |
+--------+--------+------+
4 rows in set (0.00 sec)
コマンドプロンプト(MySQLのテーブル作成時の定義)
mysql> create table 商品リスト (商品ID int primary key, 商品名 varchar(20), 単価 decimal(10));

上記のテーブルの内容を、VBAからSELECT文で取得するには次のようなコードを記述します。
なお、事前に参照設定Microsoft ActiveX Data Object 6.1 Library)をしています。
ユーザー名とパスワードは、ご自身の設定内容に修正してください。

VBAからSELECT文を実行(最低限の記載内容)
'SELECT文の実行
Sub ConnectToMysqlDatabase_Select()
    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=root;" & _
                          "Password=XXXXXXXX"
    cn.Open

    'SELECT文の実行(取得した内容の確認)
    Dim rs As New ADODB.Recordset
    rs.Open "SELECT * FROM 商品リスト", cn 'SQL文の実行
    Do Until rs.EOF
        Debug.Print rs("商品ID") & ", " & rs("商品名") & ", " & rs("単価")
        rs.MoveNext
    Loop

    'メモリの解放(無くとも構わない)
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
End Sub
イミディエイトウィンドウ
1, ビール, 5000
2, 焼き鳥, 7000
3, 柿ピー, 500
4, ポテチ, 3000

参考サイト(MySQLにADOで接続)
ADO(ActiveX Data Objects)の使い方の要点
データベース(MySQL)に接続する(ADO)
C#からMySQLに接続する

2. 接続方法の詳細

ここでは、ConnectionStringプロパティについての説明を書いておきます。
ADOに関する基本的なことは、必要に応じてこちらの記事などを参照してください。

ConnectionStringプロパティの設定例
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Port=3306;" & _
                          "Database=test_db;" & _
                          "User=root;" & _
                          "Password=XXXXXXXX;" & _
                          "Charset=sjis;"

<ConnectionStringプロパティの設定内容一覧>

キーワード 設定内容 指定例
Driver ODBCドライバを指定 Driver={MySQL ODBC 8.0 Unicode Driver}
Server ホスト名又はIPアドレスを指定 Server=192.168.3.5
Port サーバのポート番号を指定 Port=3306
Database データベース名を指定 Database=販売管理DB
User (UID) ユーザー名を指定 UID=taka
Password (PWD) パスワードを指定 PWD=12345678
Charset クライアント側の文字コードを指定 Charset=cp932

・キーワードと値は等号=で結びつけます。キーワードと値のペアごとにセミコロン;で区切ります。
・キーワードは、大文字と小文字は区別されません。
UserUIDPasswordPWDと書いても同じ結果となります。

参考サイト
SqlConnection.ConnectionStringプロパティ

2-1. Driver(ドライバ)の指定

まず、Driverの指定についてです。
使用するドライバ名を波括弧で括って記述します。

Driver={MySQL ODBC 8.0 Unicode Driver}

このドライバ名は、PCにインストールされているODBCドライバのバージョンに基づいて記述することが必要です。
ドライバは、ODBCデータソースアドミニストレーターから確認できます。以下、確認方法について書いておきます。

2-1-1. ODBCデータソースアドミニストレーターの起動方法

ODBCデータソースアドミニストレーターは次のような画面で表示されます。
2021-02-21 225805.png
ODBCデータソースアドミニストレーターの起動方法には、次のような起動方法があります。

①コントロールパネルから起動
Windows10では、スタートメニューから「ODBC データ ソース」と検索して起動することができます。

②コントロールパネルから起動
コントロールパネルからでも、「システムとセキュリティ」→「管理ツール」をクリックすれば、管理ツールのショートカット一覧が表示されますので、そこから使用する環境に合わせてODBC データ ソース (64 ビット)又はODBC Data Sources (32-bit)のどちらかを起動します。

③実行ファイルから直接起動
次のディレクトリから直接起動することもできます。

<64ビット版>
C:\Windows\System32\odbcad32.exe

<32ビット版>
C:\Windows\SysWOW64\odbcad32.exe

2-1-2. MySQLのODBCドライバーの確認

ODBCデータソースアドミニストレーターが起動できたら「ドライバ」タブを開きます。
すると、次のような一覧が表示されます。
2021-02-11 164523.png
既にドライバがインストールされていれば、次の2つのドライバを確認することができると思います。

ドライバ名 対応する文字コード
MySQL ODBC 8.0 ANSI Driver UTF-8
MySQL ODBC 8.0 Unicode Driver UTF-16(UCS-2)

対応する文字コードは、実際に入力して確認した結果(経験則)なので、もう少し正しい表現があるかもしれません。

2-1-2-1. MySQL ODBC 8.0 Unicode Driver (UTF-16の場合)

Windows VBAで使用されている文字コードはUnicode(UTF-16)なので、MySQL ODBC 8.0 Unicode Driverを選びます(ただし、VBAで入力できる文字コードはSHIFT_JISなので、ここはややこしいところ)。
何にしても、WindowsのVBAであれば迷わずMySQL ODBC 8.0 Unicode Driverを選んでください。

2-1-2-2. MySQL ODBC 8.0 ANSI Driver (UTF-8の場合)

一方、MySQL ODBC 8.0 ANSI Driverを使用するとデータベース名、テーブル名、カラム名がUTF-8で読み込まれるため、VBAでは文字化けして使用は困難となります。
例えば、「商品ID」と「商品名」というカラムがあると、UTF-8で取得された文字列を無理矢理SHIFT_JISで読もうとするので、次のような文字化けが起こります。

MySQLのカラム名 UTF-8文字コード ANSI Driverを介した表示(SHIFT_JISの文字コード)
商品ID e5 95 86 e5 93 81 49 44 蝠・刀ID(e5 95 / 〓 / 93 81 / 49 / 44)
商品名 e5 95 86 e5 93 81 e5 90 8d 蝠・刀蜷・(e5 95 / 〓 / 93 81 / e5 90 / 〓)

ただし、各レコードのデータ(データベースに格納する個々のデータ)は、ANSI Driverでも文字化けせずに読み込むことができます(各レコードのデータは、必要に応じてCharsetの設定で文字コードを指定することもできます)。
なので、データベース名、テーブル名、カラム名を全て英数字(正確にはASCII文字)にするなどの処置を行えば、ANSI Driverでも何とかなります(やる価値はないと思いますが)。

なお、Macの文字コードはUTF-8なので、MacでODBCドライバを使用する場合は、こちらのANSI Driverを使うのかもしれません(試していないのでわかりません)。

参考サイト
ODBCドライバーのバージョンを確認する
10.1.10 Unicode のサポート

2-1-3. MySQLのODBCドライバーが無い場合

私の手元の環境では、MySQLのインストール時にドライバも一緒にインストールされていました(おそらく)。
使用する環境にドライバがインストールされていない場合には、次のようなWEBサイトなどを参考にして、インストールを行ってください。

参考サイト
MySQL の ODBC ドライバーをインストールする
MySQL ODBC ドライバのインストール(Windows 上)
MySQLへのODBC接続設定をする

2-2. Server(サーバ名)の指定

次に、Server(サーバ名)の指定についてです。
ここは、MySQLのデータベースを格納しているホスト名又はIPアドレスを指定します。

<自分のローカルPC上のMySQLデータベースを参照する場合>
Server=localhost

<IPアドレスで指定する場合>
Server=192.168.3.5

ローカルに作成しているMySQLのデータベースはlocalhostと指定すれば大丈夫です。

2-2-1. ホスト名で指定する場合

Serverをホスト名で指定する方法は、色々な場合では試していないので、確認した範囲で書いておきます。
ローカルPC上のMySQLデータベースのホスト名を確認すると、次のように自分のPC名が表示されます(参照:mysqlで接続しているDBサーバのホスト名を確認する方法)。

mysql> show variables like 'hostname';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| hostname      | DESKTOP-PC      |
+---------------+-----------------+
1 row in set, 1 warning (0.02 sec)

このホスト名「DESKTOP-PC」をServerに指定して接続をしてみます。

cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};Server=DESKTOP-PC;Database=test_db;UID=root;PWD=XXXXXXXX;"

しかし、次のようにアクセスが拒否されます。
2021-02-23 120818.png
これを無理矢理接続するために、次のようなユーザーを作成します(ユーザー作成方法の詳細は「MySQLの使い方 ユーザーの作成」を参照してください)。

mysql> create user 'yama'@'DESKTOP-PC' identified by '123456';
Query OK, 0 rows affected (0.53 sec)

mysql> grant select on test_db.* to 'yama'@'DESKTOP-PC';
Query OK, 0 rows affected (0.17 sec)

最初のCREATE USER文で、ユーザー名を「yama」、クライアントのホスト名を「DESKTOP-PC」(ローカルホスト名)、パスワードを「123456」とするユーザーを作成しています。
続くGRANT文で、作成したユーザーに、「test_db」データベースの全てのテーブルに対して、SELECT権限を与えています。
これにより、MySQLのサーバは、「yama@DESKTOP-PC」というユーザーに対してアクセス権を与えたことになります。

この作成したユーザーで、接続をすると無事にホスト名でAccessをすることができます。

Serverにホスト名を指定する
cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};Server=DESKTOP-PC;Database=test_db;UID=yama;PWD=123456;"

理屈としては、同じローカルホスト内であっても、ホスト名を指定して接続する以上は、アクセスするクライアントに対しても接続の許可を与えないとならないということでしょうか…(詳しくはわかりません)。
外部サーバに接続する場合は、このあたりを上手く設定すれば良さそうです。

参考サイト
MySQL を外部接続できるようにする
Host xxx is not allowed to connect to this MySQL server の対応

2-2-2. IPアドレスで指定する場合

自分のPCのIPアドレスは、ipconfigコマンドで確認することができます。

C:\>ipconfig
Windows IP 構成
(中略)
イーサネット アダプター イーサネット:
   接続固有の DNS サフィックス . . . . .:
   リンクローカル IPv6 アドレス. . . . .:
   IPv4 アドレス . . . . . . . . . . . .: 192.168.3.5
   サブネット マスク . . . . . . . . . .: 255.255.255.0
   デフォルト ゲートウェイ . . . . . . .: 192.168.3.1
(以下略)

上記のうち、IPv4 アドレスが自分のPCのIPアドレス(プライベートIPアドレス)になります。

確認できたIPアドレス「192.168.3.5」をServerに指定して接続してみますが、IPアドレスの場合も前例のホスト名の場合と同様に、先に、接続するためのユーザーを作成します。

mysql> create user 'tama'@'192.168.3.5' identified by '123456';
Query OK, 0 rows affected (0.28 sec)

mysql> grant all on *.* to 'tama'@'192.168.3.5';
Query OK, 0 rows affected (0.18 sec)

前例と基本的に同じですが、GRANT文においては、作成したユーザーに対して、全てのデータベースの全てのテーブル(*.*)に対して、全ての権限(grant all)を与えています。

ConnectionStringプロパティの部分には次のように記載します。これでうまく動作すると思います。

ServerにIPアドレスを指定する
cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};Server=192.168.3.5;Database=test_db;UID=tama;PWD=123456;"

2-3. Port(ポート番号)の指定

Port(ポート番号)の指定については次のように記載します。

Port=3306

MySQLのポート番号はデフォルトで3306です。
デフォルトのままであれば、Port番号の指定は無くとも構いません。

2-4. Database(データベース名)の指定

Database(データベース名)の指定については次のように記載します。これ以上何も言うことはありません。

Database=test_db

2-5. ユーザー名とパスワードの指定

User(ユーザー名)とPassword(パスワード)の指定については次のように記載します。

<例1
User=tama
Password=123456

<例2
UID=tama
PWD=123456

UserはUIDと書くこともでき、PasswordはPWDと書くこともできます。

2-6. Charset(文字セット)の指定

最後に、Charset(文字セット)の指定です。
次の例ではクライアント側の文字コードをSHIFT_JISと指定していることになります。
Windows VBAでは、sjis又はcp932としておけば間違いないところと思います。

Charset=sjis

参考とさせていただいたサイトによると、クライアント側の文字コードとデータベース側の文字コードが異なる場合に、クライアント側の文字コードを指定するとしています。

つまり、Charsetはあくまで、クライアント側の設定と言うことになります。
MySQLデータベースの文字コードがUTF-8(utf8mb4)であっても、SHIFT_JIS(cp932)であっても、Charset(文字セット)の指定には何ら影響はありません(次の表を見ればわかります)。

2-6-1. 文字コードの設定と読取り内容の確認

Driver及びCharsetの設定がどのように影響するかについて、確認した内容は次の表のとおりです。

MySQL DBの文字コード Driverの指定 Charsetの指定 カラム名読み込み結果 レコード読み込み結果 ADOXカラム名読み込み結果
utf8mb4 / cp932 Unicode Driver 指定なし 正常 正常 文字化け
utf8mb4 / cp932 Unicode Driver utf8mb4 正常 文字化け 文字化け
utf8mb4 / cp932 Unicode Driver utf8 正常 正常 文字化け
utf8mb4 / cp932 Unicode Driver sjis / cp932 正常 正常 正常
utf8mb4 / cp932 ANSI Driver 指定なし 文字化け 正常 正常
utf8mb4 / cp932 ANSI Driver utf8mb4 文字化け 文字化け 文字化け
utf8mb4 / cp932 ANSI Driver utf8 文字化け 文字化け 文字化け
utf8mb4 / cp932 ANSI Driver sjis / cp932 文字化け 正常 正常

MySQLのデータベースの文字コードは、utf8mb4で指定してもcp932で指定しても結果は変わらないのでまとめて記載しています。
赤字のところは予想外の結果です(何でだろう?)。
最後の「ADOXカラム名読み込み」は、ADOXTablesコレクションから得たカラム名の確認結果です(具体的なことは最後に取り上げます)。

2-6-2. 文字コードの設定による挙動のまとめ

① MySQL DBにおいて設定している文字コードは、VBAの結果に影響を及ぼさない。
② ANSI Driverを選択すると、DB名、テーブル名、カラム名の読取りに文字化けが生じる(データはUTF-8で取得されるがVBAでは読み込めないため)。
③ Unicode Driverを選択すると、DB名、テーブル名、カラム名の文字化けは生じない。
④ Charsetの指定に、utf8mb4(UTF-8)を選択するとレコードに文字化けが生じる(ただし、Unicode DriverでUTF-8を指定すると何故か正常に読み取れる)。
⑤ ADOXを使用する際は、CharsetをSHIFT_JIS(cp932)と指定する必要がある。

結論として、DriverMySQL ODBC 8.0 Unicode Driverを選択して、Charsetsjis(cp932)を選択すれば問題はないということになります。

参考サイト
ODBCドライバと各社RDBクライアント文字コードの関係
VBA Unicode 文字の入力や変換、読み込みについて

3. 主なSQL構文によるソースコード例

以下、テーブルの操作を中心に、主要なSQL文を書いておきます。

3-1. SELECT文(レコードの取得)

SELECT文の例は最初に挙げているので、少し形を変えて、カラム名も取得してExcelのシートに書き出すソースコードを貼っておきます。

'SELECT文(レコード内容の取得)
Sub ConnectToMysqlDatabase_SELECT()
    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=yama3;" & _
                          "Password=123456"
    cn.Open

    'SELECT文の実行
    Dim rs As New ADODB.Recordset 'Recordsetオブジェクトのインスタンスを生成
    rs.Open "SELECT * FROM 商品リスト", cn

    '出力先のワークシートオブジェクトを取得
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Worksheets("Sheet1")

    'カラム名をワークシートに書き込む
    Dim i As Long, j As Long
    i = 1
    For j = 1 To rs.Fields.count
        ws.Cells(i, j) = rs(j - 1).Name
    Next j

    'レコードをワークシートに書き込む
    Do Until rs.EOF
        i = i + 1
        For j = 1 To rs.Fields.count
            ws.Cells(i, j) = rs(rs(j - 1).Name)
        Next j
        rs.MoveNext
    Loop

    'メモリの解放
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
End Sub

出力結果は次のようになります。
2021-02-24 215735.png

上記のコードでは、カラム数を取得するためにFieldsコレクションを、カラム名を取得するためにFieldオブジェクトを使用しています。
ADOでよく使用するプロパティは次のあたりです。

名称 内容 備考
Countプロパティ Fieldsコレクション内のFieldオブジェクト数を示す Fieldsコレクションのプロパティ
Nameプロパティ フィールド名(カラム名)を示します Fieldオブジェクトのプロパティ
Typeプロパティ データ型をDataTypeEnumで示します Fieldオブジェクトのプロパティ

3-2. CREATE TABLE文(テーブルの作成)

テーブルの作成にはCREATE TABLE文を使用します。

'CREATE TABLE文(テーブル作成)
Sub ConnectToMysqlDatabase_CreateTable()
    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=yama3;" & _
                          "Password=123456;"
    cn.Open

    'CREATE TABLE文の実行
    Dim cm As New ADODB.Command 'Commandオブジェクトのインスタンスを生成
    cm.ActiveConnection = cn
    cm.CommandText = "CREATE TABLE 買物リスト(" & _
                        "ID INT PRIMARY KEY," & _
                        "品名 VARCHAR(20)," & _
                        "区分 VARCHAR(10)," & _
                        "単価 DECIMAL(10)," & _
                        "購入数 SMALLINT," & _
                        "購入日 DATE)"
    cm.Execute

    'メモリの解放
    Set cm = Nothing
    cn.Close: Set cn = Nothing
End Sub

実行結果をコマンドプロンプトから確認すると、次のようなテーブルが作成されていることが分かります。

コマンドプロンプト
mysql> show create table 買物リスト\G
*************************** 1. row ***************************
       Table: 買物リスト
Create Table: CREATE TABLE `買物リスト` (
  `ID` int NOT NULL,
  `品名` varchar(20) DEFAULT NULL,
  `区分` varchar(10) DEFAULT NULL,
  `単価` decimal(10,0) DEFAULT NULL,
  `購入数` smallint DEFAULT NULL,
  `購入日` date DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.07 sec)

PRIMARY KEY制約を付加すると、自動的にNOT NULL制約も付加されています。また、明示されていませんがUNIQUE制約も内包されています。

3-3. INSERT文(レコードの追加)

レコードの追加にはINSERT文を使用します。

3-3-1. レコードを1つ追加するINSERT文

まず、レコードを1つ追加する場合です。

'INSERT文(データの追加)
Sub ConnectToMysqlDatabase_InsertInto1()
    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=yama3;" & _
                          "Password=123456;"
    cn.Open

    'INSERT文の実行
    Dim cm As New ADODB.Command 'Commandオブジェクトのインスタンスを生成
    cm.ActiveConnection = cn
    cm.CommandText = "INSERT INTO 買物リスト VALUES (1, 'ほうれんそう', '野菜', 150, 2, '2021-2-25')"
    cm.Execute

    'メモリの解放
    Set cm = Nothing
    cn.Close: Set cn = Nothing
End Sub

コマンドプロンプトから確認すると、次のようにレコードが追加されているのがわかります。

コマンドプロンプト
mysql> select * from 買物リスト;
+----+--------------+------+------+--------+------------+
| ID | 品名         | 区分 | 単価 | 購入数 | 購入日     |
+----+--------------+------+------+--------+------------+
|  1 | ほうれんそう | 野菜 |  150 |      2 | 2021-02-25 |
+----+--------------+------+------+--------+------------+
1 row in set (0.04 sec)

3-3-2. Excelシートからまとめてレコードを追加するINSERT文

次に、複数のレコードを一気に追加する場合のコードです。
ここでは、次のようなExcelシートの表を、MySQLデータベースに格納してみます。
2021-02-24 234850.png

コードは次のとおりです。

'INSERT文(データの追加)
Sub ConnectToMysqlDatabase_InsertInto2()

    'ADOを使用してExcelファイルに接続
    Dim xlCn As New ADODB.Connection
    xlCn.ConnectionString = "Driver={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};" & _
                            "DBQ=" & ThisWorkbook.FullName & ";" & _
                            "ReadOnly=1"
    xlCn.Open

    'ExcelのシートをSELECT文で読み込んでINSERT文を作成
    Dim xlRs As New ADODB.Recordset
    xlRs.Open "SELECT * FROM [サンプルテーブル$]", xlCn
    Dim sqlStr As String: sqlStr = "INSERT INTO 買物リスト VALUES " 'これに続けてテーブル内容を列挙していく
    Dim comma As String: comma = ""
    Do Until xlRs.EOF
        sqlStr = sqlStr & comma & "(" & xlRs("ID") & ", '" & xlRs("品名") & "', '" & xlRs("区分") & "', " & xlRs("単価") & ", " & xlRs("購入数") & ", '" & xlRs("購入日") & "')"
        If comma = "" Then comma = ", "
        xlRs.MoveNext
    Loop

    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=yama3;" & _
                          "Password=123456;"
    cn.Open

    'INSERT文の実行
    Dim cm As New ADODB.Command 'Commandオブジェクトのインスタンスを生成
    cm.ActiveConnection = cn
    cm.CommandText = sqlStr
    cm.Execute

    'メモリの解放
    xlRs.Close: Set xlRs = Nothing
    xlCn.Close: Set xlCn = Nothing
    Set cm = Nothing
    cn.Close: Set cn = Nothing
End Sub

レコード1つにき1つのINSERT文を発行しても良いのですが、それだと処理に時間が掛かってしまうので、複数のデータをまとめて追加するINSERT文を使用して、一括でデータを追加しています。

コマンドプロンプトから確認すると、次のようにレコードが追加されています。

コマンドプロンプト
mysql> select * from 買物リスト;
+----+--------------+------+------+--------+------------+
| ID | 品名         | 区分 | 単価 | 購入数 | 購入日     |
+----+--------------+------+------+--------+------------+
|  1 | ほうれんそう | 野菜 |  150 |      2 | 2021-02-25 |
|  2 | にんじん     | 野菜 |   70 |      3 | 2021-02-09 |
|  3 | りんご       | 果物 |  150 |      2 | 2021-02-28 |
|  4 | みかん       | 果物 |   40 |     10 | 2021-02-01 |
|  5 | キャベツ     | 野菜 |  180 |      1 | 2021-02-28 |
|  6 | トマト       | 野菜 |   50 |      3 | 2021-02-28 |
|  7 | じゃがいも   | 野菜 |   50 |     15 | 2021-01-30 |
|  8 | バナナ       | 果物 |  300 |      2 | 2021-02-03 |
|  9 | メロン       | 果物 | 1500 |      1 | 2021-02-26 |
+----+--------------+------+------+--------+------------+
9 rows in set (0.00 sec)

3-4. UPDATE文(レコードの更新)

レコードの内容を更新するにはUPDATE文を使います。

'UPDATE文(レコードの更新)
Sub ConnectToMysqlDatabase_UPDATE()
    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=yama3;" & _
                          "Password=123456;"
    cn.Open

    'UPDATE文の実行
    Dim cm As New ADODB.Command 'Commandオブジェクトのインスタンスを生成
    cm.ActiveConnection = cn
    cm.CommandText = "UPDATE 買物リスト SET 購入数 = 20 WHERE ID = 7"
    cm.Execute

    'メモリの解放
    Set cm = Nothing
    cn.Close: Set cn = Nothing
End Sub

コマンドプロンプトで確認すると、IDが7のレコードの購入数が15から20に更新されていることがわかります。

コマンドプロンプト
mysql> select * from 買物リスト;
+----+--------------+------+------+--------+------------+
| ID | 品名         | 区分 | 単価 | 購入数 | 購入日     |
+----+--------------+------+------+--------+------------+
|  1 | ほうれんそう | 野菜 |  150 |      2 | 2021-02-25 |
|  2 | にんじん     | 野菜 |   70 |      3 | 2021-02-09 |
|  3 | りんご       | 果物 |  150 |      2 | 2021-02-28 |
|  4 | みかん       | 果物 |   40 |     10 | 2021-02-01 |
|  5 | キャベツ     | 野菜 |  180 |      1 | 2021-02-28 |
|  6 | トマト       | 野菜 |   50 |      3 | 2021-02-28 |
|  7 | じゃがいも   | 野菜 |   50 |     20 | 2021-01-30 |
|  8 | バナナ       | 果物 |  300 |      2 | 2021-02-03 |
|  9 | メロン       | 果物 | 1500 |      1 | 2021-02-26 |
+----+--------------+------+------+--------+------------+
9 rows in set (0.01 sec)

3-5. DELETE文(レコードの削除)

レコードの削除にはDELETE文を使います。

3-5-1. レコードの一部を削除するDELETE文

'DELETE文(レコードの一部削除)
Sub ConnectToMysqlDatabase_Delete1()
    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=yama3;" & _
                          "Password=123456;"
    cn.Open

    'DELETE文の実行
    Dim cm As New ADODB.Command 'Commandオブジェクトのインスタンスを生成
    cm.ActiveConnection = cn
    cm.CommandText = "DELETE FROM 買物リスト WHERE 区分 = '野菜'"
    cm.Execute

    'メモリの解放
    Set cm = Nothing
    cn.Close: Set cn = Nothing
End Sub

実行結果をコマンドプロンプトで確認すると、次のとおりです。

コマンドプロンプト
mysql> select * from 買物リスト;
+----+--------+------+------+--------+------------+
| ID | 品名   | 区分 | 単価 | 購入数 | 購入日     |
+----+--------+------+------+--------+------------+
|  3 | りんご | 果物 |  150 |      2 | 2021-02-28 |
|  4 | みかん | 果物 |   40 |     10 | 2021-02-01 |
|  8 | バナナ | 果物 |  300 |      2 | 2021-02-03 |
|  9 | メロン | 果物 | 1500 |      1 | 2021-02-26 |
+----+--------+------+------+--------+------------+
4 rows in set (0.02 sec)

3-5-2. レコードの全部を削除するDELETE文

'DELETE文(レコードの全部削除)
Sub ConnectToMysqlDatabase_Delete2()
    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=yama3;" & _
                          "Password=123456;"
    cn.Open

    'DELETE文の実行
    Dim cm As New ADODB.Command 'Commandオブジェクトのインスタンスを生成
    cm.ActiveConnection = cn
    cm.CommandText = "DELETE FROM 買物リスト"
    cm.Execute

    'メモリの解放
    Set cm = Nothing
    cn.Close: Set cn = Nothing
End Sub

実行結果をコマンドプロンプトで確認すると、次のとおりテーブルの中身が空(Empty)であることがわかります。

コマンドプロンプト
mysql> select * from 買物リスト;
Empty set (0.00 sec)

3-6. DROP TABLE文(テーブルの削除)

テーブルそのものを削除する場合はDROP TABLE文を使います。

'DROP TABLE文(テーブル削除)
Sub ConnectToMysqlDatabase_DropTable()
    'ADOを使用してMySQLに接続
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=test_db;" & _
                          "User=yama3;" & _
                          "Password=123456;"
    cn.Open

    'DROP TABLE文の実行
    Dim cm As New ADODB.Command 'Commandオブジェクトのインスタンスを生成
    cm.ActiveConnection = cn
    cm.CommandText = "DROP TABLE 買物リスト"
    cm.Execute

    'メモリの解放
    Set cm = Nothing
    cn.Close: Set cn = Nothing
End Sub

実行結果をコマンドプロンプトで確認すると、次のように「買物リスト」テーブルが存在しないことが確認できます。

コマンドプロンプト
mysql> select * from 買物リスト;
ERROR 1146 (42S02): Table 'test_db.買物リスト' doesn't exist

3-7. テーブルが存在するか否かを確認する

少し方向性が異なりますが、最後に、データベース内にテーブルが存在するか否かの確認を行うコードを書いておきます。
ここでは、データベースのテーブル一覧を取得するためにADOXオブジェクトを使用します。
以下の例では、事前に、Microsoft ADO Ext. 6.0 for DDL and Securityというライブラリの参照設定をしています。

'テーブルが存在するかの確認を実行
Sub IsExistMysqlTableTest()
    Debug.Print IsExistMysqlTable("test_db", "買物リスト")
End Sub

'テーブルが存在するか否かを確認する関数(戻り値:True=存在する, False=存在しない)
Function IsExistMysqlTable(dbName As String, tblName As String) As Boolean
    Dim cn As New ADODB.Connection 'Connectionオブジェクトのインスタンスを生成
    cn.ConnectionString = "Driver={MySQL ODBC 8.0 Unicode Driver};" & _
                          "Server=localhost;" & _
                          "Database=" & dbName & ";" & _
                          "User=yama3;" & _
                          "Password=123456;" & _
                          "charset=cp932;" 'クライアント側の文字コードを指定
    cn.Open

    Dim ct As New ADOX.Catalog: ct.ActiveConnection = cn 'Catalogオブジェクトを現在のDBに関連付け
    Dim tbl As Table 'テーブルオブジェクトを格納する変数
    IsExistMysqlTable = False '初期値を明示(書かなくともFalse)

    For Each tbl In ct.Tables 'CatalogオブジェクトのTablesコレクションを1つずつ取得
        If tbl.Type = "TABLE" And StrComp(tbl.Name, tblName, vbTextCompare) = 0 Then 'TableオブジェクトのTypeプロパティとNameプロパティで一致するかを確認
            IsExistMysqlTable = True
            Exit For
        End If
    Next

    Set ct = Nothing 'Catalogオブジェクトの解放
    cn.Close: Set cn = Nothing 'Connectionオブジェクトの解放
End Function

3-7-1. ADOXのTableオブジェクトについて

ADOXのCatalogオブジェクトには、データベースのコレクション (テーブル、ビュー、ユーザーなど) が格納されます。
ここでは、TablesコレクションTableオブジェクトが持っている次のプロパティを使用して、テーブルの存否を確認しています。

プロパティ名 内容 備考
Typeプロパティ テーブルの種類を指定する文字列値を返す "TABLE"、"SYSTEM TABLE"、"GLOBAL TEMPORARY"など
Nameプロパティ テーブル名を示す (大文字と小文字の区別がされていないようです)

3-7-2. 取得されるテーブル名の文字コードについて

上記ソースコードのConnectionStringプロパティでは、charset=cp932と指定しています(charset=sjisでもよい)。
これを指定しないと、テーブル名が文字化けして、正しく読み取ることができません。

なお取得される文字コードは、使用するドライバ(MySQL ODBC 8.0 Unicode Driver又はMySQL ODBC 8.0 ANSI Driver)によっても若干異なります。
この挙動については、前掲「2-6-1. 文字コードの設定と読取り内容の確認」のとおりです。

さいごに

ドライバの選択や、文字コードについては調べても分からないことが多かったので、手元で確認してみた結果を書いておきました。
本記事は個人的な備忘にすぎませんが、何らかのお役に立つことがあれば幸いです。

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

【Docker】エラー Could not find gem 'mysql2 (~> 0.5)' in any of the gem sources listed in your Gemfile

はじめに

Dockerの環境構築中に発生したエラーの解決した方法を記録します。
ただ、エラーは発生までの経緯で解決方法が違ってくるので、参考程度にしてください。

【エラー文】
Could not find gem 'mysql2 (~> 0.5)' in any of the gem sources listed in your Gemfile

環境

Docker version 20.10.0
docker-compose version 1.27.4

Docker内の環境

ruby:2.6.5
Rails:6.0.0
データベース:mysql

Dockerfile

docker-compose.yml
FROM ruby:2.6.5
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs\
    vim 

WORKDIR /[作成したディレクトリ名]
COPY Gemfile Gemfile.lock /[作成したディレクトリ名]/
RUN bundle install

docker-compose.yml

docker-compose.yml
version: '3'

services:
  web:
    build: .
    ports:
      - 3000:3000
    volumes:
      - '.:/[作成したディレクトリ名]'
    tty: true
    stdin_open: true

結論

結論は以下の2点を事項することで解決に至りました。
・bundle installしてmysqlをインストール
・webpackerをインストール

経緯と対応

dockerc-composeでコンテナを作成後Railsのセットアップを行いサーバーを起動した時に発生しました。
エラー内容はmysql2が見つからないという内容でした。

対応1

Dockerfileにmysqlの記述がないから当然??と思いましたが、Gemfileにはmysqlが記述されてるのでとりあえずコンテナ内でbundle installをしてインストールしてみることにしてみました。

かなり時間が経ってgemfileにインストールされました。

対応2

再びrails s -b 0.0.0.0をして起動しようと試みましたが今度はPlease run rails webpacker:install Error dockerというエラーが出ました。
これも、Dockerfileに書いてないので当然??と思いながら調べてるとwebpackerを使う為にはyarnが必要で、yarnをインストールする為には下記の記述も必要との事でDockerfileを編集してイメージ作成からやり直しました。
最後コンテナ内でwebpackerをインストールするとうまくいきました。

対応2の手順

Dockerfileにyarnを追記します。

Dockerfile
FROM ruby:2.6.5
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs\
    yarn \ ←ここ
    vim 

WORKDIR /exam
COPY Gemfile Gemfile.lock /exam/
RUN bundle install

rails webpacker:installのコマンドを入力してwebpackerをインストールします。

ターミナル
root@c21d03f52523:/exam# rails webpacker:install
.
.
.
省略
Webpacker successfully installed ? ?
root@c21d03f52523:/exam# 

サーバーを起動します。

ターミナル
root@c21d03f52523:/exam# rails s -b 0.0.0.0
=> Booting Puma
=> Rails 6.1.2.1 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.2.1 (ruby 2.6.5-p114) ("Fettisdagsbulle")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 156
* Listening on http://0.0.0.0:3000
Use Ctrl-C to stop

かなり時間がかかりましたが、インストール後rails s -b 0.0.0.0で見事サーバーが立ち上がりました!

この記事では上記のエラー解決のみの内容ですがこの後データベースを作ってDocker内での開発環境を整えて行きます。
もしこの先にもご興味あれば下記の記事を参考にしてみてください。

【Docker】Ruby2.6.5とRails6.0.0とmysql DockerComposeで環境構築
後日更新予定

最後に

Dockerについて完全に理解できておらず、今回の対応も対処療法ですのでこれからも継続学習が必要です。
万が一情報が間違っている場合ご指摘していただけると幸いです。

参考

https://qiita.com/matata0623/items/2ff3125d2cdbd5c13528

https://qiita.com/anx/items/88cb4bbf67ff6c046e32

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

Ruby 3.x, Rails 6.x, MySQL 8.x の Docker 環境構築。

概要

Docker と docker-compose を使い、アプリケーションサーバを Ruby 3.x, Ruby on Rails 6.x、DB サーバを MySQL 8.x でコンテナの構築するまでの手順となります。

なお、この記事ではセキュリティについての考慮は一切していません。

Windows 10 の WSL2 - Ubuntu 18.04 と Mac で確認していますが、後述する理由により Mac の方がお勧めです。

PostgreSQL の方が好みの方は、以下のページをご確認ください。
cf. クィックスタート: Compose と Rails

前提

下記の環境が設定されていること。

Windows 10

  • WSL2 Ubuntu 18.04+
  • Docker
  • docker-compose
  • MySQL Client(必要に応じて)

Mac

  • Docker
  • docker-compose
  • Docker Desktop for Mac
  • MySQL Client(必要に応じて)

また、Docker のサービスが起動済みであること。

初期ファイル構成

以下のファイルから Rails プロジェクトを新規に作成します。

.
├── docker
│   ├── app
│   │   ├── Dockerfile
│   │   └── entrypoint.sh
│   └── db
│       ├── Dockerfile
│       ├── conf.d
│       │   └── my.cnf
│       └── initdb.d
│           └── init.ddl.sql
├── scripts
│   └── wait-for-it.sh
├── docker-compose.yml
├── Gemfile
└── Gemfile.lock

初期ファイル設定

docker-compose.yml

注意点は app 側の build: context: で基準になるフォルダを root として、Dockerfile のパスを指定しているところです。
これは Dockerfile 内で Gemfile をコピーする必要があるのですが、build: ./docker/app としてしまうとフォルダを遡って Gemfile の操作ができないため、起点を root にしています。
Dockerfile が root にあれば関係ないのですが、今回は docker フォルダ下にしているため、このような対応となります。

また app の command: で先述の wait-for-it.sh を利用し、DB が起動するまで rails server を立ち上げないようにしています。

docker-compose.yml
version: "3.3"

services:
  db:
    container_name: "db"
    build: ./docker/db
    restart: always
    tty: true
    environment:
      MYSQL_DATABASE: app_development
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"
    volumes:
      - ./docker/db/conf.d:/etc/mysql/conf.d
      - ./docker/db/initdb.d:/docker-entrypoint-initdb.d
    networks:
      - backend

  app:
    container_name: "app"
    build:
      context: ./
      dockerfile: ./docker/app/Dockerfile
    ports:
      - "3000:3000"
    environment:
      PORT: 3000
      BINDING: 0.0.0.0
    tty: true
    depends_on:
      - "db"
    command: ["./scripts/wait-for-it.sh", "db:3306", "--", "bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
    volumes:
      - .:/app
    networks:
      - frontend
      - backend

networks:
  frontend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.10.0/24
  backend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.20.0/24

app 用設定ファイル

docker/app/Dockerfile

ここで Gemfile および Gemfile.lock をホスト(ローカル)からゲスト(コンテナ)にコピーしています。

docker/app/Dockerfile
FROM ruby:3.0

ENV LANG C.UTF-8
ENV TZ Asia/Tokyo

RUN apt-get update -qq && \
    apt-get install -y --no-install-recommends sudo curl apt-transport-https wget build-essential libpq-dev nodejs default-mysql-client

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && \
    apt-get install --no-install-recommends -y yarn

RUN apt-get clean && \
    rm -rf /var/lib/apt/lists/*

RUN mkdir /app
WORKDIR /app
COPY Gemfile /Gemfile
COPY Gemfile.lock /Gemfile.lock
RUN bundle install
COPY . /app

COPY docker/app/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

docker/app/entrypoint.sh

こちらは以下のサイトの entrypoint.sh からいただきました。
cf. Quickstart: Compose and Rails

docker/app/entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

db 用設定ファイル

docker/db/Dockerfile

docker/db/Dockerfile
FROM mysql:8.0

RUN apt-get update -qq && \
    apt-get install -y --no-install-recommends locales && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    locale-gen ja_JP.UTF-8

RUN sed -i -E 's/# (ja_JP.UTF-8)/\1/' /etc/locale.gen && locale-gen

ENV LANG ja_JP.UTF-8
ENV TZ Asia/Tokyo

docker/db/conf.d/my.cnf

my.cnf は文字コード指定が中心ですが、今回はシンプルに Rails の実行のみを考えているため、default_authentication_plugin=mysql_native_password で認証プラグインを変更しておきます。

docker/db/conf.d/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin
default-storage-engine=INNODB
explicit-defaults-for-timestamp=1
general-log=1
general-log-file=/var/log/mysql/mysqld.log

default_authentication_plugin=mysql_native_password

[mysqldump]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4

[client]
default-character-set=utf8mb4

docker/db/initdb.d/init.ddl.sql

データベースを development 以外に test 用も作成する場合のファイルです。
実際は test 用データベースは rake db:create で作成されるはずですのでなくても問題ないと思われます。
コピー先の /docker-entrypoint-initdb.d フォルダではシェルの実行も可能なので、組み込み次第ではいろいろと対応できるようです。

docker/db/initdb.d/init.ddl.sql
CREATE DATABASE IF NOT EXISTS `app_test`;
GRANT ALL ON app_test.* TO 'user'@'%';

Gemfile

Gemfile

初期は Rails のバージョン指定のみとなります。

Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6'

Gemfile.lock

初期状態は空ファイルとなります。

Gemfile.lock

初期起動までの手順

rails new

Docker のサービスが起動しているか確認の上、docker-compose.yml があるフォルダで database を MySQL に指定して rails new を実行します。

$ docker-compose run app rails new . --force --database=mysql

問題なく実行が完了すると、実行したフォルダに Rails アプリのファイル群が作成されます。
このとき、Mac の場合は実行したユーザーの権限でファイルが作成されますが、WSL の場合は root 権限となり、そのままではファイルの更新が行えません。

cf. 【Docker】 WSL 2 を利用したコンテナー内開発で権限をどう設定するべきか

根本的な解決ではないとは思いますが、とりあえず以下のコマンドで権限を実行ユーザーに振り替えて対応することは可能です。
ただし、ここだけではなく、scaffold など rails のコマンドでファイルを作成・編集するごとに権限を書き換える必要があります。
(これが WSL よりも Mac をお勧めする理由です)

$ sudo chown -R $USER:$USER .

この時点で、作成された config/database.yml を編集し、MySQL へのアクセス設定を変更します。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: user
  password: password
  host: db

docker-compose build, up

build でサービスを作成し、問題がなければ up でコンテナを起動します。
この際、--no-cache オプションを付けるのは、bundle install 実行時に gem ファイルを巧く取り込めない場合がある(らしい)ためです。

$ docker-compose build --no-cache
$ docker-compose up -d

この時、db および app がほぼ同時に立ち上がりますが、docker-compose.yml で記述した通り、app の rails server は MySQL サーバとの接続が確立するまで実行されないようになっています。
それぞれのコンテナのログは docker logs で確認できるため、以下のように確認して下さい。

$ docker logs app # アプリケーションサーバのログ
$ docker logs db # DB サーバのログ

実行に問題がなければ、ブラウザまたは curl などで http://localhost:3000/ にアクセスすることで、いつもの Rails の初期画面が表示されます。

image.png

scaffold 作成と実行の確認

scaffold で MVC を作成して動作が可能か確認します。

$ docker-compose run app rails g scaffold user name:string email:string

WSL で操作している場合はファイル権限の変更をしてください。

$ sudo chown -R $USER:$USER .

db:migrate でテーブルを作成します。

$ docker-compose run app rails db:migrate

ブラウザで http://localhost:3000/users にアクセスすることで scaffold で作成した Rails 標準の UI が表示され、CRUD の一連の操作が可能なことが確認できます。

http://localhost:3000/users/new
image.png

http://localhost:3000/users
image.png

実際の DB を確認したい場合は MySQL Client が入っていればコマンドで確認できます。
(設定を変えていない場合は、user アカウントのパスワードは password となります)

$ mysql -u user -h 127.0.0.1 -D app_development -p
mysql> show create table users;
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                                                              |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select * from users;
+----+------+-----------------+----------------------------+----------------------------+
| id | name | email           | created_at                 | updated_at                 |
+----+------+-----------------+----------------------------+----------------------------+
|  1 | test | aaa             | 2021-02-25 15:56:02.123864 | 2021-02-25 15:56:02.123864 |
|  2 | test | aaa@example.com | 2021-02-25 16:38:33.311981 | 2021-02-25 16:38:33.311981 |
+----+------+-----------------+----------------------------+----------------------------+
2 rows in set (0.00 sec)

今回は以上となります。

参考資料

以下の記事、情報を参考にさせていただきました。
ありがとうございます。

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

【環境構築】docker(docker-syncも) + Vue.js + Rails + MySQL をエラー地獄切り抜け、構築した方法

はじめに

色々なサイトを見ながら、まる三日かけてついに構築できましたので、スムーズに解決できた構築方法をご紹介したいと思います。

・Ruby 2.5.8 (x86_64-linux)
・Ruby on Rails 5.2.4.5

スクリーンショット 2021-02-26 1.09.36.png

初めに、ファイル作成

$ cd 作業するディレクトリ

作業ディレクトリに移動後。

$ touch Gemfile Gemfile.lock docker-compose.yml Dockerfile

ファイル構成はこちら

[project_name]
├── docker-compose.yml
│── Dockerfile
│── Gemfile
├── Gemfile.lock

ファイルの中身を編集していきます

・Gemfile↓

source 'https://rubygems.org'
gem 'rails', '~>5'
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7.19
    environment:
      - MYSQL_ROOT_PASSWORD=root
    ports:
      - "3307:3306"
    volumes:
      - ./tmp/db:/var/lib/mysql
  webpacker:
    build: .
    command: bundle exec bin/webpack-dev-server
    volumes:
      - .:/myapp
    ports:
      - "3035:3035"
  web:
    build: .
    environment:
      RAILS_ENV: development
    command: bash -c "bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
      - bundle-data:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db
      - webpacker
volumes:
  bundle-data:

db(データベース用)、webpacker、アプリケーション用、それぞれのコンテナを作成します。

・Dockerfile↓

FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev

RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
  && apt-get install -y nodejs

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn

RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

CMD ["rails", "server", "-b", "0.0.0.0"]

Railsの初期画面を表示させる

まず、rails newでプロジェクトの作成をします。

$ docker-compose run web rails new . --force --database=mysql --webpack=vue --skip-coffee

--skip-coffee:CoffeeScriptのセットアップをスキップ。

$ sed -i ".bak" -e "s/host: localhost/host: webpacker/g" config/webpacker.yml

sed -i:テキストファイルをフィルター処理で直接編集します。

buildでイメージの構築をします。

$ docker-compose build

datebase.ymlを編集します。

datebase.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root

# ここからした2行
  password: # ここには、datebase-compose.ymlの、- MYSQL_ROOT_PASSWORD=ここを入力
  host: db

development:
  <<: *default
  database: myapp_development

webコンテナ上で、データベースの作成をします。

$ docker-compose run web rails db:create

イメージ構築〜コンテナ起動までをバックグラウンド(-d)で行います

$ docker-compose up -d

localhost:3000にアクセスすると、yey!Railsの初期画面が表示されたのではないでしょうか?

webpackerをインストールしていく

$ docker-compose run web rails webpacker:install
$ docker-compose run web rails webpacker:install:vue
$ docker-compose build
$ docker-compose up -d

Hello Vue!表示しにいく

スクリーンショット 2021-02-26 1.09.36.png

config/routes.rbを下記のように編集

routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end

続いて、home controllerを作成していきます。

$ docker-compose run --rm web rails g controller home index

app/controllers/home_controller.rbと、app/views/home/index.html.erbの編集をする

home_controller.rb
class HomeController < ApplicationController
  def index
  end
end

webpacker は app/javascript/packs/ 配下に設置されたファイルをコンパイルします。
よって、index.html.erbでは、下記のようになります。

index.html.erb
<%= javascript_pack_tag 'hello_vue' %>

localhost:3000を開いてください。

スクリーンショット 2021-02-26 1.09.36.png

追記

2021/02/28:docker-syncの導入を追記しました。

docker for macをお使いの方なら陥ると考えられるlocalhostを開くのが以上に遅い問題を解決していきます。

まだ、これからrailsでアプリを作成する方は是非とも見ていただきたいです。

localhostが遅くなる原因と解決法

【原因】

  • MacのファイルIOが以上に遅いため

つまり、docker for macを使用することによった、ホストとコンテナ間での大量のファイルの同期処理の遅さが原因です。

僕の場合は、1文字変更しただけでもコンパイルに1分以上かかっていました。
もはや、開発どころではありません笑

【解決方法】

  • docker-syncを使用して解決

解決方法は、調べればわかると思いますが解決方法が複数あります。
どれも賛否両論です。

下記にも記載のある、volumeのマウントや、バインドマウントオプションを使用する、名前解決。
それぞれ試してみましたが、僕の場合効果なしでした。

docker-syncを使用した解決方法が最も効果があったため紹介いたします。

他にも、

  • Virtual Box(仮想マシン)上でdockerを使用する
  • バインドマウントオプションを使用する
  • volumeのマウント
  • NFSボリュームシェアリング
  • (DNSで、名前解決)

などなど、様々ありますのでググってみてください。

docker-syncを導入

【参考】
docker-syncでホスト-コンテナ間を爆速で同期する

この記事の通り進めればOKです。

お疲れ様でした?

参考

Rails世渡り2【docker × Ruby on Rails × Vue.js】

Dockerでlocalhostへのアクセスが遅い時の対処法

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

【環境構築】docker + Vue.js + Rails + MySQL をエラー地獄切り抜け、構築した方法

はじめに

色々なサイトを見ながら、まる三日かけてついに構築できましたので、スムーズに解決できた構築方法をご紹介したいと思います。

・Ruby 2.5.8 (x86_64-linux)
・Ruby on Rails 5.2.4.5

スクリーンショット 2021-02-26 1.09.36.png

初めに、ファイル作成

$ cd 作業するディレクトリ

作業ディレクトリに移動後。

$ touch Gemfile Gemfile.lock docker-compose.yml Dockerfile

ファイル構成はこちら

[project_name]
├── docker-compose.yml
│── Dockerfile
│── Gemfile
├── Gemfile.lock

ファイルの中身を編集していきます

・Gemfile↓

source 'https://rubygems.org'
gem 'rails', '~>5'
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7.19
    environment:
      - MYSQL_ROOT_PASSWORD=root
    ports:
      - "3307:3306"
    volumes:
      - ./tmp/db:/var/lib/mysql
  webpacker:
    build: .
    command: bundle exec bin/webpack-dev-server
    volumes:
      - .:/myapp
    ports:
      - "3035:3035"
  web:
    build: .
    environment:
      RAILS_ENV: development
    command: bash -c "bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
      - bundle-data:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db
      - webpacker
volumes:
  bundle-data:

db(データベース用)、webpacker、アプリケーション用、それぞれのコンテナを作成します。

・Dockerfile↓

FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev

RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
  && apt-get install -y nodejs

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn

RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

CMD ["rails", "server", "-b", "0.0.0.0"]

Railsの初期画面を表示させる

まず、rails newでプロジェクトの作成をします。

$ docker-compose run web rails new . --force --database=mysql --webpack=vue --skip-coffee

--skip-coffee:CoffeeScriptのセットアップをスキップ。

$ sed -i ".bak" -e "s/host: localhost/host: webpacker/g" config/webpacker.yml

sed -i:テキストファイルをフィルター処理で直接編集します。

buildでイメージの構築をします。

$ docker-compose build

datebase.ymlを編集します。

datebase.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root

# ここからした2行
  password: # ここには、datebase-compose.ymlの、- MYSQL_ROOT_PASSWORD=ここを入力
  host: db

development:
  <<: *default
  database: myapp_development

webコンテナ上で、データベースの作成をします。

$ docker-compose run web rails db:create

イメージ構築〜コンテナ起動までをバックグラウンド(-d)で行います

$ docker-compose up -d

localhost:3000にアクセスすると、yey!Railsの初期画面が表示されたのではないでしょうか?

webpackerをインストールしていく

$ docker-compose run web rails webpacker:install
$ docker-compose run web rails webpacker:install:vue
$ docker-compose build
$ docker-compose up -d

Hello Vue!表示しにいく

スクリーンショット 2021-02-26 1.09.36.png

config/routes.rbを下記のように編集

routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end

続いて、home controllerを作成していきます。

$ docker-compose run --rm web rails g controller home index

app/controllers/home_controller.rbと、app/views/home/index.html.erbの編集をする

home_controller.rb
class HomeController < ApplicationController
  def index
  end
end

webpacker は app/javascript/packs/ 配下に設置されたファイルをコンパイルします。
よって、index.html.erbでは、下記のようになります。

index.html.erb
<%= javascript_pack_tag 'hello_vue' %>

localhost:3000を開いてください。

スクリーンショット 2021-02-26 1.09.36.png

お疲れ様でした?

参考

Rails世渡り2【docker × Ruby on Rails × Vue.js】

Dockerでlocalhostへのアクセスが遅い時の対処法

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