Ansibleを触ってみた

Ansibleを触ったのでメモ

Ansibleとは

エージェントレス型の構成管理ツール。エージェント型とエージェントレス型では以下のような違いがある。
◯エージェントレス型
管理対象のサーバにエージェントを入れる必要がない
管理対象のサーバへの接続情報が必要になる
 管理端末から構成反映の処理を行う必要がある
◯エージェント型
管理対象のサーバにエージェントを入れる必要がある
起動時などに自動で構成が適用される

複数の設定が異なるサーバを管理する場合は、管理用サーバを別で持てるエージェントレス型の方が扱いやすいような気がします。それからAnsibleはエージェント型のchefやpuppetより後発のため洗練されている部分がありますし、RedHatがAnsibleを買収したため今後も開発が続けられることが期待できます。

0.インストール環境準備

以下のvagrant環境にインストール

Vagrant.configure("2") do |config|

  config.vm.define :node1 do |node|
    node.vm.box = "geerlingguy/centos7"
    node.vm.network "forwarded_port", guest: 22, host: 2001, id: "ssh"
    node.vm.network "private_network", ip: "192.168.33.11"
  end

  config.vm.define :node2 do |node|
    node.vm.box = "geerlingguy/centos7"
    node.vm.network "forwarded_port", guest: 22, host: 2002, id: "ssh"
    node.vm.network "forwarded_port", guest: 80, host: 8000, id: "http"
    node.vm.network "private_network", ip: "192.168.33.12"
  end
end

ssh接続準備

管理用端末が管理対象の端末を操作する際にはsshプロトコルを使用するので、鍵を配置する等の事前準備は必要になると思います。

1.Ansibleインストール

yumを使ってインストールする

# vagrant ssh node1
# sudo yum install epel-release
# sudo yum install ansible

2.管理対象ホストの設定とAnsibleの疎通確認

管理対象ホストの設定

$ echo 192.168.33.12 > hosts

pingでの疎通確認

node1の方で実行
$ ansible -i hosts 192.168.33.12 -m ping

192.168.33.12 | success >> {    
    "changed": false,    
    "ping": "pong"    
}    

ansibleコマンドで連携対象サーバ上で任意のコマンドを実行する

$ ansible -i hosts 192.168.33.12 -a 'uname -r'
192.168.33.12 | success | rc=0 >>
2.6.32-431.el6.x86_64

任意のコマンドを実行

ansible -i hosts 192.168.33.12 -m yum -s -a name=telnet

ansible yumは以下でドキュメントを確認できる

$ ansible-doc yum

3.Playbookの作成から実行まで

Playbookの作成

# vi simple-playbook

---
- hosts: test-servers
  become: yes
  tasks:
    - name: be sure httpd is installed
      yum: name=httpd state=installed

    - name: be sure httpd is running and enabled
      service: name=httpd state=started enabled=yes

Playbookの文法チェック

ansible-playbookに–syntax-checkのオプションをつけて実行する

ansible-playbook -i hosts simple-playbook.yml --syntax-check

Playbookのタスク一覧を確認する

–list-tasksオプションをつけてplyabookのnameを一覧表示してタスクを確認

[vagrant@localhost ~]$ ansible-playbook -i hosts simple-playbook.yml --list-tasks

playbook: simple-playbook.yml

  play #1 (test-servers): test-servers  TAGS: []
    tasks:
      be sure httpd is installed    TAGS: []
      be sure httpd is running and enabled  TAGS: []

Playbookの実行

[vagrant@localhost ~]$ ansible-playbook -i hosts simple-playbook.yml

PLAY [test-servers] ************************************************************

TASK [setup] *******************************************************************
ok: [192.168.33.12]

TASK [be sure httpd is installed] **********************************************
changed: [192.168.33.12]

TASK [be sure httpd is running and enabled] ************************************
changed: [192.168.33.12]

PLAY RECAP *********************************************************************
192.168.33.12              : ok=3    changed=2    unreachable=0    failed=0

ansibleには冪等星性(ある操作を何度実行しても常に結果が同じになる)が備わっており、上記実行例ではchangedの部分が新しく実行された部分になるようです。これでnode2のサーバにPlaybookで指定した通りapacheがインストールされました。
node2内でhttpdが動作しているか確認

[vagrant@localhost ~]$ sudo systemctl status httpd

たったこれだけで、Apacheがインストールできました。chefはほとんど触ったことありませんが、それよりもシンプルで使いやすい気がします。

Playbook入門

yumを使う

tasks:
  - name: be sure httpd is installed
    yum: name=httpd state=installed

nameでインストール対象を指定する。state=installedはインスートールされているか確認し、されていなかったらインストールする。state=latestでは最新が入っているか確認し最新でなければインストールする。

コマンドを実行する

tasks:
  - name: disable selinux
    command: /sbin/setenforce 0

ファイルを編集する

文字列を置換する

tasks:
  - name: set selinux permisive
    replace: >
      dest=/etc/selinux/config
      regexp='SELINUX=enforcing'
      replace='SELINUX=disabled'

http://docs.ansible.com/ansible/replace_module.html

ホスト端末のファイルを送る

事前に転送するファイルを準備

echo hogehoge > copyFile
tasks:
  - name: copy test
    copy: >
      src=/home/vagrant/copyFile
      dest=/home/vagrant/copyFile

http://docs.ansible.com/ansible/copy_module.html

vagrant の基礎

ansibleを触る環境を準備するためにvagrantを触ったのでその時のメモ

mac環境へのインストール

1.公式サイトからインストーラをダウンロードする。

https://www.vagrantup.com/downloads.html
/usr/local/bin/vagrantにインストールされた
~ which vagrant
/usr/local/bin/vagrant

2.vagrantfileの作成

# mkdir -p ~/vagrantspace/vagrant_getting_started    
# cd ~/mywork/vagrantspace/vagrant_getting_started    
# vagrant init

vagrant initでVagrantfileが作られる。Vagrantfileはvagrantのバージョンコントロールを行うためのファイルになる。

3.boxの追加

公式の手順に従ってhashicorp/precise64のboxを追加する。

# vagrant box add hashicorp/precise64

別のboxを追加する場合はatlasが提供している以下のダウンロードサイトから探すことができる。
https://atlas.hashicorp.com/boxes/search?_ga=1.197033387.661843880.1488614682
自前でisoを用意したい場合は、以下を参考にする。
http://kan3aa.hatenablog.com/entry/2015/05/29/120212

vagrantで使用するダウンロード済みboxの一覧を確認する場合は以下を実行する。

# vagrant box list

4.vmを起動して接続する

VirtualBoxをインストールしていないのであれば、公式からインストーラーをダウンロードして入れておく。
https://www.virtualbox.org/wiki/Downloads
vmの起動

# vagrant up

vmssh接続する

# vgrant ssh

vm接続を解除する

# logout
又は
# exit
vagrant基本コマンド
再起動
vagrant reload

ステータス確認
vagrant status

一時停止
vagrant suspend

一時停止からの復帰
vagrant resume

停止
vagrant halt

起動
vagrant up

boxの初期化
vagrant init

boxの削除
vagrant destory
vagrantの起動状態を確認する

status管理用のプラグインインストール

# vagrant plugin install 
# vagrant global-status

ディレクトリの共有

通常Vagrantfileはssh接続後の/vagrant/Vagrantfileにマウントされる。

# ls /vagrant/

/vagrantディレクトリ直下にファイルを作成したらvmのホスト側のVagrantfileがあったディレクトリにファイルが作られる動きをする。

# touch /vagrant/hoge
# exit
# ls

5.vagrant起動時のスクリプトapacheのインストール

以下のapacheインストールスクリプトをbootstarp.shのファイル名でVagrantfileがあるディレクトリに保存しておく。 # vi /vagrant/bootstrap.sh

#!/usr/bin/env bash

apt-get update
apt-get install -y apache2
if ! [ -L /var/www ]; then
  rm -rf /var/www
  ln -fs /vagrant /var/www
fi

それから設定ファイルを編集し/vagrantディクトリにbootstrap.shをvagrant起動時に実行するスクリプトにす指定する。

Vagrant.configure("2") do |config|
  config.vm.box = "hashicorp/precise64"
  config.vm.provision :shell, path: "bootstrap.sh"
end

次にvagrant reloadして再起動

# vagrant reload --provision
# vagrant up

vagrantssh接続している状態でapacheにアクセスできるか確認

# wget -qO- 127.0.0.1

6.ポートフォワード

ホスト端末からvagrantへアクセスできるようにポートフォワードを設定する
# vi Vagrantfile

config.vm.network "forwarded_port", guest: 80, host: 4567

ホスト端末からブラウザでlocalhost:4567にアクセスして動作確認

PyMongo覚書

Pymongoの覚書になります、と言ってもmongoのクライアントから直接実行するのとほとんど同じように使うことができます。

モジュールインポート
import json
import re
import pymongo

import bson
from bson.son import SON
from bson.code import Code
from pymongo import MongoClient
from bson.code import Code
from datetime import datetime, timedelta
DB接続

事前に外部接続を許可する等して接続できるようにしておく必要があります。

mongo_client = MongoClient('ホスト名:27017')
db=mongo_client['DB名']
コレクションの一覧確認
for c in db.collection_names():
    print(c)
||

*** インサート
>|python|
# インサート
db.book_collection.insert_one(
    {
        'category': 'it',
        'title':  '入門 python3',
        'synopsis': 'Pythonが誕生して四半世紀。データサイエンスやウェブ開発、セキュリティなどさまざまな分野でPythonの人気が急上昇中です。',
        'publish': 'オライリー',
        'authors': ['Bill Lubanovic', '斎藤康毅', '長尾高弘'],
        'created': datetime.strptime("2015-12-01", "%Y-%m-%d")
    }
)

以下の検索、集約では上でインサートしたフィールド情報を持ったコレクションを使って行います。

検索
# 条件なし検索
for c in db.book_collection.find().limit(5):
    print(c)

# ○条件付き created > "2016-09-01"
for c in db.book_collection.find({'created': {'$gt': datetime.strptime("2016-09-01", "%Y-%m-%d")}}).limit(5):
    print(c)

# ◯複数条件 created > "2016-09-01" と category = 'fantasy'
for c in db.book_collection.find({
        'created': {'$gt': datetime.strptime("2016-09-01", "%Y-%m-%d")},
        'category': 'fantasy'
    }).limit(5):
    print(c)
    
#◯条件 authorsに田中太郎が含まれる
for c in db.book_collection.find({'authors': {"$in":[u"田中太郎"]}}).limit(5):
    print(c)

# ◯条件指定を変数に格納する created > "2016-10-01"
condition={'created': {'$gt': datetime.strptime("2016-10-01", "%Y-%m-%d")}}
for c in db.book_collection.find(condition).limit(5):
    print(c)

# ソート
# ◯createdで昇順にソート
for c in db.book_collection.find({},{'title': 1, 'created': 1} ).sort('created', pymongo.DESCENDING):
    print(c) 
集約
# 集約
# ◯publishで集約した件数を表示
for c in db.book_collection.aggregate([
        { '$group' : {'_id': "$publish", 'count':{ '$sum': 1 } }}
]):
    print(c)

# ◯createdの年、月、日で集約した件数を表示
for c in db.book_collection.aggregate([
        { '$group' : {'_id': {'month': { '$month': '$created'}, 'day': { '$dayOfMonth': '$created' }, 'year': { '$year': "$created" }}, 'count':{ '$sum': 1 } }}
]):
    print(c)


#◯publichとcreatedの年で集約した件数を表示
for c in db.book_collection.aggregate([
        { '$group' : {'_id': {'publish': "$publish", 'year': { '$year': "$created" }}, 'count':{ '$sum': 1 } }}
]):
    print(c)

# ◯createdの年、月、日で集約してソートした件数を表示
for c in db.book_collection.aggregate([
        { '$group' : {'_id':{'year': { '$year': "$created" }, 'month': { '$month': "$created" } }, 'count':{ '$sum': 1 } }},
        { '$sort': {'_id.month':1}}
]):
    print(c)


# ◯createdの年、月、日で集約し件数上位3件を表示
for c in db.book_collection.aggregate([
        { '$group' : {'_id':{'year': { '$year': "$created" }, 'month': { '$month': "$created" } },'count':{ '$sum': 1 } }},
        { '$sort': {"count":-1}},
        { '$limit': 3 }
]):
    print(c)

# ◯authorに田中太郎が含まれるものをpublichで集約して表示
for c in db.book_collection.aggregate([
        { '$match': {'authors': {"$in":['田中太郎']}}},
        { '$group' : {'_id': "$publish", 'count':{ '$sum': 1 } }},
        { '$sort': {"count":-1}}
]):
    print(c)

# マップリデュースによるpublishごとの集約
mapper = Code("""
        function () {
            emit(this.publish, 1);
        }
 """)

reducer = Code("""
        function (key, values) {
            var total = 0;
            for (var i = 0; i < values.length; i++) {
                total += values[i];
            }
            return total;
        }
""")

result = db.book_collection.map_reduce(mapper, reducer, "myresults")
for doc in result.find():
    print(doc['_id'], ':', doc['value'])

MongoDB覚書

MongDB

MongoDBをcliから操作時のメモです。Mngoのインストール等を試した環境はCentOS7.2になります。

CentOS7環境でのインストール

リポジトリ追加 公式ページを確認しyumリポジトリを追加する(https://www.mongodb.com/download-center#community)

/# vi /etc/yum.repos.d/mongodb.repo

[mongodb-org-3.4]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/3.4/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.4.asc

MongoDB起動

# systemctl start mongod

mongo接続

# mongo

mongo接続終了

exit

外部アクセスを許可する(IP指定なし)

/# vi /etc/mongod.conf

net:
  port: 27017
  #bindIp: 127.0.0.1,192.168.1.15

bindIpを未指定または"bind_ip: 0.0.0.0"にすることで接続元のIPアドレスを制限しないようにできます。

外部アクセスを許可する(IP指定あり)

/# vi /etc/mongod.conf

net:
  port: 27017
  bindIp: 127.0.0.1,192.168.1.15

bindIpを指定することでアクセス元のIPアドレスを制限できます。

bsonのリストア

# mongorestore –host localhost –db DB名 bsonファイル

DB一覧表示

show dbs

DBの選択

use DB名

DBの作成

use DB名

コレクションの作成

db.createCollection(“test_collection”)

DBの削除

use DB名 db.dropDatabase()

コレクション一覧確認

show collections

挿入

db.コレクション名.insert({
  フィールド名:値,,,,
})

サンプル
db.book_collection.insert({
 category: 'fantasy',
  title: 'タイトル1',
  synopsis: 'あらすじ1',
  publish: '田中出版',
  authors: ['田中太郎', '佐藤賢作'],
  created: ISODate("2016-11-29"),
})

検索

◯テストデータ
db.book_collection.insert({
 category: 'fantasy',
  title: 'タイトル1',
  synopsis: 'あらすじ1',
  publish: '田中出版',
  authors: ['田中太郎', '佐藤賢作'],
  created: ISODate("2016-11-29"),
})
db.book_collection.insert({
 category: 'horror',
  title: 'ホラー1',
  synopsis: ' ホラー1',
  publish: '竹中出版',
  authors: ['田中太郎', '伊藤優一'],
  created: ISODate("2016-08-21"),
})
db.book_collection.insert({
 category: 'fantasy',
  title: 'ファンタジー2',
  synopsis: ' ファンタジー2',
  publish: '田中出版',
  authors: ['加藤信之'],
  created: ISODate("2016-07-15"),
})

db.book_collection.insert({
 category: 'SF',
  title: 'SF1',
  synopsis: ' SF1',
  publish: 'はなまる文庫',
  authors: ['田中賢介'],
  created: ISODate("2016-09-15"),
})

db.book_collection.insert({
 category: 'SF',
  title: 'SF2',
  synopsis: ' SF2',
  publish: 'はなまる文庫',
  authors: ['田中賢介'],
  created: ISODate("2016-09-16"),
})
○一度に取得する件数変更(デフォルトでは20件ずつのデータ取得になっていたはず)
DBQuery.shellBatchSize = 200


○条件なし
db.コレクション名.find()

◯条件付き x > 1
db.コレクション名.find({x: {$gt: 1}})


○条件付き created > "2011-11-01"
db.コレクション名.find({created: {$gt: ISODate("2011-11-01T00:00:00+09:00")}})

◯複数条件 created > "2011-11-01" と category = 'fantasy'
db.コレクション名.find({
      created: {$gt: ISODate("2011-11-01T00:00:00+09:00")},
      category: 'fantasy'
      }
)
・sample
db.book_collection.find({
      created: {$gt: ISODate("2011-11-01T00:00:00+09:00")},
      category: 'fantasy'
      }
)

◯条件 authorsに田中太郎が含まれる
db.コレクション名.find({authors: {"$in":['田中太郎']}})


◯条件 作者の名前が田中で前方一致する
db.コレクション名.find({authors: {"$in":[/^田中/]}})
*/をつける時はシングルクォーテーションを外さないと見つからなかった

◯表示するフィールドを指定する titleのみ表示
db.コレクション名.find({}, {'title': 1})


◯条件指定を変数に格納する created > "2016-08-01"
condition={created: {$gt: ISODate("2016-08-01T00:00:00+09:00")}}
db.コレクション名.find(condition)

◯表示条件を変数に格納する titleのみ表示
view={title:1}
db.コレクション名.find({}, view)

ソート

◯createdで昇順にソート
db.コレクション名.find().sort({created:1})

集約

◯publishで集約した件数を表示
db.コレクション名.aggregate([
        { $group : {_id: "$publish", count:{ $sum: 1 } }}
])


◯createdの年、月、日で集約した件数を表示
db.コレクション名.aggregate([
        { $group : {_id: {month: { $month: "$created" }, day: { $dayOfMonth: "$created" }, year: { $year: "$created" }}, count:{ $sum: 1 } }}
])

◯publichとcreatedの年で集約した件数を表示
db.コレクション名.aggregate([
        { $group : {_id: {publish: "$publish", year: { $year: "$created" }}, count:{ $sum: 1 } }}
])

◯createdの年、月、日で集約してソートした件数を表示
db.コレクション名.aggregate([
        { $group : {_id:{year: { $year: "$created" }, month: { $month: "$created" } },count:{ $sum: 1 } }},
        { $sort: {"_id.month":1}}
])

◯createdの年、月、日で集約し件数上位3件を表示
db.コレクション名.aggregate([
        { $group : {_id:{year: { $year: "$created" }, month: { $month: "$created" } },count:{ $sum: 1 } }},
        { $sort: {"count":-1}},
        { $limit: 3 }
])

◯authorに田中太郎が含まれるものをpublichで集約して表示
db.コレクション名.aggregate([
      { $match: {authors: {"$in":['田中太郎']}}},
        { $group : {_id: "$publish", count:{ $sum: 1 } }},
        { $sort: {"count":-1}}
])

CentOS7でユーザのコマンドに制限をかける

以前特定のコマンドしか実行できないユーザを作る必要があった時にrbashを使用したのが便利だったので、その時のメモになります。確認した環境はCentOS 7.2になります。

1. rbashを使えるようにする
# ln -s /bin/bash /bin/rbash
# vi /etc/shells

/bin/rbash ←この行を追加

2. 実行シェルの変更
# su 制限するユーザ
$ cssh
新しいシェル [/bin/bash]: /bin/rbash ←「/bin/rbash」を入力

3. bash_profileの所有者変更および
rootにユーザーを切り替えてtenda_keieiのbash_profileの所有者と所有グループを変更
# chown root:root ./bash_profile
# chmod 755 .bash_profile

4. 実行可能コマンドの設定
# mkdir /home/制限するユーザ/command
# ln -s /usr/bin/java /home/制限するユーザ/java
# ln -s /usr/bin/date /home/制限するユーザ/date
# ln -s /usr/bin/mv /home/制限するユーザ/mv

 
5. 実行可能コマンドをpathに追加
# vi /home/tenda_keiei/.bash_profile

# settings for rbahs
# PATH=$PATH:$HOME/.local/bin:$HOME/bin
PATH=/home/制限するユーザ
export PATH=$PATH:/home/制限するユーザ/command

export PATH

javaで作ったバッチを実行するときとか
・実行するスクリプトの置き場所もpathに追加しておく
javaコマンドでクラスパスを指定する場合、bash_profileないで事前にクラスパス用の環境変数を用意しておき、それを使うようにする(rbashでは/を含んだコマンドを実行できない!)

◯その他
コマンド制限かけたユーザでバッチを実行する場合は以下の点に注意が必要になります。
・cronはrbashを有効にする前であれば設定できる。
・cronの場合にbash_profileで設定した環境変数が使えなかったら、rootユーザとかでにcronを設定しておきsourceで読み込ませる。(制限ユーザに対してsourceコマンドを有効にすることでも対応可能かは未確認です。)
0 3 * * * source /home/ruser/.bash_profile; ◯◯◯.sh

Chainerで簡単なクラス分類をしてみる

Chainerを試してみるために簡単なサンプルプログラムを動かしてみたいと思います。

まず必要なライブラリをインポートします。

import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, Variable
from chainer import optimizers, serializers, utils
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L

それから、学習用データを読み込みます。今回はsklearnからアヤメのデータを使って4入力(がくの長さ、幅と茎の長さ、幅)からアヤメの種類(setosa, versicolor, virginica)を分類できるように試してみます。

# Set data
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data.astype(np.float32)
Y = iris.target.astype(np.float32)
N = Y.size
Y2 = np.zeros(3 * N).reshape(N,3).astype(np.float32)
for i in range(N):
    Y2[i,np.int(Y[i])] = 1.0

index = np.arange(N)
xtrain = X[index[index % 2 != 0],:]
ytrain = Y2[index[index % 2 != 0],:]
xtest = X[index[index % 2 == 0],:]
yans = Y[index[index % 2 == 0]]

モデルの定義として4入力、3出力で中間層はなしのものを定義しています。入力に対して出力が簡単に結びつくような今回のケースでは中間層は必要なさそうに思います。多クラスの分類としてはソフトマックス関数を使用し誤差関数として二乗誤差を使用しています。ソフトマックスを利用した多クラス分類等では交差エントロピーを使うように思っていたのですが、この辺りはちゃんと学習して使いこなせるようにしたいと思います。

# Define model
class IrisRogi(Chain):
    def __init__(self):
        super(IrisRogi, self).__init__(
            # 入力4軸(がくの長さ、幅と茎の長さ、幅) 出力3クラス分類(irisの種類setosa, versicolor, virginica)
            l1=L.Linear(4,3),
        )

    def __call__(self,x,y):
        # 順伝番した結果に対してmean_squared_errorで二乗誤差求めている
        return F.mean_squared_error(self.fwd(x), y)

    def fwd(self,x):
        # 多クラス分類(irisの種類)のための各ユニットの出力としてソフトマックスを使用する
        return F.softmax(self.l1(x))

# Initialize model

model = IrisRogi()
# パラメータの最適化で比較的高速に良い値を出すadamを使用する
optimizer = optimizers.Adam()
optimizer.setup(model)


それから学習を行います。

# Learn

n = int(index.size / 2)
bs = 25
for j in range(5000):
    sffindx = np.random.permutation(n)
    accum_loss = None
    for i in range(0, n, bs):
        x = Variable(xtrain[sffindx[i:(i+bs) if (i+bs) < n else n]])
        y = Variable(ytrain[sffindx[i:(i+bs) if (i+bs) < n else n]])
        model.zerograds() # 勾配を初期化
        loss = model(x,y) # 順方向に計算し誤差を算出
        loss.backward() # 逆伝番で勾配の向きを計算
        optimizer.update() # 逆伝番で得た勾配からパラメータを更新する

学習で得たパラメータを利用しテストをしてみます。

# Test
xt = Variable(xtest, volatile='on')
yy = model.fwd(xt)

ans = yy.data
nrow, ncol = ans.shape
ok = 0
for i in range(nrow):
    cls = np.argmax(ans[i,:])
    # print( ans[i,:], cls)
    if cls == yans[i]:
        ok += 1

print (ok, "/", nrow, " = ", (ok * 1.0)/nrow)

自分の環境で動かしてみたところ、73/75の精度で正解していたのでちゃんと動いてることは確認できます。

Scalaのfor式とflatMapについて

自分は職場では良くjava7でプログラミングするのでjava8のStream APIなどを触ることがないのですが、そのような状態だとscalaのfor式やflatMapに抵抗があったので簡単に使い方だけでもおさらいしてみたいと思います。

まず、動作確認で使うクラスは以下になります。Todo管理用のクラスを用意し、特定の作業者のタスクを検索するプログラムを試します。

  case class Todo(
    TodoId: Int, 
    CategoryId: Int, 
    Title: String, 
    Text: String, 
    Order: Int, 
    worker: List[String]){
    override def toString = Text
  }
  case class TodoCategory(
    CategoryId: Int, 
    Category: String, 
    Order: Int, 
    TodoList: List[Todo])

それから動作確認ようデータは以下になります。

  val collecting_doc = 
    Todo(1, 1, "collect data", "資料を集める", 1, 
      List("sato", "nakata"))
  val prepare_tool = 
    Todo(2, 1, "prepare tool", "ツールを準備する", 2, 
    List("kato", "nakata"))
  val investigate = 
    Todo(3, 2, "investigate", "調査する", 1, 
    List("sato"))
  val summarize_result = 
    Todo(4, 2, "summarize result", "調査結果をまとめる", 2, 
    List("kato", "nakata"))
  val review = 
    Todo(5, 2, "review", "レビューを受ける", 3, 
    List("sato", "nakata"))
  val presentation = 
    Todo(6, 3, "presentation", "発表する", 1, 
    List("sato"))
  val improve = 
    Todo(7, 3, "improve", "改善する", 2, 
    List("sato"))

  val done = TodoCategory(1, "完了", 1, 
    List(collecting_doc, prepare_tool))
  val doing = TodoCategory(2, "実施中", 2, 
    List(investigate, summarize_result, review))
  val next = TodoCategory(3, "待ち", 3, 
    List(presentation, improve))

  val allTodo: List[TodoCategory] = List(done, doing, next)

for式を使い特定の作業者のタスクを検索する場合は以下のようになります。

  def workerTaskFor(name: String): List[Todo] ={
    for(
      category <- allTodo;
      todo <- category.TodoList;
      tw <- todo.worker
      if tw eq name
    ) yield todo
  }

"category <- allTodo;"の部分でallTodoの一つずつの要素がcategoryには入ります。同様にtw <- todo.workerではtodoに設定されている一人一人のwoerkerが入り if tw eq nameにより名前での絞り込みを行っています。最後にyield todoの部分で絞り込まれたtodoでListを作成しています。


次にflatmapを使った場合は以下のようになります。

  def workerTaskFlatmap(name: String): List[Todo] = {
    allTodo flatMap(category =>
      category.TodoList flatMap( todo =>
        todo.worker withFilter (worker => worker eq name) map( 
          worker =>
            todo
        )
      )
    )
  }

やっていることはfor式と同じなのですがworkerの絞り込みにwithFilterというコレクションAPIを使用しています。コレクションAPIはTraverable を実装するコレクションクラスで利用することができ、これに慣れておくとデータの操作周りがすごい快適になると思います。自分の実感からもこの辺りになれるかどうかでScalaに感じる抵抗がぐっと変わってくるのかなという気がします。

最後にプログラムの全体は以下のようになりました。

object MyStudy {
  case class Todo(TodoId: Int, CategoryId: Int, Title: String, Text: String, Order: Int, worker: List[String]){
    override def toString = Text
  }
  case class TodoCategory(CategoryId: Int, Category: String, Order: Int, TodoList: List[Todo])

  val collecting_doc = Todo(1, 1, "collect data", "資料を集める", 1, List("sato", "nakata"))
  val prepare_tool = Todo(2, 1, "prepare tool", "ツールを準備する", 2, List("kato", "nakata"))
  val investigate = Todo(3, 2, "investigate", "調査する", 1, List("sato"))
  val summarize_result = Todo(4, 2, "summarize result", "調査結果をまとめる", 2, List("kato", "nakata"))
  val review = Todo(5, 2, "review", "レビューを受ける", 3, List("sato", "nakata"))
  val presentation = Todo(6, 3, "presentation", "発表する", 1, List("sato"))
  val improve = Todo(7, 3, "improve", "改善する", 2, List("sato"))

  val done = TodoCategory(1, "完了", 1, List(collecting_doc, prepare_tool))
  val doing = TodoCategory(2, "実施中", 2, List(investigate, summarize_result, review))
  val next = TodoCategory(3, "待ち", 3, List(presentation, improve))

  val allTodo: List[TodoCategory] = List(done, doing, next)


  def main(args: Array[String]): Unit ={

    workerTaskFor("nakata") foreach(println)

    workerTaskFlatmap("nakata") foreach(println)

  }

  def workerTaskFor(name: String): List[Todo] ={
    for(
      category <- allTodo;
      todo <- category.TodoList;
      tw <- todo.worker
      if tw eq name
    ) yield todo
  }

  def workerTaskFlatmap(name: String): List[Todo] = {
    allTodo flatMap(category =>
      category.TodoList flatMap( todo =>
        todo.worker withFilter (worker => worker eq name) map( 
          worker =>
            todo
        )
      )
    )
  }
}