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
  )
)

まとめ

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

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

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