Adwaysアドベントカレンダー2017

こんにちは!まっちゃんです。

時が過ぎるのは早いもので、もう12月となりました。

12月と言えばそう!あのイベントですね!

「アドベントカレンダー」

の季節です!!

今年も我らエンジニアブログでやっていきます!

本日よりクリスマスまでの営業日毎日ブログを更新していきます!

Adwaysのエンジニアが日替わりで合計17記事投稿します。

新卒からベテランまで色々な人がそれぞれ好きなジャンルの記事を投稿するので、1つでも誰かの目に止まればいいな。。。と思います。

過去の記事はブログの下の方にリンクを記載しています。

それではみんなでクリスマスを楽しみにしながら、カレンダーを開けていきましょう。。。

f:id:AdwaysEngineerBlog:20161202174449j:plain


12/1

blog.engineer.adways.net

12/4

blog.engineer.adways.net

12/5

blog.engineer.adways.net

12/6

blog.engineer.adways.net

12/7

blog.engineer.adways.net

12/8

blog.engineer.adways.net

12/11

blog.engineer.adways.net

12/12

blog.engineer.adways.net

12/13

blog.engineer.adways.net

12/14

blog.engineer.adways.net

12/15

blog.engineer.adways.net

Selenium-WebDriver を使って工数入力チェック業務を自動化

Adways Advent Calendar 2017 1日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar_2017


こんにちは!まっちゃんです。

さっそくですが、アドベントカレンダーのトップバッターを務めさせていただきます!


さて、本日はある業務を自動化した話を書きます。

現在、私たちのチームでは個人ミッションを掲げています。 自分が掲げた個人ミッションの1つに工数入力チェック業務自動化があります。

毎日自分がどの業務をどれくらい行ったのか、登録するシステムが社内にあります。 基本的に終業時にそのシステムにアクセスをして登録をするのですが、中には忘れる人もいます。 現状は都度チームのマネージャーが確認して、未入力などがあればSlack等でやりとりを行い対応をしています。

これは自動化できるのではないかと、チームのマネージャーであるK先輩にヒアリングしたところ 「入力されているかしか確認をしていない」とのことだったので、 未入力者をSlackへ通知するスクリプトを開発しました。

スクリプトはRubyとSelenium-WebDriverを使いました。

まずはSelenium-WebDriverの導入

今回はGoogle Chromeでどのように動いているのかも見たかったので、下記よりChromeDriverをインストールします。

https://sites.google.com/a/chromium.org/chromedriver/downloads

対象のドライバーをインストールしましたら、 ~/.rbenv/shims/ へ配置します。

コマンドなら下記の感じでダウンロードから、配置までいけると思います。

$ cd ~/tmp
$ wget https://chromedriver.storage.googleapis.com/2.33/chromedriver_linux64.zip
$ unzip chromedriver_linux64.zip
$ cp ~/tmp/chromedriver ~/.rbenv/shims/

Rubyの方では、selenium-webdriver というgemが提供されているので、bundler経由で入れます。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "selenium-webdriver"

Gemfileに記述しましたら、インストールします。

$ bundler install --path vendor/bundle

これで準備は完了です。

ログインをどうしようか

工数入力チェックする際に、ログインをしないといけません。

例えば、このようなログインフォームがあったとします。

<form action="/login" method="post">
  <label>id : <input type="text" name="id"></label><br>
  <label>password : <input type="password" name="password"></label><br>
  <input type="submit" value="login">
</form>

このようなRubyスクリプトを書けばログインすることができます。

#!/usr/bin/env ruby

require 'selenium-webdriver'

def main
  driver = Selenium::WebDriver.for :chrome
  driver.navigate.to 'http://localhost/demo/'

  id_element       = driver.find_element(:name, 'id')
  password_element = driver.find_element(:name, 'password')

  id_element.send_keys ENV['USER_ID']
  password_element.send_keys ENV['USER_PASSWORD']
  password_element.submit

  driver.quit
end

main

実行してみます。

$ USER_ID=machaaaan USER_PASSWORD=************ bundler exec ruby script1.rb

実際に簡易のログインフォームを作ってみてやってみました。

f:id:AdwaysEngineerBlog:20171201144821g:plain

ざっくり解説します。

driver = Selenium::WebDriver.for :chrome
driver.navigate.to 'http://localhost/login/'

webdriver経由でGoogle Chromeを立ち上げ、 対象のページ(今回はログインフォームのあるページ)へアクセスします。

id_element       = driver.find_element(:name, 'id')
password_element = driver.find_element(:name, 'password')

フォームの要素を取得します。

id_element.send_keys ENV['USER_ID']
password_element.send_keys ENV['USER_PASSWORD']
password_element.submit

send_keysメソッドで、取得したフォームの要素に対して入力を行います。

今回は環境変数で外に定義したものを入力します。

submitメソッドで入力確定(キーボード入力で言うエンターですね!)をします。

driver.quit

webdriver経由で立ち上げたGoogle Chromeを閉じます。

ちなみに余談ですが、サーバー側で認証を挟んでいる際は下記のように対応します。

driver.navigate.to "http://#{ENV['AUTH_USER']}:#{ENV['AUTH_PASSWORD']}@localhost/top/"

どの要素を取得しようか

工数を登録するシステムの詳しい仕様を知らなかったので、調べてみました。

どうやら、未入力など異常があった場合は違うclass名になるようです。

...
<th class="column">27</th>
<th class="column">28</th>
<th class="alert-column">29</th>
<th class="alert-column">30</th>
...

これは class="alert-column" を取得すればいけそうです。

if driver.find_elements(:class, 'alert-column').size != 0
  alert_elements = driver.find_elements(:class, 'alert-column')
else
  alert_elements = []
end

find_elementsメソッドで指定した要素をすべて取得します。

alert_elementsには、class="alert-column"の要素で取得されたものが格納されています。

str = ''
alert_elements.each do |item|
  str += "#{item.text}"
end

textメソッドで、取得した要素の中にある文字を取得できます。

こうすれば 29日 30日 という形になります。

slackに通知をしよう

今回はwebhookで通知します。 slackのwebhook設定を行い、webhook用のURLを取得します。

Ruby側は slack-incoming-webhooks というgemがあるので、Gemfileに追記します。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "selenium-webdriver"
gem "slack-incoming-webhooks" ### [Add]

bundler経由でインストールします。

$ bundler install

Ruby側のスクリプトは下記のようにします。

#!/usr/bin/env ruby

require 'slack/incoming/webhooks'

def main
  slack = Slack::Incoming::Webhooks.new ENV['WEBHOOK_URL'], channel: ENV['SLACK_CHANNEL'], username: 'てすと'

  slack.link_names = 1
  slack.icon_emoji = ':cake:'

  slack.post "スクリプトから通知できとーと?できとーよ! @here"
end

main

環境変数でwebhookURLとチャンネル名を指定します。

実行してみます。

$ WEBHOOK_URL=webhookのURL SLACK_CHANNEL=#test_ch bundler exec ruby script2.rb

下記のようにSlackへ通知されます。

f:id:AdwaysEngineerBlog:20171201140620p:plain

ここまでできたらスクリプト同士を組み合わせていい感じにします。

完成。そして。

個人ミッションの進捗を確認するミーティングが昨日あったので、 チームのマネージャーのK先輩とT先輩に見せました。

好評だったのでさっそくチームのSlackチャンネルに導入することになりました。

下記は実際に本日実行してみた画像です。

f:id:AdwaysEngineerBlog:20171201141442p:plain

cronなどで定期的に回すようにすれば、自動化完了です!

まとめ

初めてSeleniumを触ってみたり、Rubyスクリプトを書いてみましたが、純粋に楽しかったです!

現在はe2eテストなどが盛り上がってきている感じもしているので、

この経験を活かして、既存システムのe2eテスト化などに切り込んでいけたらなと思います。

Google Analyticsからデータの取り込み

こんにちは、渡部です。

今回は、Google Analyticsからデータを取り出してみたいと思います。

Google Analytics

新卒の頃からGoogle Analyticsの存在は知っていましたが、どのように活用すべきか、いつも疑問に感じてました。(使うのならちゃんと使ってあげたいですよね!)

Google Analyticsのデータを活用するのは公式のレポート画面でも十分だとは思うのですが、自分が見知った環境にデータを取り込むのも一つの方法だと思います。

今回は、Pythonを使ってデータをGoogle Analyticsのデータを取り込みたいと思います。

完成版は、https://github.com/watanany/ga_testにあります。

Google Analyticsからデータの取り込み

APIのセットアップ

Google AnalyticsはAPIを公開しており、それを利用します。

はじめてのアナリティクス Reporting API v4: インストール済みアプリケーション向け Python クイックスタート | アナリティクス Reporting API v4 | Google Developers

を参考に、APIの有効化と認証情報のセットアップを行います。

セットアップが完了したら、client_secrets.jsonView IDが手に入ります。

完成版では、./config/client_secrets.jsonにファイルを置くようにしました。

View ID./lib/get_print.pyVIEW_IDにセットしました。

Google Analyticsのデータ

レポートデータの構造

Google Analyticsではディメンジョン(dimension)と指標(metrics explorer)という用語が出てきます。

ディメンションと指標 - アナリティクス ヘルプ

あまり見ない言葉なのですが、「ディメンジョン」が言葉で表せる属性(性別や地域など)、「指標」が数値で表せる属性(セッションの数、ページ遷移の数など)のようです。

セグメントについて - アナリティクス ヘルプ を見るとレポートデータの構造としては以下のようになっているのだと思われます。

ユーザー > セッション(30分以内のブラウジング) > ヒット(ページ内でのアクション)

(セグメントはレポートデータのフィルタ群らしい)

このレポートデータから、どんな情報を取り出すかを指定できるのがディメンジョンと指標です(間違えてたらすみません(^^;))

ディメンジョンと指標の組み合わせ

ディメンジョンと指標を組み合わせて、どのようにレポートデータを取り出すかを指定できますが、その中でも組み合わせられるものと組み合わせられないものがあるようです。

どのようなものを組み合わせられるかをチェックするのは面倒ですが、以下のURLで組み合わせの整合性をチェックできるようです。

Dimensions & Metrics Explorer | アナリティクス Reporting API v4 | Google Developers

今回の組み合わせ

./lib/get_print.pyでは「ユーザーのID(ga:userBucket)」と「ユーザーごとに訪れたページのタイトル(ga:pageTitle)」というディメンジョン、「ページの表示数(ga:pageViews)」という指標を使ってデータを取り出しています。

一ヶ月分のデータの取り込み

以下のコードで、データを取り込めるようにしました。

https://github.com/watanany/ga_test/blob/master/lib/get_print.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import csv
from io import StringIO
from functools import reduce
from operator import add
from httplib2 import Http
from toolz import compose
from oauth2client import client
from oauth2client import file
from oauth2client import tools
from apiclient.discovery import build

SCOPES = ['https://www.googleapis.com/auth/analytics.readonly']
DISCOVERY_URI = ('https://analyticsreporting.googleapis.com/$discovery/rest')
CLIENT_SECRETS_PATH = './config/client_secrets.json'

VIEW_ID = '<取得したView IDを入れる>'

DIMENSIONS = ['ga:userBucket', 'ga:pageTitle']
METRICS = ['ga:pageViews']
ORDER_BYS = ['ga:userBucket']


def initialize_analyticsreporting():
    """Initializes the analyticsreporting service object.
    Returns:
      analytics an authorized analyticsreporting service object.
    """
    # Parse command-line arguments.
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        parents=[tools.argparser])
    flags = parser.parse_args([])

    # Set up a Flow object to be used if we need to authenticate.
    flow = client.flow_from_clientsecrets(
        CLIENT_SECRETS_PATH, scope=SCOPES,
        message=tools.message_if_missing(CLIENT_SECRETS_PATH))

    # Prepare credentials, and authorize HTTP object with them.
    # If the credentials don't exist or are invalid run through the native client
    # flow. The Storage object will ensure that if successful the good
    # credentials will get written back to a file.
    storage = file.Storage('./config/analyticsreporting.dat')
    credentials = storage.get()
    if credentials is None or credentials.invalid:
        credentials = tools.run_flow(flow, storage, flags)
    http = credentials.authorize(http=Http())

    # Build the service object.
    analytics = build('analytics', 'v4', http=http, discoveryServiceUrl=DISCOVERY_URI)

    return analytics

def get_report(analytics, page_token=None):
    # Use the Analytics Service Object to query the Analytics Reporting API V4.
    body = {
        'reportRequests': [
            {
                'viewId': VIEW_ID,
                'dateRanges': [{'startDate': '31daysAgo', 'endDate': 'today'}],
                'dimensions': [{'name': d} for d in DIMENSIONS],
                'metrics': [{'expression': m} for m in METRICS],
                'orderBys': [{'fieldName': f} for f in ORDER_BYS],
                'pageToken': page_token
            }
        ]
    }

    return analytics.reports().batchGet(body=body).execute()

def get_reports(analytics):
    page_token = None
    while page_token != -1:
        response = get_report(analytics, page_token)
        report = response['reports'][0]
        page_token = report.get('nextPageToken', -1)
        yield report

def convert(report):
    """Convert Google Analytics Report to 2D Array
    Returns:
      M 2D Array
    """
    M = []
    for row in report['data']['rows']:
        dimensions = row['dimensions']
        metrics = row['metrics'][0]['values']
        M.append(dimensions + metrics)
    return M

def main():
    analytics = initialize_analyticsreporting()
    M = compose(list, reduce)(add, [convert(report) for report in get_reports(analytics)])

    csv_io = StringIO(newline='')
    w = csv.writer(csv_io, quoting=csv.QUOTE_ALL)
    w.writerow(DIMENSIONS + METRICS)
    for row in M:
        w.writerow(row)

    print(csv_io.getvalue())


if __name__ == '__main__':
  main()

中身を見ていただくとわかるのですが、一度のAPI呼び出しでは全てのデータを取り出すことはできません。

APIのレスポンスには、nextPageTokenという値が入っていて、それを使っていくことによってデータを一ヶ月分取り出しています。

感想

Google Analyticsはデータの計測ツールとして頻繁に使われるツールだと思います。

Webページにタグを埋め込むだけで使えるという簡単さも魅力的ですが、使いこなすにはまだまだ覚えることが多そうでした。 というか用語が難しい

このツールを使うことによってデータを有用に活用でき、このツールを知ることによってどのようにデータを整理すべきかを学べる素晴らしいツールですね。

本当はGoogle Analyticsのデータを使って勉強会で行った「KMeans + 遺伝的アルゴリズム」で、本ブログのユーザーをクラスタリングしてみたかったのですが、取り込む手順だけで長くなってしまったので省略します。

ソースコードは、https://github.com/watanany/ga_test(./lib/calc.py)に置いてあるので、興味がある方はぜひ見てみてください。

それでは〜。

シェルの結果をJSONで出力 - awkを使いこなしたい -

こんにちは!インフラの奥村です!

インフラエンジニア歴1年半になりました。
日々の業務にも慣れだし、「まぁまぁこなせてるんじゃないか?」と思ってしまうまでになりました。

とは思いつつも
いざ、障害、トラブルなどが発生したときに迅速に対処できるかと言われるとまだまだです。
入社当初から先輩社員に言われている

「まずログを見よう」

「まずログを見る」という行為 はしているものの、必要なログを抜き出す能力が明らかに足りていません。
ワンライナーのシェルコマンドを実行し、必要な情報を抜き出しているエンジニア見るたびに「すげぇ」としか思えていません。

そこで!そういった能力を身につけるために「awk」の勉強をはじめました!

今回は勉強の一環として「awk」を使ったシェルスクリプトを作成しました。

スクリプト

標準入力から値を受け取りそれをJSON形式で出力するスクリプトです。

  • loj.sh
#!/bin/sh
usage() {
  echo 'outjson.sh [-k] key colum number [-c] "1,colum name 2,columm name" [-e] Except from top 0..*  [-s] <,> <:> default Value is space'
}
EXCEPT=1
while getopts k:c:e:s: OPT
do
  case $OPT in
    k ) KEY=$OPTARG ;;
    c ) COLUMM_TEXT=$OPTARG ;;
    e ) EXCEPT=$OPTARG ;;
    s ) SEP="-F ${OPTARG}" ;;
    * ) usage
          exit 1 ;;
  esac
done

if [ -p /dev/stdin ] ; then
    a=$(cat -)
    echo "$a" |
    tail -n +$EXCEPT |
    awk $SEP -v key=${KEY} -v colum_text="${COLUMM_TEXT}" ' \
    BEGIN {
      split(colum_text, colum, " ")
      for (i in colum ) {
         split(colum[i], sp_colum, ",")
         colums_hash[sp_colum[1]]=sp_colum[2]
      }
      print "{"
    }
    {
       printf("\"%s\": {",$key)
       for(i in colums_hash ) {
         printf("\"%s\": \"%s\",",colums_hash[i], $i);
       }
       printf( "},")
    }
    END{
       print "}"
    }' |
    sed -e 's/,}/}/g'

else
    echo "need STDIN"
    usage
fi

説明

全プロセスからsystemdのプロセスを探す例で説明します。

パイプで値を渡し、必要な引数を与えます

ps aux | grep systemd | ./loj.sh -k 2 -c "2,PID 11,CMD" -e 2
  • -k
    • JSONのキーにしたいもの。標準出力されたものの列番号を入れる(数字)-k 2
  • -c
    • JSONの値として入れたいもの。標準出力されたものの列番号とキー名をダブルクウォートで囲って入れる。 -c "2,PID 11,CMD"
  • -e
    • 一番最初の行から無視する行数を入れる。 -e 2

として出力された結果がこちらです。

{
"319": {"CMD": "/lib/systemd/systemd-udevd","PID": "319"},"807": {"CMD": "/usr/bin/dbus-daemon","PID": "807"},"853": {"CMD": "/lib/systemd/systemd-logind","PID": "853"},"13703": {"CMD": "/lib/systemd/systemd","PID": "13703"},"22499": {"CMD": "grep","PID": "22499"}}

これをjqに渡してるみると

{
  "319": {
    "CMD": "/lib/systemd/systemd-udevd",
    "PID": "319"
  },
  "807": {
    "CMD": "/usr/bin/dbus-daemon",
    "PID": "807"
  },
  "853": {
    "CMD": "/lib/systemd/systemd-logind",
    "PID": "853"
  },
  "13703": {
    "CMD": "/lib/systemd/systemd",
    "PID": "13703"
  },
  "22489": {
    "CMD": "grep",
    "PID": "22489"
  }
}

こんな感じで出力されます。

lsのパターン

  • 通常の出力
$ ls -la
合計 8
drwxrwxr-x 2 vagrant vagrant 4096 1116 17:35 2017 .
drwxrwxr-x 3 vagrant vagrant 4096 1116 17:35 2017 ..
-rw-rw-r-- 1 vagrant vagrant    0 1116 17:35 2017 file1
-rw-rw-r-- 1 okumura okumura    0 1116 17:35 2017 file2
-rwx------ 1 vagrant vagrant    0 1116 17:35 2017 file3
ls -la | loj -k 10 -c "1,PERMISSION 3,USER 4,GROUP 5,SIZE 6,MONTH 7,DAY 8,TIME, 9,YEAR" -e 4 | jq

出力結果

{
  "file1": {
    "GROUP": "vagrant",
    "SIZE": "63",
    "MONTH": "11月",
    "DAY": "16",
    "TIME": "17:41",
    "YEAR": "2017",
    "PERMISSION": "-rw-rw-r--",
    "USER": "vagrant"
  },
  "file2": {
    "GROUP": "okumura",
    "SIZE": "0",
    "MONTH": "11月",
    "DAY": "16",
    "TIME": "17:35",
    "YEAR": "2017",
    "PERMISSION": "-rw-rw-r--",
    "USER": "okumura"
  },
  "file3": {
    "GROUP": "vagrant",
    "SIZE": "223",
    "MONTH": "11月",
    "DAY": "16",
    "TIME": "17:42",
    "YEAR": "2017",
    "PERMISSION": "-rwx------",
    "USER": "vagrant"
  }
}

行と列がある出力ならJSON形式で出力でます!

まとめ

このスクリプト作成を通して「awk」、「シェル」の奥深さを知りました。

加えて、自分が入り口にすら立っていなかったことを思い知らされました。

今後も日々鍛錬を重ね、自分の頭とシェルが繋がっているかのようなスピードでワンライナーを生み出す

一流のシェル芸人になりたいです。

ScalaのMapを使って2つのサマリーテーブルの結果を結合する

こんにちは。エンジニアのまっちゃんです!

現在は広告サービスのレポート機能に携わっています。

その中で2つのサマリーテーブルから集計したい場面が出てきたのですが、

自分だけの力では解決できず、チーフとペアプロを行って解決できたのでそれについて書いていきます。

想定の仕様

※ 仕様については置き換えて書かせていただきます。

仕様としては下記のようなテーブルがあるとします。 f:id:AdwaysEngineerBlog:20171109154949p:plain

summary_guestテーブルでは、イベント、チケットごとのゲスト人数を集計してます。 summary_action_guestテーブルでは、イベント、チケット、アクションごとのアクションを起こしてくれたゲスト人数を集計してます。

  • イベントは具体的なイベント名
  • チケットはチケット区別(前売り券、当日券など)
  • アクションは行動(アンケート記入、本購入など)

を想定してます。

この2つのテーブル結果をScalaのMap経由で1つのシーケンスへと結合します。

※ このコードはIntelliJのScala WorkSheetで実行確認ができます。

前準備1: Entity を作成

テーブルと結果を表すEntityをcase classで作成します。

// summary_guest
case class SummaryGuestEntity(
  eventId: Option[Int]    = None,
  ticketId: Option[Int]   = None,
  guestCount: Option[Int] = None
)

// summary_action_guest
case class SummaryActionGuestEntity(
  eventId: Option[Int]          = None,
  ticketId: Option[Int]         = None,
  actionId: Option[Int]         = None,
  actionGuestCount: Option[Int] = None
)

// 結果 Entity
case class SummaryEntity(
  eventId: Option[Int]          = None,
  ticketId: Option[Int]         = None,
  actionId: Option[Int]         = None,
  guestCount: Option[Int]       = None,
  actionGuestCount: Option[Int] = None
)

前準備2: 共通Key を設定

case class SummaryKey(
  eventId:  Option[Int] = Some(0),
  ticketId: Option[Int] = Some(0)
)

前準備3: 取得結果イメージを作成

本来はDBの結果をシーケンスで取得しますが、 今回は仮データで作成します

// summary_guest からの取得結果イメージ
val guestResults = Seq(
  SummaryGuestEntity(
    eventId    = Some(1),
    ticketId   = Some(1),
    guestCount = Some(10)
  ),
  SummaryGuestEntity(
    eventId    = Some(1),
    ticketId   = Some(2),
    guestCount = Some(20)
  ),
  SummaryGuestEntity(
    eventId    = Some(1),
    ticketId   = Some(3),
    guestCount = Some(30)
  ),
  SummaryGuestEntity(
    eventId    = Some(2),
    ticketId   = Some(1),
    guestCount = Some(5)
  )
)

// summary_action_guest の取得結果イメージ
val actionGuestResults = Seq(
  SummaryActionGuestEntity(
    eventId          = Some(1),
    ticketId         = Some(1),
    actionId         = Some(1),
    actionGuestCount = Some(2)
  ),
  SummaryActionGuestEntity(
    eventId          = Some(1),
    ticketId         = Some(2),
    actionId         = Some(1),
    actionGuestCount = Some(4)
  ),
  SummaryActionGuestEntity(
    eventId          = Some(2),
    ticketId         = Some(1),
    actionId         = Some(1),
    actionGuestCount = Some(2)
  ),
  SummaryActionGuestEntity(
    eventId          = Some(2),
    ticketId         = Some(2),
    actionId         = Some(2),
    actionGuestCount = Some(1)
  )
)

処理1: 取得結果を Map にする

DBから取得した結果をMapにします。

// guest の結果を Map にする
val guestMap = guestResults.map { guest =>
  (SummaryKey(
    eventId  = guest.eventId,
    ticketId = guest.ticketId
  ), guest)
}.toMap

// actionGuest の結果を Map にする
val actionGuestMap = actionGuestResults.map { actionGuest =>
  (SummaryKey(
    eventId  = actionGuest.eventId,
    ticketId = actionGuest.ticketId
  ), actionGuest)
}.toMap

処理2: Map の結合処理メソッドを作成

SummaryKeyを見て、データを結合します。

def combine(guestMap: Map[SummaryKey, SummaryGuestEntity], actionGuestMap: Map[SummaryKey, SummaryActionGuestEntity]): Map[SummaryKey, (Option[SummaryGuestEntity], Option[SummaryActionGuestEntity])] = {
  // 重複排除した guest の key
  val guestKey = Set(guestMap.keysIterator.toList: _*)
  // 重複排除した actionGuest の key
  val actionGuestKey = Set(actionGuestMap.keysIterator.toList: _*)
  // guest と actionGuest に存在している key
  val intersection = guestKey & actionGuestKey

  // guest actionGuest 両方の key に存在するデータ
  val bothData =
    intersection.map { keyName =>
      (keyName, (Some(guestMap(keyName)), Some(actionGuestMap(keyName))))
    }.toMap
  // guest の key にしか存在しないデータ
  val guestData = guestMap.filterKeys(!intersection.contains(_)).map{ case (key, guestEntity) =>
    (key, (Some(guestEntity), None))
  }
  // actionGuest の key にしか存在しないデータ
  val actionGuestData = actionGuestMap.filterKeys(!intersection.contains(_)).map{ case (key, actionGuestEntity) =>
    (key, (None, Some(actionGuestEntity)))
  }
  
  bothData ++ guestData ++ actionGuestData
}

処理3: 結果Entityを生成するメソッドを作成

guest、actionGuest 両方のデータがあれば結合した結果を返します。 片方しかない場合は片方のデータのみを返します。

def makeSummaryEntity(maybeGuestEntity: Option[SummaryGuestEntity], maybeActionGuestEntity: Option[SummaryActionGuestEntity]): Option[SummaryEntity] = {
  // guest actionGuest ともに結果がない場合は None を返す
  if (maybeGuestEntity.isEmpty && maybeActionGuestEntity.isEmpty) return None

  val guestSummaryEntity = maybeGuestEntity.flatMap{ guest =>
    Some(
      SummaryEntity(
        // guest actionGuest で共通な要素
        eventId    = guest.eventId,
        ticketId   = guest.ticketId,
        // guest のみに存在している要素
        guestCount = guest.guestCount
      )
    )
  }.getOrElse(SummaryEntity())

  Some(
    maybeActionGuestEntity.flatMap { actionGuest =>
      Some(
        guestSummaryEntity.copy(
          // guest actionGuest で共通な要素
          eventId = actionGuest.eventId,
          ticketId = actionGuest.ticketId,
          // actionGuest のみに存在している要素
          actionId = actionGuest.actionId,
          actionGuestCount = actionGuest.actionGuestCount
        )
      )
    }.getOrElse(guestSummaryEntity)
  )
}

処理4: 作成したメソッドを実行

Mapにした取得結果と作成したメソッドを使います。

combine(guestMap, actionGuestMap).flatMap { case (key, data) =>
  makeSummaryEntity(data._1, data._2)
}.toSeq

これで期待通り、1つのシーケンスで返ってきます。

以下が出力結果です。

List(
  SummaryEntity(
    Some(1),  // eventId
    Some(1),  // ticketId
    Some(1),  // actionId
    Some(10), // guestCount
    Some(2)   // actionGuestCount
  ),
  SummaryEntity(
    Some(2),  // eventId
    Some(2),  // ticketId
    Some(2),  // actionId
    None,     // guestCount
    Some(1)   // actionGuestCount
  ),
  SummaryEntity(
    Some(2),  // eventId
    Some(1),  // ticketId
    Some(2),  // actionId
    Some(5),  // guestCount
    Some(1)   // actionGuestCount
  ),
  SummaryEntity(
    Some(1),  // eventId
    Some(3),  // ticketId
    None,     // actionId
    Some(30), // guestCount
    None      // actionGuestCount
  ),
  SummaryEntity(
    Some(1),  // eventId
    Some(2),  // ticketId
    Some(1),  // actionId
    Some(20), // guestCount
    Some(4)   // actionGuestCount
  )
)

まとめ

自分一人では行き詰まり期待通りの実装ができませんでしたが、

チーフとのペアプロを行う事により、仕様通りの動きを書くことができました。

またペアプロでは新たな気付きもあったので、タイミングがあえばチーム内でペアプロをして行きたいです。