msawady’s learning memo

ただのJavaエンジニアが、学んだことをログっていくところ

【AWS】【Python】Lambdaを利用して他のAWSアカウントのEC2インスタンスを再起動する

Lambdaを利用した他アカウントの操作

やったことの流れは以下の通り。

  • IAMロール/ポリシーの設定、信頼関係の編集
  • 他のアカウントのIAMロールにSTSしてEC2クライアントを取得
  • tagの値で対象のインスタンスを抽出し、再起動

IAMロール/ポリシーの設定

Lambdaにアタッチするロールと、他のアカウント(操作対象のインスタンスを保持しているアカウント)側のIAMロールの2つを編集する必要があります。

LambdaのIAMロール/ポリシー

今回のケースで必要な権限(ポリシー)は以下の2つです。

  • AWSLambdaExecute (built-in):
    Lambda実行のための基本的な権限です。具体的には、cloudwatch logs へのフルアクセス、S3へのPut/Get

  • 他のアカウントのIAMロールへのSTS権限:
    他のアカウントのIAMロールへの一時的な認証情報を取得(STS)してインスタンスのコントロールを行うために、Lambda側には「他のアカウントのIAMロールへのSTSを行う権限」を与える必要が有ります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "arn:aws:iam::999999999999:role/OtherAccountRole"
            ]
        }
    ]
}

他のアカウント側のIAMロール/ポリシー

他のアカウント(操作対象のインスタンスを保持しているアカウント)では以下のポリシーを持ったIAMロールを作成します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:RebootInstances"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

また、このIAMロールの信頼済みエンティティに、上記のLambdaロールを追加する必要が有ります。IAM画面から信頼関係の編集を行い、以下のように編集します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:role/RebootLambdaRole"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Lambda のコード、テストイベント

Python3.6で書きます。

ソースコード

'''
AWS Lambda script for reboot instances.
This scripts need STS for IAM Role on Another Account.
'''

import boto3


def lambda_handler(event, context):
    sts_client = boto3.client('sts')
    sts_res = sts_client.assume_role(
        RoleArn=event['sts_target'],
        RoleSessionName='RebootOtherAccount'
    )
    cred = sts_res['Credentials']

    ec2_client = boto3.client(
        'ec2',
        aws_access_key_id=cred['AccessKeyId'],
        aws_secret_access_key=cred['SecretAccessKey'],
        aws_session_token=cred['SessionToken'],
    )

    full_info = ec2_client.describe_instances(
        Filters=[
            {
                'Name': 'tag:need_reboot',
                'Values': ['true']
            }
        ]
    )
    
    instance_ids = []
    for r in full_info['Reservations']:
        for i in r['Instances']:
            instance_ids.append(i['InstanceId'])

    return ec2_client.reboot_instances(
        DryRun=event.get('dry_run', True),
        InstanceIds=instance_ids,
    )

テストイベント

{
  "dry_run": true,
  "sts_target": "arn:aws:iam::999999999999:role/OtherAccountRole"
}

解説と補足

  • テストイベントから取得した他アカウントのIAMロールへのSTSを行い、取得した認証情報を利用してEC2クライアントを作成します。STSを利用して他アカウントのリソースにアクセスする場合は、clientの初期化時に以下のようにする必要があります。
    cred = sts_res['Credentials']

    ec2_client = boto3.client(
        'ec2',
        aws_access_key_id=cred['AccessKeyId'],
        aws_secret_access_key=cred['SecretAccessKey'],
        aws_session_token=cred['SessionToken'],
    )
  • 対象となるインスタンスをtagの値でフィルターします。ここではneed_reboot というタグにtrueと書いてあるインスタンスを抽出しています。
full_info = ec2_client.describe_instances(
        Filters=[
            {
                'Name': 'tag:need_reboot',
                'Values': ['true']
            }
        ]
    )
  • 細かいですが、DryRunフラグはちゃんと付けておきたいですね。
    return ec2_client.reboot_instances(
        DryRun=event.get('dry_run', True),
        InstanceIds=instance_ids,
    )

終わりに

以上です。IAMロールの設定のところで結構ハマりましたが、IAMロール/ポリシー、信頼関係などについて学ぶことが出来ました。 業務ではこんな感じのLambdaを書くことが増えたので、ブログの方にもメモを残していきたいと思います。

【GitLab】【Python】GitLabにユーザーを一括で登録、グループ参加

GitLab API を使ってユーザー一括登録

必要なもの

  • Python3
  • GitLabのAdminユーザーのprivate access token

python-gitlab のセットアップ

  • pip を利用してpython-gitlab のインストール
pip install python-gitlab 
  • 以下のような設定ファイル(python-gitlab.cfg)を用意
[default]
url = https://gitlab.somewhere.com/
private_token = AdminAccessToken
timeout = 5
ssl_verify = True
api_version = 4

ソースコード

#!/usr/bin/env python3

import argparse
import csv
import gitlab


def add_user(api, csvfile):
    with open(csvfile, 'r') as cf:
        csvreader = csv.reader(cf, delimiter=',', quotechar='|')
        for row in csvreader:
            params = {}
            params['email'] = row[0]
            params['username'] = row[1]
            params['name'] = row[2]
            params['reset_password'] = True
            user = api.users.create(params)
            print('User:{} has created'.format(user))


def add_user_to_group(api, csvfile):
    all_group = api.groups.list()
    with open(csvfile, 'r') as cf:
        csvreader = csv.reader(cf, delimiter=',', quotechar='|')
        for row in csvreader:
            user = api.users.list(username=row[0])[0]
            for i in range(1, len(row)):
                group = [g for g in all_group if g.full_path == row[i]][0]
                params = {}
                params['user_id'] = user.id
                params['access_level'] = gitlab.DEVELOPER_ACCESS
                group.members.create(params)
                print('User:{0} has joined in {1}'.format(user.name, group.name))


if __name__ == "__main__":
    parser=argparse.ArgumentParser()
    parser.add_argument('--csvfile')
    parser.add_argument('--group', action='store_true')
    args=parser.parse_args()

    api=gitlab.Gitlab.from_config(config_files=['./python-gitlab.cfg'])
    gl.session.proxies = {
          'http': 'http://yourproxy.com:9090',
          'https': 'https://yourproxy.com:8443'
    }

    if args.group:
        add_user_to_group(api=api, csvfile=args.csvfile)
    else:
        add_user(api=api, csvfile=args.csvfile)

補足と解説

  • 設定ファイルを読み込んでGitLabのAPIを初期化して、そのapiを利用してユーザー登録/グループ参加 を行います
  • Proxy環境で使いたいときは、apiにproxy設定をいれます
    api=gitlab.Gitlab.from_config(config_files=['./python-gitlab.cfg'])
    gl.session.proxies = {
          'http': 'http://yourproxy.com:9090',
          'https': 'https://yourproxy.com:8443'
    }
  • ユーザー登録時にパスワードを設定させるメールを送るようにしています
            params['reset_password'] = True

決まった初期パスワードを設定したいときは以下のようにします

            params['password'] = 'secret_p@ssw0rd'
  • Group に追加するときはDeveloper として登録されます
                params['access_level'] = gitlab.DEVELOPER_ACCESS

使い方

ユーザー登録

  • 以下のようなcsvファイルを用意する。左からメールアドレス、ログインID、表示名です。
tom.brady@patriots.com,tom.brady,Tom Brady
david.andrews@patriots.com,david.andrews,David Andrews
patrick.chung@patriots.com,patrick.chung,Patrick Chung
./add-user --csvfile filename.csv

ユーザーのグループ参加

  • 以下のようなcsvファイルを用意する。一番左にユーザーのログインID、右に登録したいグループをフルパスで並べます。
tom.brady,offence
david.andrews,offence,offence/lines
patrick.chung,defence
./add-user --group --csvfile filename.csv

終わりに

  • やっつけでつくったものですが、それなりに綺麗にかけたかなと。
  • グループ登録時の権限指定はファイルに書くようにしたいですね。
  • コードやサンプルファイルはGitHubに置きました。よろしければご利用ください。

github.com

業務ではPythonを使うことが多くなってきました。こういうことをパッと出来ると成長を感じますね。 AWSのLambdaなども書く機会が増えてきたので、Python繋がりでそういう記事も書きたいです。

2018年にエンジニアとして学びたいこと

新年の抱負的なことを書いてみる

  • 皆様あけましておめでとうございます。
  • 新年ということで今年学びたいと思っていることを書きます。
  • ざっくりな方向性としては以下です。

英語

現在のプロジェクトが多国籍チームということもあり、英語でコミュニケーションを取りたいことが多々あります。
一方で、直属の上司が日本語ネイティブ(のイギリス人!)ということもあり、あまり危機感を持って取り組めていないのも現状です。
もちろん英語を使うことは増えましたが、「英語力が上がった」というよりは「英語混じりでのコミュニケーション能力が上がった」というほうが正しい表現です。

また、直属の上司に言われたことですが、

  • そもそも「まともなエンジニア」を見つけるのがまず大変(これは日本に限らない話だそうです)
  • そして日本で「英語を話せる人」を見つけるのもなかなか大変
  • なので「英語を話せるまともなエンジニア」は外資系企業が血眼になって探している

ので、英語を話せるだけで転職市場での価値が大きく上がるとのことでした。

仕事を上手く進めるためだけでなく、自分の今後のキャリアを広げるためにも、今年こそちゃんと勉強したいと思います。 具体的には、TOEFL のスコアを上げていきたいと思います。Writing や Speaking もあるので、アウトプット能力を高めることにも繋がっていくと考えています。

コンピュータサイエンス、インフラの基礎

現在のプロジェクトではAWSを利用したインフラ/運用設計を行っています。
文系学部を卒業しアプリのエンジニアをやってきた自分としては、インフラやネットワークの知識が足りないと感じることが増えています。

また先輩からも「コンピュータサイエンスの基礎を知ってるだけでも問題を切り分けるセンスが大きく上がる」とも言われています。
今後、開発や運用の中でもう1段シニアなロールを担っていくにあたり、コンピュータサイエンスの基礎を学んでいくことが重要だと考えています。

具体的には、基本情報技術者応用情報技術者の資格を取ろうと思います。
資格が重要視される業界でもないですが、知識レベルのベンチマークとしては良いという声をよく聞くので、春に基本情報、秋には応用情報を取ることを目標にしたいと思います。

その他、各種技術

上に書いたような英語や基礎知識だけでなく、個別の技術にもキャッチアップしていきたいと思っています。
今時点で考えているのは、AWSとフロントエンドの2つです。

AWS

現在のプロジェクトでメインに使う分野なので、もっと習熟したいと思っています。
仕事ではシニアな人が大きな部分の設計をしてくれたりCloudFormationをゴリゴリ書いてくれたりしているのですが、自分でもそういったことをできるような知識や能力をつけていきたいです。
アプリのエンジニアをやっていくにしても、クラウドの知識は必要だと考えているので、これを機会に勉強したいと思います。

フロントエンド

フロントエンドも仕事で扱ってきましたが、これもシニアな人がベースとなる部分を作ってくれた上で開発を行ってきただけなので、もう少し深く理解をしたいと考えています。
ReactやVueといったFrameworkをいくつか理解すれば、日進月歩のフロントエンド界にキャッチアップするための足がかりを作れるのではと期待しているので、自習ベースで頑張っていきたいと思います。

おわりに

去年の夏頃にブログを初めて、やはりアウトプットの習慣がある方が良いということは実感しました。
11月頃を境に公私共に忙しくなり、更新回数が減ってしまったのは反省点です。
2018年は仕事やプライベートが忙しい中でもブログを書くという習慣を作っていきます。
勉強したいことも山ほど有る中で、ブログにアウトプットしながら精進していきたいと思います。

【Python】key と value が連なった List を Dict に変換する

Python の tips を備忘代わりに

  • プロジェクトが変わりまして、Pythonを触ることが増えました
  • 掲題の件で悩んだので、パフォーマンスの検証も含めて post しておきます

やりたいこと

こういう List を

hoge_list = ['key1',  'value1',  'key2',  'value2',  'key3',  'value3',,,,]

こういう Dict にしたい

hoge_dict = {'key1':  'value1',  'key2':  'value2',  'key3':  'value3',,,,}

これだけ。

調べて出てきたやり方

困ったときは、stackoverflow ですね。 stackoverflow.com

method1

hoge_dict = dict(zip(hoge_list[0::2], hoge_list[1::2]))

綺麗なワンライナーですね。奇数番目の要素を取り出した List と、偶数番目の要素を取り出した List を zip するというやり方です。

method2

hoge_dict = { hoge_list[i]: hoge_list[i+1] for i in range(0, len(hoge_list), 2) }

dict comprehension を使って、値を順々に key: value に入れていくというやり方です。

method3 (Python3 のみ)

i = iter(hoge_list)
hoge_dict = dict(zip(i, i))

Python3 からは zip が遅延評価になっているので、このやり方で iterator から順々に要素を取り出して zip することができます。

パフォーマンス検証

検証用コード

1万個の key と value のセットを並べたリストを、それぞれのやり方で1万回ずつ回して、実行時間を取ります。

from datetime import datetime


def method1(hoge_list):
    return dict(zip(hoge_list[0::2], hoge_list[1::2]))


def method2(hoge_list):
    return {hoge_list[i]: hoge_list[i + 1] for i in range(0, len(hoge_list), 2)}


def method3(hoge_list):
    i = iter(hoge_list)
    return dict(zip(i, i))


if __name__ == '__main__':
    hoge_list = []
    for i in range(0, 10000):
        hoge_list.append('key{}'.format(i))
        hoge_list.append('value{}'.format(i))

    start = datetime.now()
    for i in range(0, 10000):
        method1(hoge_list)
    time1 = datetime.now() - start

    start = datetime.now()
    for i in range(0, 10000):
        method2(hoge_list)
    time2 = datetime.now() - start

    start = datetime.now()
    for i in range(0, 10000):
        method3(hoge_list)
    time3 = datetime.now() - start

    print('time1: {}'.format(time1))
    print('time2: {}'.format(time2))
    print('time3: {}'.format(time3))

結果

time1: 0:00:13.092214
time2: 0:00:24.749644
time3: 0:00:09.524495

method3 < method1 <<< method2 という結果になりました。

  • method3 が method1 より速いのは納得です。「1回 key のリストと value のリストを作ってから zip する」のと、「順々に値を取り出してzipする」のでは、後者のほうがメモリコピーの回数が少ないので速くなりそうですね。
  • method2 がこんなに遅いのがちょっとビックリでした。調べたところ、Dict Comprehension は内部的に update() を呼んでいるようなので、「1回ずつupdate()を行う」のと、「zipでまとめてからdictに変換する」のでは確かに後者のほうが速そうですね。

結論

method3を使いましょう。

i = iter(hoge_list)
hoge_dict = dict(zip(i, i))

参考ページ

  • 各種 comprehension の内部的な挙動(英語)

Stupid Python Ideas: How do comprehensions work?

  • zip の内部的な挙動 (英語)

How does Python's zip function work internally? - Quora

【Scala】【mongoDB】mongoDB のレコードを case クラスにマッピングする

株のポジション管理を出来るようにしたいと思っています

  • 大分放置してしまいましたが、ちょっとずつ再開します。
  • まずは銘柄一覧をブラウザから見えるようにします。
  • http://kabusapo.com/ranking/ からダウンロードしたcsvファイルを case クラスにパースしてmongoDBにインサート→ mongoDBからフェッチして画面表示しました
  • クライアントなどは前回のTODO管理で作ったものをほとんど再利用しています。

msawady.hatenablog.com

コード

Stock.scala

package services.domain

import org.mongodb.scala.bson.ObjectId
import org.mongodb.scala.bson.codecs.Macros._
import org.mongodb.scala.bson.codecs.DEFAULT_CODEC_REGISTRY
import org.bson.codecs.configuration.CodecRegistries.{fromRegistries, fromProviders}


case class Stock(_id: ObjectId, code: String, name: String, market: String, industryType: String, unit: Int, isNK225: Boolean)

object Stock {
  def apply(code: String, name: String, market: String, industryType: String, unit: Int, isNK225: Boolean): Stock 
  = new Stock(new ObjectId, code, name, market, industryType, unit, isNK225)
  
  implicit val codecRegistry = fromRegistries(fromProviders(classOf[Stock]), DEFAULT_CODEC_REGISTRY)
}

Entityクラスはこんな感じで書きました。 * mongoDBのユニークキーであるObjectIdを生成してからインスタンス化するようにしています。 * codecRegistry は クラスとJsonのマッパーのようなものだと解釈しています。

StockRepositry.scala

package services.repository

import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Singleton

import org.mongodb.scala.{MongoClient, MongoCollection}
import play.Environment
import services.domain.Stock
import services.repository.Helpers._

import scala.io.Source

@Singleton
class StockRepository {

  val mongoClient = MongoClient()
  val database = mongoClient.getDatabase("database").withCodecRegistry(Stock.codecRegistry)
  val initialized = new AtomicBoolean()


  def toStock(data: String): Stock = {
    val el = data.split(",")
    Stock(el(0), el(1), el(2), el(3), safeStringToInt(el(4)).getOrElse(0), if (el.length > 5) "1".equals(el(5)) else false)
  }

  def initializeIfNeeded(): Unit = {
    if (initialized.get()) {
      return
    }

    val collection: MongoCollection[Stock] = database.getCollection("stock")
    collection.drop().results()

    val s = Source.fromFile(Environment.simple().getFile("data/stocklist.csv"), "UTF-8")
    val lines = s.getLines().toList
    val stocks: List[Stock] = lines.tail.map(l => toStock(l)).toList

    collection.insertMany(stocks).results()
    this.initialized.set(true)
  }

  def getStockList(): List[Stock] = {
    initializeIfNeeded()

    val collection: MongoCollection[Stock] = database.getCollection("stock")
    return collection.find().results().toList
  }

  def safeStringToInt(str: String): Option[Int] = {
    import scala.util.control.Exception._
    catching(classOf[NumberFormatException]) opt str.toInt
  }

}
  • AtomicBoolean を初期化済みフラグとして、初期化されていなかったらファイルの読み込み→インサートとしています。

    • ホントはファイルをダウンロードしたいんだけど上手く行かなかった...
  • tailを使うことで1列目を読み飛ばす(= ヘッダーを読み飛ばす)ことが出来ます。

    val stocks: List[Stock] = lines.tail.map(l => toStock(l)).toList
  • 文字列からオブジェクトにパースする際に、単元株数の列に"単元制度なし"というレコードが有ったので(#^ω^)ピキピキ
  def safeStringToInt(str: String): Option[Int] = {
    import scala.util.control.Exception._
    catching(classOf[NumberFormatException]) opt str.toInt
  }

といったメソッドで Option を返すようにしています。

結果

こんな感じになりました。 f:id:msawady:20171029143445p:plain

次にやること

  • まずは kendo-ui で上手くフィルタリング出来るようにしたい
  • ポジションを登録できるようにする
  • 時価をうまい具合に取れるようにする
  • お気に入り銘柄(ウォッチリスト)を作れるようにする

くらいですかね。ちょっとずつ頑張ります。

【mongoDB】【Scala】mongoDB のインストール → Scala で CRUD する

次のテーマは mongoDB

  • twitter で呟いたりしたんですが、次のプロジェクトではmongoDB使います
  • ただ、自分のタスクはインフラ運用に近い部分なので、実際にどうクエリするかみたいなところは暫く触らなそう
  • なので、自習でやってみることにしました
  • とりあえずインストール→CRUDまでやってみる。

インストール

  • 以下のサイトからダウンロード、インストール。

    MongoDB Download Center | MongoDB

  • データ出力先、ログ出力先のフォルダを作成する

mkdir C\mongo\data
mkdir C\mongo\log
  • 実行ファイルにパスを通す。デフォルトだとC:\Program Files\MongoDB\Server\3.4\bin

  • 以下のコマンドを実行する。コマンドプロンプトは開きっぱなしにする。(.batファイルにしておくと便利)

mongod --dbpath c:\mongo\data --logpath c:\mongo\log\mongodb.log
mongo
  • テスト用のDBを作成 → 接続
use helloMongo

めっちゃ簡単ですね。

scalaCRUDする

依存関係にMongo Scala Driverを追加

build.sbtに以下を追加。

libraryDependencies += "org.mongodb.scala" %% "mongo-scala-driver" % "2.1.0

mongodb.github.io

Observable を処理するための Helper クラス作成

コピペ from 公式チュートリアル

mongo-scala-driver/Helpers.scala at master · mongodb/mongo-scala-driver · GitHub

import java.util.concurrent.TimeUnit

import scala.concurrent.Await
import scala.concurrent.duration.Duration

import org.mongodb.scala._

object Helpers {

  implicit class DocumentObservable[C](val observable: Observable[Document]) extends ImplicitObservable[Document] {
    override val converter: (Document) => String = (doc) => doc.toJson
  }

  implicit class GenericObservable[C](val observable: Observable[C]) extends ImplicitObservable[C] {
    override val converter: (C) => String = (doc) => doc.toString
  }

  trait ImplicitObservable[C] {
    val observable: Observable[C]
    val converter: (C) => String

    def results(): Seq[C] = Await.result(observable.toFuture(), Duration(10, TimeUnit.SECONDS))

    def headResult() = Await.result(observable.head(), Duration(10, TimeUnit.SECONDS))

    def printResults(initial: String = ""): Unit = {
      if (initial.length > 0) print(initial)
      results().foreach(res => println(converter(res)))
    }

    def printHeadResult(initial: String = ""): Unit = println(s"${initial}${converter(headResult())}")
  }

}

CRUD する

import org.mongodb.scala._
import org.mongodb.scala.model.Updates._
import org.mongodb.scala.model.Filters._
import Helpers._

object HelloMongo {

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

    // connect to //localhost:27017
    val mongoClient: MongoClient = MongoClient()
    val database: MongoDatabase = mongoClient.getDatabase("helloMongo")

    // create and get collection
    database.createCollection("stocks").results()
    val stocks: MongoCollection[Document] = database.getCollection("stocks")

    // insert
    stocks.insertOne(Document("code" -> "8410", "name" -> "セブン銀行")).results()
    println("=========after INSERT=========")
    stocks.count().printHeadResult("count: ")
    stocks.find().results().foreach(println(_))

    // update
    stocks.updateOne(equal("code", "8410"), set("tradingUnit", "100")).results()
    println("=========after UPDATE=========")
    stocks.count().printHeadResult("count: ")
    stocks.find().results().foreach(println(_))

    // delete
    stocks.deleteOne(equal("code", "8410")).results()
    println("=========after DELETE=========")
    stocks.count().printHeadResult("count: ")
    stocks.find().results().foreach(println(_))

    // drop connection
    stocks.drop().results()

  }
}

実行結果

=========after INSERT=========
count: 1
Document((_id,BsonObjectId{value=59d0dea2223c756f2445f7eb}), (code,BsonString{value='8410'}), (name,BsonString{value='セブン銀行'}))
=========after UPDATE=========
count: 1
Document((_id,BsonObjectId{value=59d0dea2223c756f2445f7eb}), (code,BsonString{value='8410'}), (name,BsonString{value='セブン銀行'}), (tradingUnit,BsonString{value='100'}))
=========after DELETE=========
count: 0

コード解説

  • コレクション(RDBでいうテーブル)を作成・取得
    database.createCollection("stocks").results()
    val stocks: MongoCollection[Document] = database.getCollection("stocks")
  • ドキュメント(RDBでいうレコード)をINSERT
stocks.insertOne(Document("code" -> "8410", "name" -> "セブン銀行")).results()
  • UPDATE

    updateOne だとwhere条件にマッチした最初のドキュメントしか更新されないので注意。 where にマッチした全てのドキュメントを更新したい場合はupdateManyを利用する。
    stocks.updateOne(equal("code", "8410"), set("tradingUnit", "100")).results()
  • DELETE

    update と同様に、deleteOneだとマッチした最初のドキュメントしか削除されない。全て消す場合はdeleteMany
    stocks.deleteOne(equal("code", "8410")).results()
  • コレクションの削除

    DROP TABLE がこんなあっさりアプリケーションから出来るってのは中々面白いと思いました。 一時テーブルをアプリ側から作成→削除まで行えるのは何かと便利そうです。
    stocks.drop().results()

感想

  • mongoDB のインストールの早さと簡単さに衝撃を受けました
  • Mongo Scala Driver は公式ドキュメントやサンプルコードがちゃんと揃っているので使いやすいですね
    • ライブラリの"品質" というと api の使い勝手やパフォーマンスが重視されるけど、こういうところが地味に大事だと思う。
  • 操作自体は単純なものの、これを使ったエンティティ設計どうするんだろう...というのはやってみないと分からなそう
    • Play Framework と組み合わせてポジション管理とかやってみようと思います

以上です。思ったよりもあっさり出来てびっくりしてます。来週には動くページを作れるように頑張ります。

【Javascript】【DataTables】DataTables を利用した Bootstrap テーブルのフィルタ機能

テーブルのフィルタリングを実装

  • DataTables を使ってフィルタリング機能を実装しました

DataTables | Table plug-in for jQuery

  • フッターにセレクトボックスを利用したエクセル的なフィルタリングを出来るようになっています。

ソース

build.sbtに依存関係を追加

webJarsを利用してインポートを行います。

libraryDependencies += "org.webjars.bower" % "datatables" % "1.10.15"

html の headの中でインポート

    @Html(webJarsUtil.css("jquery.dataTables.min.css"))
    @Html(webJarsUtil.css("dataTables.bootstrap4.min.css"))
    @Html(webJarsUtil.script("jquery.dataTables.min.js"))
    @Html(webJarsUtil.script("dataTables.bootstrap4.min.js"))

DataTables を読み込み

    var todoTable = $('#todoTable').DataTable({
         initComplete: function () {
             this.api().columns().every( function () {
                 var column = this;
                 var select = $('<select><option value=""></option></select>')
                     .appendTo( $(column.footer()).empty() )
                     .on( 'change', function () {
                         var val = $.fn.dataTable.util.escapeRegex(
                             $(this).val()
                         );

                         column
                             .search( val ? '^'+val+'$' : '', true, false )
                             .draw();
                     } );

                 column.data().unique().sort().each( function ( d, j ) {
                     select.append( '<option value="'+d+'">'+d+'</option>' )
                 } );
             } );
         }
     } );

DataTables example - Individual column searching (select inputs) からコピペしています。

var todoTable = $('#todoTable').DataTable(

とすることで、テーブルをDataTableにすることができます。

                 var select = $('<select><option value=""></option></select>')
                     .appendTo( $(column.footer()).empty() )
                     .on( 'change', function () {
                         var val = $.fn.dataTable.util.escapeRegex(
                             $(this).val()
                         );

                         column
                             .search( val ? '^'+val+'$' : '', true, false )
                             .draw();
                     } );

              

html に 空のtfootタグを追加し上記スクリプトを読み込むと、各カラムのフッターにセレクトボックスを追加し、on('change')で選ばれた値と同一の値を持つレコードでフィルタ(検索→再描画)をすることが出来ます。

 column.data().unique().sort().each( function ( d, j ) {
                     select.append( '<option value="'+d+'">'+d+'</option>' )
                 } );

セレクトボックスの中身を作っているところです。各カラムのデータをuniqして、セレクトボックスを作っています。

結果

f:id:msawady:20170919220930p:plain "DOOING" を選ぶと、DOINGのTODOをフィルタすることが出来ます。 f:id:msawady:20170919221048p:plain

感想

  • なんともいえない、「これじゃない」感がありますね....
  • header 側に上手くつけたかったんですが、力及ばず...
  • kendo grid のほうが自分のイメージに合ってるので、次回はこっちへの差し替えをやってみようかなと思います。

demos.telerik.com

  • ライブラリを使いやすいようにハックする力がないと、思ったようにフロントエンドの実装を進められないなぁという小並感。
  • いやまぁ力不足以外のなにものでもないんですが....

以上です。なんとも歯がゆい一週間でした。