こんにちは。エンジニアのまっちゃんです!
現在は広告サービスのレポート機能に携わっています。
その中で2つのサマリーテーブルから集計したい場面が出てきたのですが、
自分だけの力では解決できず、チーフとペアプロを行って解決できたのでそれについて書いていきます。
想定の仕様
※ 仕様については置き換えて書かせていただきます。
仕様としては下記のようなテーブルがあるとします。
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 ) )
まとめ
自分一人では行き詰まり期待通りの実装ができませんでしたが、
チーフとのペアプロを行う事により、仕様通りの動きを書くことができました。
またペアプロでは新たな気付きもあったので、タイミングがあえばチーム内でペアプロをして行きたいです。