[Scala] play架構使用tree結構json

今天想用樹狀結構去顯示Presto裡面的table source

隨手找了一個github project來研究
https://github.com/jonmiles/bootstrap-treeview

使用的API是吃json的格式

var tree = [
  {
    text: "Parent 1",
    nodes: [
      {
        text: "Child 1",
        nodes: [
          {
            text: "Grandchild 1"
          },
          {
            text: "Grandchild 2"
          }
        ]
      },
      {
        text: "Child 2"
      }
    ]
  },
  {
    text: "Parent 2"
  },
  {
    text: "Parent 3"
  },
  {
    text: "Parent 4"
  },
  {
    text: "Parent 5"
  }
];

對應到目前開發中的scala play project,就面臨到了如何做格式轉換的問題

目前是將資料庫裡面catalog, schema, table等資訊用TableData這個物件封裝起來

case class TableData(catalog:String, schema:String, table: String)
def listTables: List[TableData] = {
...

//result example
(mysql, database1, table1)
(mysql, database1, table2)
(hive, schema1, table3)

ㄧ開始想的很簡單,轉成list/map的組合用內建的toJson函式去解就好了

val tablesTree = listTables.groupBy(_.catalog).toList.collect{
      case (c, table) => Map("text" -> c, "nodes" -> table.groupBy(_.schema).toList.collect{
        case (s, table) => Map("text" -> s, "nodes" -> table.map(x => Map("text" -> x.table)))
      })
    }
Ok(toJson(tablesTree))

但是馬上就出了問題

No Json serializer found for type List[scala.collection.immutable.Map[String,java.io.Serializable]]. Try to implement an implicit Writes or Format for this type.

當單純使用List/Map的時候其實沒問題,但這邊問題出在Map使用了不同的data type當value

Map(text -> String, nodes -> List)

不同的data type當value時, scala直接把它統一看成java.io.Serializable

這樣怎麼辦呢?只好自己製作json writer

參考:https://www.playframework.com/documentation/2.1.1/ScalaJsonCombinators

先自制一個TreeNode資料結構, 然後將原先List/Map資料轉換改成TreeNode的格式

case class TreeNode(text:String, nodes:List[TreeNode])

val tablesTree = tables.groupBy(_.catalog).toList.collect{
   case (c, table) => new TreeNode(c, table.groupBy(_.schema).toList.collect{
     case (s, table) => new TreeNode(s, table.map(x => new TreeNode(x.table, List())))
     })
}

之後補上一段轉換json的code, 使用lazyWrite做recursive的tree結構處理

import play.api.libs.functional.syntax._
import play.api.libs.json._
case class TreeNode(text:String, nodes:List[TreeNode])
implicit val TreeNodeWrite:Writes[TreeNode] = (
  (__ \ "text").write[String] ~
    (__ \ "nodes").lazyWrite(Writes.traversableWrites[TreeNode](TreeNodeWrite))
  )(unlift(TreeNode.unapply))


val tablesTree = tables.groupBy(_.catalog).toList.collect{
  case (c, table) => new TreeNode(c, table.groupBy(_.schema).toList.collect{
    case (s, table) => new TreeNode(s, table.map(x => new TreeNode(x.table, List())))
  })
}

但是這時候跑出了一段錯誤訊息

forward reference extends over definition of value

原來問題出在TreeNodeWrite在宣告的同時就讓它被lazyWrite使用出的錯誤

後來找到的解法是可用 lazy 去關鍵字去解決這問題

import play.api.libs.functional.syntax._
import play.api.libs.json._
case class TreeNode(text:String, nodes:List[TreeNode])
implicit lazy val TreeNodeWrite:Writes[TreeNode] = (
  (__ \ "text").write[String] ~
    (__ \ "nodes").lazyWrite(Writes.traversableWrites[TreeNode](TreeNodeWrite))
  )(unlift(TreeNode.unapply))


val tablesTree = tables.groupBy(_.catalog).toList.collect{
  case (c, table) => new TreeNode(c, table.groupBy(_.schema).toList.collect{
    case (s, table) => new TreeNode(s, table.map(x => new TreeNode(x.table, List())))
  })
}

Ok(toJson(tablesTree))

加上延遲處理的判斷之後就可以順利執行了

*補充

這段code還有個小問題,就是最後一定會把nodes屬性傳給前端

前端需要一個處理把空的nodes給移除掉

$.getJSON("presto/listTables", function(json){

(function filter(obj) {
    $.each(obj, function(key, value){
        //移除空值

        if (value === "" || value === null || value.length == 0){
            delete obj[key];
        } else if (Object.prototype.toString.call(value) === '[object Object]') {
            filter(value);
        } else if (Array.isArray(value)) {
            value.forEach(function (el) { filter(el); });
        }
    });
})(json);
  $('#tree').treeview({data: json});
});
comments powered by Disqus