如何将派生变量添加到 ResultSet

How to add derived variables to a ResultSet

我见过的几乎所有 guides/tutorials 都只展示了如何从数据库中直接可用的列中解析值。例如,以下是一个非常常见的模式,我了解它在很多方面的用处:

case class Campaign(id: Int, campaign_mode_id: Int, name: String)

class Application @Inject()(db: Database) extends Controller {

  val campaign = {
    get[Int]("campaign.id") ~
    get[Int]("campaign.campaign_mode_id") ~
    get[String]("campaign.name") map {
      case id ~ campaign_mode_id ~ name  => Campaign(id, campaign_mode_id, name)
    }
  }

  def index = Action {
    val data : List[Campaign] = db.withConnection { implicit connection =>
      SQL("SELECT id, campaign_mode_id, name FROM campaign").as(campaign.*)
    }

    Ok(Json.toJson(data))
  }
} 

它会产生如下所示的结果:

[
    {
        id: 2324,
        campaign_mode_id: 13,
        name: "ABC"
    },
    {
        id: 1324,
        campaign_mode_id: 23,
        name: "ABCD"
    }   
]

现在,如果活动 table 中有一个额外的日期字段(如 started_on 表示活动开始时间怎么办?或者另一个名为 num_followers 的字段是指关注者数量的整数?

假设我想在 运行 数据库查询之后和返回我的 JSON 之前进行一些计算。例如,我想包含一个 latest_compaign_date 引用最新活动的 started_on 日期。或者说我想包括一个 average_num_followers,它指的是所有活动的平均关注者数量。

我的最终结果如下:

{
    latest_compaign_date: 12 Dec 2018,
    average_num_followers: 123,
    campaigns: [
        {
            id: 2324,
            campaign_mode_id: 13,
            name: "ABC"
        },
        {
            id: 1324,
            campaign_mode_id: 23,
            name: "ABCD"
        }   
    ]
}

我知道对于我给出的示例,最好在我的数据库查询中而不是在我的应用程序代码中进行这些计算。但是如果我不得不做一些非常复杂的事情并且出于某种原因想在我的应用程序代码中做这件事怎么办?我应该如何修改我的 ResutSetParser 以促进此操作?

以下是我尝试过的几种方法:

不要使用 ResultSetParser 而是手动执行所有操作

  case class CampaignData(newestCampaignDate: Long, newestCampaignId: Long, averageNumFollowers: Float, campaigns: Seq[Campaign])

  def aggregater(rows: Seq[Row]): CampaignData = {
    val newestCampaignDate: Long = getNewestDate(rows)
    val newestCampaignId: Long = getNewestCampaign(rows)
    val averageNumFollowers: Float = getAverageNumFollowers(rows)

    val campaigns: Seq[Campaign] = rows.map(row => {
      val rowMap: Map[String, Any] = row.asMap

      Campaign(
        rowMap("campaign.id").asInstanceOf[Int],
        rowMap("campaign.campaign_mode_id") match { case None => 0 case Some(value) => value.asInstanceOf[Int]},
        rowMap("campaign.name") match { case None => "" case Some(value) => value.asInstanceOf[String]}
      )
    })

    CampaignData(newestCampaignDate, newestCampaignId, averageNumFollowers, campaigns)
  }

  def index = Action {
    val data : Seq[Row] = db.withConnection { implicit connection =>
      SQL("SELECT id, campaign_mode_id, name, started_on, num_followers FROM campaign")
    }

    Ok(Json.toJson(aggregater(data)))
  }

这种方法很难闻,因为必须使用 asInstanceOfmatch 处理每个字段非常乏味,老实说感觉不安全。而且凭直觉,我觉得 Anorm 应该对此有更好的东西,因为我可能不是第一个 运行 遇到这个问题的人。

结合其他函数使用 ResultSetParser

case class Campaign(id: Int, campaign_mode_id: Int, name: String)
case class CampaignData(newestCampaignDate: Long, newestCampaignId: Long, averageNumFollowers: Float, campaigns: Seq[Campaign])

val campaign = {
  get[Int]("campaign.id") ~
  get[Int]("campaign.campaign_mode_id") ~
  get[Int]("campaign.num_followers") ~
  get[Long]("campaign.started_on") ~
  get[String]("campaign.name") map {
    case id ~ campaign_mode_id ~ num_followers ~ started_on ~ name  => Map(
      "id" -> id, 
      "campaign_mode_id" -> campaign_mode_id, 
      "num_followers" -> num_followers,
      "started_on" -> started_on,
      "name" -> name
    )
  }
}

def index = Action {
  val data : Map[String, Any] = db.withConnection { implicit connection =>
    SQL("SELECT id, campaign_mode_id, name, started_on, num_followers FROM campaign").as(campaign.*)
  }

  Ok(Json.toJson(aggregator(data)))
}

def aggregator(data: Map[String, Any]): CampaignData = {
  val newestCampaignDate: Long = getNewestDate(data)
  val newestCampaignId: Long = getNewestCampaign(data)
  val averageNumFollowers: Float = getAverageNumFollowers(data)
  val campaigns: Seq[Campaign] = getCampaigns(data)

  CampaignData(newestCampaignDate, newestCampaignId, averageNumFollowers, campaigns)
}

从我不必处理 isInstanceOf 的意义上说,这种方法更好,但是还有一个更大的问题,那就是必须处理中间 Map。它使所有 getter 函数(例如 getCampaigns)变得更加复杂。我觉得 Anorm 必须提供一些我不知道的开箱即用的东西。

正如您在第一个代码片段中发布的那样,以下代码

def index = Action {
    val data : List[Campaign] = db.withConnection { implicit connection =>
      SQL("SELECT id, campaign_mode_id, name FROM campaign").as(campaign.*)
    }

    Ok(Json.toJson(data))
  }

returns 一个类型安全的活动列表,感谢 Anorm 提取器。

通常,您将 pre-process 使用像这样的类型安全函数得到结果

case class CampaignAggregateData(campaigns:List[Campaign], averageNumFollowers:Int, newestCampaignId:Option[Long])

def aggregate(f:List[Campaign]):CampaignAggregatedData

def index = Action {
    val manyCampaign : List[Campaign] = db.withConnection { implicit connection =>
      SQL("SELECT id, campaign_mode_id, name FROM campaign").as(campaign.*)
    }

    val aggregatedData:CampaignAggregateData = aggregate(manyCampaign)

    Ok(Json.toJson(data))
  }

在需要数据库引擎执行聚合的情况下,您通常会在单个操作中包含多个 db.withConnection 语句