精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

一次設(shè)計演進(jìn)之旅

開發(fā) 開發(fā)工具
今天我給大家講一講關(guān)于設(shè)計演講的過程。

一、需求背景

[[182446]]

我們需要實(shí)現(xiàn)對存儲在HDFS中的Parquet文件執(zhí)行數(shù)據(jù)查詢,并通過REST API暴露給前端以供調(diào)用。由于查詢的結(jié)果可能數(shù)量較大,要求API接口能夠提供分頁查詢。在第一階段,需要支持的報表有5張,需要查詢的數(shù)據(jù)表與字段存在一定差異,查詢條件也有一定差異。

每個報表的查詢都牽涉到多張表的Join。每張表都被創(chuàng)建為數(shù)據(jù)集,對應(yīng)為一個Parquet文件。Parquet文件夾名就是數(shù)據(jù)集名,名稱是系統(tǒng)自動生成的,所以我們需要建立業(yè)務(wù)數(shù)據(jù)表名、Join別名以及自動生成的數(shù)據(jù)集名的映射關(guān)系。數(shù)據(jù)集對應(yīng)的各個字段信息都存儲在Field元數(shù)據(jù)表中,其中我們需要的三個主要屬性為:

  • CodeName:創(chuàng)建數(shù)據(jù)集時,由系統(tǒng)自動生成
  • FieldName:為客戶數(shù)據(jù)源對應(yīng)數(shù)據(jù)表的字段名
  • DisplayName:為報表顯示的列名

說明:為了便于理解,我將要實(shí)現(xiàn)的五個報表分別按照序號命名。

二、解決方案

1. 前置條件

本需求是圍繞著我們已有的BI產(chǎn)品做定制開發(fā)。現(xiàn)有產(chǎn)品已經(jīng)提供了如下功能:

  • 通過Spark SQL讀取指定Parquet文件,但不支持同時讀取多個Parquet文件,并對獲得的DataFrame進(jìn)行Join
  • 獲取存儲在MySQL中的DataSet與Field元數(shù)據(jù)信息
  • 基于AKKA Actor的異步查詢

2. 項目目標(biāo)

交付日期非常緊急,尤其需要盡快提供最緊急的第一張報表:定期賬戶掛失后辦理支取。后續(xù)的報表也需要盡快交付,同時也應(yīng)盡可能考慮到代碼的重用,因為報表查詢業(yè)務(wù)的相似度較高。

3. 整體方案

基于各個報表的具體需求,解析并生成查詢Parquet(事實(shí)上是讀取多個)的Spark SQL語句。生成的SQL語句會交給Actor,并由Actor請求Spark的SQLContext執(zhí)行SQL語句,獲得DataFrame。利用take()結(jié)合zipWithIndex實(shí)現(xiàn)對DataFrame的分頁,轉(zhuǎn)換為前端需要的數(shù)據(jù)。

根據(jù)目前對報表的分析,生成的SQL語句包含join、where與order by子句。報表需要查詢的數(shù)據(jù)表是在系統(tǒng)中硬編碼的,然后通過數(shù)據(jù)表名到DataSet中查詢元數(shù)據(jù)信息,獲得真實(shí)的由系統(tǒng)生成的數(shù)據(jù)集名。查詢的字段名同樣通過硬編碼方式,并根據(jù)對應(yīng)數(shù)據(jù)集的ID與字段名獲得Field的元數(shù)據(jù)信息。

三、設(shè)計演進(jìn)

1. 引入模板方法模式

考慮到SQL語句具有一定的通用性(如select的字段、表名與join表名、on關(guān)鍵字、where條件、排序等),差異在于不同報表需要的表名、字段以及查詢條件。通過共性與可變性分析,我把相同的實(shí)現(xiàn)邏輯放在一個模板方法中,而將差異的內(nèi)容(也即各個報表特定的部分)交給子類去實(shí)現(xiàn)。這是一個典型的模板方法模式:

  1. trait ReportTypeParser extends DataSetFetcher with ParcConfiguration { 
  2.   def sqlFor(criteria: Option[List[Condition]]): String 
  3.   def criteriaFields: Array[Field] 
  4.  
  5.   private[parc] def predefinedTables: List[TableName] 
  6.   private[parc] def predefinedFields: List[TableField] 
  7.  
  8.   def generateHeaders: Array[Field] = { 
  9.     predefinedFields.map(tf => tf.fieldName.field(tf.table.originalName)).toArray 
  10.   }} 
  11.  
  12. class FirstReportTypeParser extends ReportTypeParser { 
  13.   override def sqlFor(criteria: Option[List[Condition]]): String = { 
  14.     s"""       
  15.        select ${generateSelectFields}       
  16.        from ${AccountDetailTable} a       
  17.        left join ${AccountDebtDetailTable} b       
  18.        left join ${AoucherJournalTable} c       
  19.        on a.${AccountDetailTableSchema.Account.toString.codeName(AccountDetailTable)} = b.${AccountDebtDetailTableSchema.Account.toString.codeName(AccountDebtDetailTable)}       
  20.        and a.${AccountDetailTableSchema.CustomerNo.toString.codeName(AccountDetailTable)} = c.${AoucherJournalTableSchema.CustomerNo.toString.codeName(AoucherJournalTable)}       
  21.        where ${generateWhereClause}$       
  22.        ${generateOrderBy}     
  23.     """ 
  24.   } 
  25.  
  26.   override private[parc] def predefinedTables: List[TableName] = ... 
  27.   override private[parc] def predefinedFields: List[TableField] = ... 
  28.  
  29.   private[parc] def generateSelectFields: String = { 
  30.     if (predefinedFields.isEmpty) "*" else predefinedFields.map(field => field.fullName).mkString(",") 
  31.   } 
  32.  
  33.   private[parc] def generateWhereCluase(conditionsOpt: Option[List[Condition]]): String = { 
  34.     def evaluate(condition: Condition): String = { 
  35.       val aliasName = aliasNameFor(condition.originalTableName) 
  36.  
  37.       val codeName = fetchField(condition.fieldId) 
  38.         .map(_.codeName) 
  39.         .getOrElse(throw ResourceNotExistException(s"can't find the field with id ${condition.fieldId}")) 
  40.  
  41.       val values = condition.operator.toLowerCase() match { 
  42.         case "between" => { 
  43.           require(condition.values.size == 2, "the values of condition don't match between operator") 
  44.           s"BETWEEN ${condition.values.head} AND ${condition.values.tail.head}" 
  45.         } 
  46.         case _ => throw BadRequestException(s"can't support operator ${condition.operator}") 
  47.       } 
  48.  
  49.       s"${aliasName}.${codeName} ${values}" 
  50.     } 
  51.  
  52.     conditionsOpt match { 
  53.       case Some(conditions) if !conditions.isEmpty => s"where  ${conditions.map(c => evaluate(c)).mkString(" and ")}" 
  54.       case _ => "" 
  55.     } 
  56.   }} 

在ReportTypeParser中,我實(shí)現(xiàn)了部分可以重用的邏輯,例如generateHeaders()等方法。但是,還有部分實(shí)現(xiàn)邏輯放在了具體的實(shí)現(xiàn)類FirtReportTypeParser中,例如最主要的sqlFor方法,以及該方法調(diào)用的諸多方法,如generateSelectFields、generateWhereCluase等。

在這其中,TableName提供了表名與數(shù)據(jù)集名、別名之間的映射關(guān)系,而TableField則提供了TableName與Field之間的映射關(guān)系:

  1. case class TableName(originalName: String,  
  2.                      metaName: String,  
  3.                      aliasName: String,  
  4.                      generatedName: String = ""
  5.  
  6. case class TableField(table: TableName,  
  7.                       fieldName: String,  
  8.                       orderType: Option[OrderType] = None) 

仔細(xì)觀察sqlFor方法的實(shí)現(xiàn),發(fā)現(xiàn)生成select的字段、生成Join的部分以及生成條件子句、排序子句都是有規(guī)律可循的。這個過程是在我不斷重構(gòu)的過程中慢慢浮現(xiàn)出來的。我不斷找到了這些相似的方法,例如generateSelectFields、generateWhereClause這些方法。它們之間的差異只在于一些與具體報表有關(guān)的元數(shù)據(jù)上,例如表名、字段名、字段名與表名的映射、表名與別名的映射。

我首先通過pull member up重構(gòu),將這兩個方法提升到ReportTypeParser中:

  1. trait ReportTypeParser extends ... { 
  2.   private[parc] def generateSelectFields: String = ... 
  3.   private[parc] def generateWhereCluase(conditionsOpt: Option[List[Condition]]): String 

此外,還包括我尋找到共同規(guī)律的join部分:

  1. trait ReportTypeParser extends ... { 
  2.   private[parc] def generateJoinKeys: String = { 
  3.     def joinKey(tableField: TableField): String = 
  4.       s"${aliasNameFor(tableField.tableName)}.${tableField.fieldName.codeName(mapping.tableName)}" 
  5.  
  6.     predefinedJoinKeys.map{ 
  7.       case (leftTable, rightTable) => s"${joinKey(leftTable)} = ${joinKey(rightTable)}" 
  8.     }.mkString(" and ") 
  9.   }} 

現(xiàn)在sqlFor()方法就變成一個所有報表都通用的方法了,因此我也將它提升到ReportTypeParser中。

2. 元數(shù)據(jù)概念的浮現(xiàn)

我在最初定義諸如predefinedTables與predefinedFields等方法時,還沒有清晰地認(rèn)識到所謂元數(shù)據(jù)(Metadata)的概念,然而這一系列重構(gòu)后,我發(fā)現(xiàn)定義在FirstReportParser子類中的方法,其核心職責(zé)就是提供SQL解析所需要的元數(shù)據(jù)內(nèi)容:

  1. class FirstReportTypeParser extends ReportTypeParser { 
  2.   private[parc] def predefinedJoinKeys: List[(TableField, TableField)] = ... 
  3.   override private[parc] def predefinedAliasNames: Map[TableName, AliasName] = ... 
  4.   override private[parc] def predefinedCriteriaFields: List[TableField] = ... 
  5.   override private[parc] def predefinedOrderByFields: List[TableField] = ... 
  6.   override private[parc] def predefinedTables: List[TableName] = ... 
  7.   override private[parc] def predefinedFields: List[TableFieldMapping] = ... 

3. 以委派取代繼承

元數(shù)據(jù)的概念給了我啟發(fā)。針對報表的SQL語句解析,邏輯是完全相同的,不同之處僅在于解析的元數(shù)據(jù)而已。這就浮現(xiàn)出兩個不同的職責(zé):

  • 提供元數(shù)據(jù)
  • 元數(shù)據(jù)解析

在變化方向上,引起這兩個職責(zé)發(fā)生變化的原因是完全不同的。不同的報表需要提供的元數(shù)據(jù)是不同的,而對于元數(shù)據(jù)的解析,則取決于Spark SQL的訪問方式(在后面我們會看到這種變化)。根據(jù)單一職責(zé)原則,我們需要將這兩個具有不同變化方向的職責(zé)分離,因此它們之間正確的依賴關(guān)系不應(yīng)該是繼承,而應(yīng)該是委派。

我首先引入了ReportMetadata,并將原來的FirstReportTypeParser更名為FirstReportMetadata,在實(shí)現(xiàn)了ReportMetadata的同時,對相關(guān)元數(shù)據(jù)的方法進(jìn)行了重命名:

  1. trait ReportMetadata extends ParcConfiguration { 
  2.   def joinKeys: List[(TableField, TableField)] 
  3.   def tables: List[TableName] 
  4.   def fields: List[TableField] 
  5.   def criteriaFields: List[TableField] 
  6.   def orderByFields: List[TableField]}trait FirstReportMetadata extends ReportMetadata 

至于原有的ReportTypeParser則被更名為ReportMetadataParser。

4. 引入Cake Pattern

如果仍然沿用之前的繼承關(guān)系,我們可以根據(jù)reportType分別創(chuàng)建不同報表的Parser實(shí)例。但是現(xiàn)在,我們需要將具體的ReportMetadata實(shí)例傳給ReportMetadataParser。至于具體傳遞什么樣的ReportMetadata實(shí)例,則取決于reportType。

這事實(shí)上是一種依賴注入。在Scala中,實(shí)現(xiàn)依賴注入通常是通過self type實(shí)現(xiàn)所謂Cake Pattern:

  1. class ReportMetadataParser extends DataSetFetcher with ParcConfiguration { 
  2.   self: ReportMetadata => 
  3.  
  4.   def evaluateSql(criteria: Option[List[Condition]]): String = { 
  5.     s"""       
  6.         select ${evaluateSelectFields}       
  7.         from ${evaluateJoinTables}       
  8.         where ${evaluateJoinKeys}       
  9.         ${evaluateCriteria(criteria)}       
  10.         ${evaluateOrderBy}     
  11.     """ 
  12.   }} 

為了更清晰地表達(dá)解析的含義,我將相關(guān)方法都更名為以evaluate為前綴。通過self type,ReportMetadataParser可以訪問ReportMetadata的方法,至于具體是什么樣的實(shí)現(xiàn),則取決于創(chuàng)建ReportMetadataParser對象時傳遞的具體類型。

通過將Metadata從Parser中分離出來,實(shí)際上是差異化編程的體現(xiàn)。這是我們在建立繼承體系時需要注意的。我們要學(xué)會觀察差異的部分,然后僅僅將差異的部分剝離出來,然后為其進(jìn)行更通用的抽象,由此再針對實(shí)現(xiàn)上的差異去建立繼承體系,如分離出來的ReportMetadata。當(dāng)我們要實(shí)現(xiàn)其他報表時,其實(shí)只需要定義ReportMetadata的實(shí)現(xiàn)類,提供不同的元數(shù)據(jù),就可以滿足要求。這就使得我們能夠有效地避免代碼的重復(fù),職責(zé)也更清晰。

5. 建立測試樁

引入Cake Pattern實(shí)現(xiàn)依賴注入還有利于我們編寫單元測試。例如在前面的實(shí)現(xiàn)中,我們通過Cake Pattern實(shí)際上注入了實(shí)現(xiàn)了DataSetFetcher的ReportMetadata類型。之所以需要實(shí)現(xiàn)DataSetFetcher,是因為我想通過它訪問數(shù)據(jù)庫中的數(shù)據(jù)集相關(guān)元數(shù)據(jù)。但是,在測試時我只想驗證sql解析的邏輯是否正確,并不希望真正去訪問數(shù)據(jù)庫。這時,我們可以建立一個DataSetFetcher的測試樁。

  1. trait StubDataSetFetcher extends DataSetFetcher { 
  2.     override def fetchField(dataSetId: ID, fieldName: String): Option[Field] = ... 
  3.     override def fetchDataSetByName(dataSetName: String): Option[DataSetFetched] = ... 
  4.     override def fetchDataSet(dataSetId: ID): Option[DataSetFetched] = ... 

StubDataSetFetcher通過繼承DataSetFetcher重寫了三個本來要訪問數(shù)據(jù)庫的方法,直接返回了需要的對象。然后,我再將這個trait定義在測試類中,并將其注入到ReportMetadataParser中:

  1. class ReportMetadataParserSpec extends FlatSpec with ShouldMatchers { 
  2.   it should "evaluate to sql for first report" in { 
  3.     val parser = new ReportMetadataParser() with FirstReportMetadata with StubDataSetFetcher 
  4.     val sql = parser.evaluateSql(None) 
  5.     sql should be(expectedSql) 
  6.   } 

6. 引入表達(dá)式樹

針對第一個報表,我們還有一個問題沒有解決,就是能夠支持相對復(fù)雜的where子句。例如條件:

  1. extractDate(a.TransactionDate) < extractDate(b.DueDate) and b.LoanFlag = 'D' 

不同的報表,可能會有不同的where子句。其中,extractDate函數(shù)是我自己定義的UDF。

前面提到的元數(shù)據(jù),主要都牽涉到表名、字段名,而這里的元數(shù)據(jù)是復(fù)雜的表達(dá)式。所以,我借鑒表達(dá)式樹的概念,建立了如下的表達(dá)式元數(shù)據(jù)結(jié)構(gòu):

  1. object ExpressionMetadata { 
  2.   trait Expression { 
  3.     def accept(parser: ExpressionParser): String = parser.evaluateExpression(this) 
  4.   } 
  5.   case class ConditionField(tableName:String, fieldName: String, funName: Option[String] = None) extends Expression 
  6.   case class IntValue(value: Int) extends Expression 
  7.   abstract class SingleExpression(expr: Expression) extends Expression { 
  8.     override def accept(evaluate: Expression => String): String = 
  9.       s"(${expr.accept(evaluate)} ${operator})" 
  10.     def operator: String 
  11.   } 
  12.  
  13.   case class IsNotNull(expr: Expression) extends SingleExpression(expr) { 
  14.     override def operator: String = "is not null" 
  15.   } 
  16.  
  17.   abstract class BinaryExpression(left: Expression, right: Expression) extends Expression { 
  18.     override def accept(parser: ExpressionParser): String = 
  19.       s"${left.accept(parser)} ${operator} ${right.accept(parser)}" 
  20.     def operator: String 
  21.   } 
  22.   case class Equal(left: Expression, right: Expression) extends BinaryExpression(left, right) { 
  23.     override def operator: String = "=" 
  24.   } 

7. 利用模式匹配實(shí)現(xiàn)訪問者模式

一開始,我為各個Expression對象定義的其實(shí)是evaluate方法,而非現(xiàn)在的accept方法。我認(rèn)為各個Expression對象都是自我完備的對象,它所擁有的知識(數(shù)據(jù)或?qū)傩?使得它能夠自我實(shí)現(xiàn)解析,并利用類似合成模式的方式實(shí)現(xiàn)遞歸的解析。

然而在實(shí)現(xiàn)時我遇到了一個問題:在解析字段名時,我們不能直接用字段名來組成where子句,因為在我們產(chǎn)品的Parquet數(shù)據(jù)集中,字段的名字其實(shí)是系統(tǒng)自動生成的。我們需要獲得:

  • 該字段對應(yīng)的表的別名
  • 該字段名在數(shù)據(jù)集中真正存儲的名稱,即code_name,例如C01。

換言之,真正要生成的條件子句應(yīng)該形如:

  1. extractDate(a.c1) < extractDate(b.c1) and b.c2 = 'D' 

然而,關(guān)于表名與別名的映射則是配置在ReportMetadata中,獲得別名與codeName的方法則被定義在ReportMetadataParser的內(nèi)部。如果將解析的實(shí)現(xiàn)邏輯放在Expression中,就需要依賴ReportMetadata與ReportMetadataParser。與之相比,我更傾向于將Expression傳給它們,讓它們完成對Expression的解析。換言之,Expression樹結(jié)構(gòu)只提供數(shù)據(jù),真正的解析職責(zé)則被委派給另外的對象,我將其定義為ExpressionParser:

  1. trait ExpressionParser { 
  2.   def evaluateExpression(expression: Expression): String} 

這種雙重委派與樹結(jié)構(gòu)的場景不正是訪問者模式最適宜的嗎?至于ExpressionParser的實(shí)現(xiàn),則可以交給ReportMetadataParser:

  1. class ReportMetadataParser extends DataSetFetcher with ParcConfiguration with ExpressionParser {override def evaluateExpression(expression: Expression): String = { 
  2.     expression match { 
  3.       case ConditionField(tableName, fieldName, funName) => 
  4.          val fullName = s"${table.aliasName}.${fieldName.codeName(table.originalName)}${orderType.getOrElse("")}" 
  5.          funName match { 
  6.             case Some(fun) => s"${funName}(${fullName})" 
  7.             case None => fullName 
  8.       case IntValue(v) => s"${v}" 
  9.       case StringValue(v) => s"'${v}'" 
  10.     } 
  11.   } 
  12.  
  13.   def evaluateWhereClause: String = { 
  14.     if (whereClause.isEmpty) return "" 
  15.     val clause = whereClause.map(c => c.accept(this)).mkString(" and ") 
  16.     s"where ${clause}" 
  17.   }} 

這里的evaluateExpression方法相當(dāng)于Visitor模式的visit方法。與傳統(tǒng)的Visitor模式不同,我不需要定義多個visit方法的重載,而是直接運(yùn)用Scala的模式匹配。

evaluateWhereClause方法會對Expression的元數(shù)據(jù)whereClause進(jìn)行解析,真正的實(shí)現(xiàn)是對每個Expression對象,執(zhí)行accept(this)方法,在其內(nèi)部又委派給this即ReportMetadataParser的evaluateExpression方法。

代碼中的whereClause是新增加的Metadata,具體的實(shí)現(xiàn)放到了FirstReportMetadata中:

  1. override def whereClause: List[Expression] = { 
  2.    List( 
  3.          LessThan( 
  4.                     ConditionField(AccountDetailTable, AccountDetailTableSchema.TransactionDate.toString, Some("extractDate")), 
  5.                     ConditionField(AoucherJournalTable, AoucherJournalTableSchema.DueDate.toString, Some("extractDate")) 
  6.                   ), 
  7.          Equal( 
  8.                 ConditionField(AccountDetailTable, AccountDetailTableSchema.LoanFlag.toString), 
  9.                 StringValue("D") 
  10.               ) 
  11.        ) 
  12.  } 

8. 用函數(shù)取代trait定義

在Scala中,我們完全可以用函數(shù)來替代trait:

  1. trait Expression { 
  2.   def accept(evaluate: Expression => String): String = evaluate(this) 
  3.  
  4. class ReportMetadataParser extends DataSetFetcher with ParcConfiguration { 
  5.   self: ReportMetadata with DataSetFetcher => 
  6.  
  7.   def evaluateExpr(expression: Expression): String = { 
  8.     expression match { 
  9.       case ConditionField(tableName, fieldName) => 
  10.         s"${aliasNameFor(tableName)}.${fieldName.codeName(tableName)}" 
  11.       case IntValue(v) => s"${v}" 
  12.       case StringValue(v) => s"'${v}'" 
  13.     } 
  14.   } 
  15.  
  16.   def evaluateWhereClause: String = { 
  17.     if (whereClause.isEmpty) return " true " 
  18.     whereClause.map(c => c.accept(evaluateExpr)).mkString(" and ") 
  19.   }} 

9. 演進(jìn)過程的提交記錄

這個設(shè)計的過程并非事先明確進(jìn)行針對性的設(shè)計,而是隨著功能的逐步實(shí)現(xiàn),伴隨著對代碼的重構(gòu)而逐漸浮現(xiàn)出來的。

整個過程的提交記錄如下圖所示(從上至下由最近到最遠(yuǎn)):

演進(jìn)過程的提交記錄

四、當(dāng)變化發(fā)生

通過前面一系列的設(shè)計演進(jìn),代碼結(jié)構(gòu)與質(zhì)量已經(jīng)得到了相當(dāng)程度的改進(jìn)與提高。關(guān)鍵是這樣的設(shè)計演進(jìn)是有價值回報的。在走出分離元數(shù)據(jù)關(guān)鍵步驟之后,設(shè)計就向著好的方向在發(fā)展。

在實(shí)現(xiàn)了第一張報表之后,后面四張報表的開發(fā)就變得非常容易了,只需要為這四張報表提供必需的元數(shù)據(jù)信息即可。

令人欣慰的是,這個設(shè)計還經(jīng)受了解決方案變化與需求變化的考驗。

1. 解決方案變化

在前面的實(shí)現(xiàn)中,我采用了Spark SQL的SQL方式執(zhí)行查詢。查詢時通過join關(guān)聯(lián)了多張表。在生產(chǎn)環(huán)境上部署后,發(fā)現(xiàn)查詢數(shù)據(jù)集的性能不盡如人意,必須改進(jìn)性能(關(guān)于性能的調(diào)優(yōu),則是另一個故事了,我會在另外的文章中講解)。由于join的表有大小表的區(qū)別,改進(jìn)性能的方式是引入broadcast。雖然可以通過設(shè)置spark.sql.autoBroadcastJoinThreshold來告知Spark滿足條件時啟用broadcast,但更容易控制的方法是調(diào)用DataFrame提供的API。

于是,實(shí)現(xiàn)方案就需要進(jìn)行調(diào)整:解析SQL的過程 ---> 組裝DataFrame API的過程

從代碼看,從原來的:

  1. def evaluateSql(criteria: Option[List[Condition]]): String = { 
  2.     logging { 
  3.       s""" 
  4.       select ${evaluateSelectFields} 
  5.       from ${evaluateJoinTables} 
  6.       on ${evaluateJoinKeys} 
  7.       where ${evaluateWhereClause}${evaluateCriteria(criteria)} 
  8.       ${evaluateOrderBy} 
  9.       """ 
  10.     } 
  11.   } 

變?yōu)榻馕龈鱾€API的參數(shù),然后在加載DataFrame的地方調(diào)用API:

  1. val dataFrames = tableNames.map { table => 
  2.       load(table.generatedName).as(table.aliasName) 
  3.     } 
  4.     sqlContext.udf.register("extractDate", new ExtractDate) 
  5.  
  6.     val (joinedDF, _) = dataFrames.zipWithIndex.reduce { 
  7.       (dfToIndex, accumulatorToIndex) => 
  8.         val (df, index) = dfToIndex 
  9.         val (acc, _) = accumulatorToIndex 
  10.         (df.join(broadcast(acc), keyColumnPairs(index)._1 === keyColumnPairs(index)._2), index) 
  11.     } 
  12.  
  13.     joinedDF.where(queryConditions) 
  14.       .orderBy(orderColumns: _*) 
  15.       .select(selectColumns: _*) 

解析方式雖然有變化,但需要的元數(shù)據(jù)還是基本相似,差別在于需要將之前我自己定義的字段類型轉(zhuǎn)換為Column類型。我們僅僅只需要修改 ReportMetadataParser類,在原有基礎(chǔ)上,增加部分獨(dú)有的元數(shù)據(jù)解析功能:

  1. class ReportMetadataParser extends ParcConfiguration with MortLogger { 
  2.   def evaluateKeyPairs: List[(Column, Column)] = { 
  3.     joinKeys.map { 
  4.       case (leftKey, rightKey) => (leftKey.toColumn, rightKey.toColumn) 
  5.     } 
  6.   } 
  7.   def evaluateSelectColumns: List[Column] = { 
  8.     fields.map(tf => tf.toColumn) 
  9.   } 
  10.   def evaluateOrderColumns: List[Column] = { 
  11.     orderByFields.map(f => f.toColumn) 
  12.   } 

2. 需求變化

我們的另一個客戶同樣需要類似的需求,區(qū)別在于他們的數(shù)據(jù)治理更好,我們只需要對已經(jīng)治理好的視圖數(shù)據(jù)執(zhí)行查詢即可,而無需跨表Join。在對現(xiàn)有代碼的包結(jié)構(gòu)做出調(diào)整,并定義了更為通用的Spark SQL查詢方法后,要做的工作其實(shí)就是定義對應(yīng)報表的元數(shù)據(jù)罷了。

僅僅花費(fèi)了1天半的時間,新客戶新項目的報表后端開發(fā)工作就完成了。要知道在如此短的開發(fā)周期內(nèi),大部分時間其實(shí)還是消耗在重構(gòu)工作上,包括重新調(diào)整現(xiàn)有代碼的包結(jié)構(gòu),提取重用代碼。現(xiàn)在,我可以悠閑一點(diǎn),喝喝茶,看看閑書,然后再重裝待發(fā),迎接下一個完全不同的新項目。

【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者】

戳這里,看該作者更多好文

責(zé)任編輯:趙寧寧 來源: 51CTO專欄
相關(guān)推薦

2014-11-12 13:22:34

2016-01-07 12:40:02

機(jī)器學(xué)習(xí)權(quán)威定義

2011-06-30 22:23:21

打印機(jī)常見問題

2020-11-02 09:48:35

C++泄漏代碼

2020-07-08 07:44:35

面試阿里加班

2013-10-22 09:22:07

Hadoop 2大數(shù)據(jù)

2011-06-28 10:41:50

DBA

2020-10-24 13:50:59

Python編程語言

2021-12-27 10:08:16

Python編程語言

2020-09-03 08:05:34

設(shè)計模式編程界

2020-03-18 13:07:16

華為

2020-03-10 07:51:35

面試諷刺標(biāo)準(zhǔn)

2020-10-18 12:53:29

黑科技網(wǎng)站軟件

2013-06-03 09:28:49

游戲設(shè)計

2024-05-31 12:56:06

.NET代碼方法

2017-02-28 11:13:36

華為

2012-08-28 09:21:59

Ajax查錯經(jīng)歷Web

2010-01-25 22:11:13

2023-08-02 10:11:00

DOM曝光封裝

2021-11-11 16:14:04

Kubernetes
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號

美女尤物久久精品| jizz久久久久久| 成人中文字幕在线| 韩国精品久久久999| www.av欧美| 99久久伊人| 一区二区三区中文免费| 国产无套精品一区二区| 无码人妻精品一区二区| 在线中文字幕第一区| 亚洲精品国产综合久久| 在线观看av日韩| 激情av在线播放| 国产亚洲欧美在线| 99国产精品久久久久老师| 精品国产午夜福利| 欧美色图首页| 中文字幕日韩有码| 制服丝袜第一页在线观看| 亚洲精品555| 午夜精品视频一区| 亚洲精品一卡二卡三卡四卡| 国产综合在线播放| 激情图片小说一区| 国产成人免费av电影| 久久久久免费看| 久久亚洲精品中文字幕蜜潮电影| 亚洲成人精品久久久| 在线观看国产中文字幕| 日本乱码一区二区三区不卡| 亚洲黄一区二区三区| 亚洲国产视频在线| 国内精品久久久久影院优| 男人天堂资源网| 日韩三级毛片| 精品免费视频一区二区| 手机在线国产视频| 国产亚洲人成a在线v网站 | 亚洲精品在线观看视频| 亚洲精品自拍网| 国产精品亚洲一区二区三区在线观看 | 亚洲a一级视频| 又骚又黄的视频| 日韩av电影免费观看高清完整版| 97在线精品国自产拍中文| 欧美日韩免费做爰视频| 亚洲精品极品少妇16p| 少妇av一区二区三区| 最新中文字幕av| 欧美女优在线视频| 亚洲欧洲午夜一线一品| 97伦伦午夜电影理伦片| 亚洲区小说区| 国产视频精品一区二区三区| 无码人妻精品一区二区三区温州| 日韩高清一级| 亚洲欧美999| 色无极影院亚洲| 免费观看久久av| 亚洲人成电影网站色www| 三上悠亚ssⅰn939无码播放 | 亚洲成人第一| 色三级在线观看| 中文字幕一区三区| 美国av在线播放| 青草视频在线免费直播 | 无码免费一区二区三区| 日韩精品一二区| 国产日韩中文字幕| 国产黄色大片网站| 丰满少妇在线观看bd| 午夜久久一区| 国a精品视频大全| 欧美一级特黄视频| 人妖欧美一区二区| 亚洲jizzjizz日本少妇| 亚洲精品久久久久avwww潮水| 成人久久视频在线观看| 免费国产一区二区| 日本福利专区在线观看| 亚洲欧美国产高清| 青青青免费在线| 欧美国产大片| 日韩一区二区三区在线| 人妻av一区二区| 欧美一区二区三区激情视频 | www.99热| 在线观看日韩| 欧美一区二区三区四区在线| 免费在线不卡av| 国产宾馆实践打屁股91| 免费试看一区| 曰本三级在线| 色拍拍在线精品视频8848| 天天色天天综合网| 在线成人动漫av| 欧美精品一区在线播放| www.国产一区二区| 国产一区二区三区黄视频| 久久精品国产综合精品| 欧美私人网站| 欧美性生活大片免费观看网址| 天天综合网久久| 久久影视三级福利片| 色哟哟亚洲精品一区二区| 久久露脸国语精品国产91| 久久av中文字幕片| 欧美黑人xxxxx| 色呦呦在线播放| 欧美在线|欧美| 久久精品综合视频| 亚洲欧美一级二级三级| 国产99久久久国产精品免费看 | 国产一区二区不卡老阿姨| 精品日本一区二区三区| 欧美边添边摸边做边爱免费| 午夜精品影院在线观看| 在线视频一二区| 精品盗摄女厕tp美女嘘嘘| 欧美肥臀大乳一区二区免费视频| 无码人妻丰满熟妇奶水区码| 高清av一区二区| 亚洲免费av网| 四虎4545www国产精品| 亚洲精品成人网| 欧美人妻精品一区二区免费看| 美女视频免费一区| 日韩欧美第二区在线观看| 久草在线资源福利站| 欧美一级电影网站| 亚洲天堂网av在线| 免费高清不卡av| 日韩国产精品一区二区三区| 美女尤物在线视频| 日韩欧美中文字幕精品| 午夜精品福利在线视频| 日产欧产美韩系列久久99| 久久免费99精品久久久久久| 91豆花视频在线播放| 欧美成人综合网站| 久久久久久久国产精品毛片| 国产乱码精品一区二区三区忘忧草 | 中文字幕线观看| 国产精品久久天天影视| 国产精品欧美日韩一区二区| 国产精品麻豆一区二区三区| 欧美午夜精品伦理| 好吊日免费视频| 欧美一区=区| 欧美凹凸一区二区三区视频| 在线最新版中文在线| 国产视频一区在线| 国产91精品看黄网站在线观看| 国产精品久久久久久妇女| 国产做a爰片久久毛片| 美日韩免费视频| 在线视频超级| 亚洲香蕉在线观看| 国产九色91回来了| 成人免费视频在线观看| 99精品视频国产| 欧美88av| 国产亚洲自拍偷拍| 麻豆国产在线| 亚洲欧美在线免费| 在线免费一级片| 亚洲男人电影天堂| 麻豆短视频在线观看| 亚洲青色在线| 日本一区免费在线观看| 韩国精品视频在线观看 | 欧美日韩高清免费| 素人一区二区三区| 久久视频中文字幕| 免费av网站在线播放| 欧美午夜精品久久久久久久| 欧美福利在线视频| 国产成人在线视频网站| aa在线观看视频| 日韩欧美网站| 高清视频一区| 国产成人精品一区二三区在线观看| 最新国产精品亚洲| 好男人www在线视频| 色综合激情五月| 国产精品 欧美激情| www.亚洲色图.com| 国产九九在线观看| 亚洲黄页一区| 一区二区视频在线播放| 亚洲精品影片| 国产精品27p| 国产偷倩在线播放| 中文字幕欧美精品在线| 国产www免费观看| 日本高清成人免费播放| 玖玖爱这里只有精品| 久久精品亚洲麻豆av一区二区| 在线观看免费av网址| 国产伦理一区| 成人高清dvd| 日韩成人精品一区二区| 亚洲欧美日韩一区在线| 黄网站色视频免费观看| 粉嫩的18在线观看极品精品| 国产精品成人国产乱一区| 欧美6一10sex性hd| 中文字幕亚洲无线码a| 西西人体44www大胆无码| 538在线一区二区精品国产| 91玉足脚交嫩脚丫在线播放| 樱桃视频在线观看一区| 永久免费毛片在线观看| 不卡视频一二三| 亚洲精品中文字幕乱码无线| 丝袜美腿一区二区三区| 亚洲一区二区三区av无码| 欧美国产一级| 日韩av电影免费播放| 欧美五码在线| 国产精品国模大尺度私拍| 色噜噜成人av在线| 国产精品久久久久av| 免费v片在线观看| 久久久久久综合网天天| 国产不卡在线| 北条麻妃在线一区二区| 懂色一区二区三区| 亚洲美女精品成人在线视频| 涩涩视频免费看| 精品免费国产一区二区三区四区| 国产一区二区在线视频聊天| 欧美系列一区二区| 波多野结衣绝顶大高潮| 色综合久久精品| 天堂а√在线中文在线新版 | 瑟瑟视频在线免费观看| 色婷婷av一区二区三区之一色屋| 国产成人亚洲欧洲在线| 亚洲成人中文在线| 日本少妇激情舌吻| 亚洲1区2区3区视频| 久久久久黄色片| 亚洲成a人片综合在线| 久久久久性色av无码一区二区| 夜色激情一区二区| 久久精品国产亚洲AV无码麻豆| 一区二区三区高清在线| 久久成人国产精品入口| 亚洲午夜一区二区| 中国一级免费毛片| 色综合久久久久综合| 欧美日韩亚洲一二三| www国产在线观看| 亚洲性线免费观看视频成熟| 激情小视频在线| 在线观看国产精品淫| 日本蜜桃在线观看| 欧美精品一二区| 97蜜桃久久| 日本91av在线播放| jizz亚洲女人高潮大叫| 成人久久18免费网站图片| 玖玖精品一区| 国产伦精品一区二区三毛| 香蕉久久夜色精品国产更新时间| 蜜桃av噜噜一区二区三区| 久9久9色综合| 制服诱惑一区| 亚洲激精日韩激精欧美精品| 日本精品一区在线观看| 秋霞电影一区二区| 波多野结衣在线免费观看| 国产成人亚洲精品狼色在线 | 中文字幕av免费观看| 7777女厕盗摄久久久| 成人毛片在线免费观看| 亚洲精选在线观看| 嫩草在线视频| 国外成人在线视频| 日本美女久久| 成人片在线免费看| 国产91精品对白在线播放| 亚州欧美一区三区三区在线| 欧美片第1页综合| 人人妻人人添人人爽欧美一区| 天堂成人国产精品一区| 日韩精品在线播放视频| 99精品桃花视频在线观看| 超碰人人人人人人人| 亚洲永久免费av| 午夜视频网站在线观看| 精品免费视频.| 98在线视频| 91国内揄拍国内精品对白| 免费一区二区三区四区| 久久精品日产第一区二区三区乱码 | 亚洲精品中文字| 2024短剧网剧在线观看| 日韩av电影院| 成人线上播放| 在线观看国产一区| 国产精品久久国产愉拍| 欧美又黄又嫩大片a级| 久久久久久久久久美女| 久久久综合久久| 亚洲成在人线免费观看| 91麻豆国产精品| 国产欧美日韩影院| 妺妺窝人体色777777| 久久精品999| 亚洲图片另类小说| 亚洲成人av电影在线| 国产精品老熟女视频一区二区| 亚洲欧美国产va在线影院| 亚洲国产精品精华素| 国产免费成人av| 国产亚洲一区二区三区啪| 99久久国产综合精品五月天喷水| 韩国av一区二区| 中文字幕欧美激情极品| 欧美日韩国产在线| 成人av手机在线| 久久亚洲综合国产精品99麻豆精品福利 | 欧美激情一区二区三区不卡| 91久久国产视频| 欧美成人一区二区| 欧美激情视频在线播放| 国产精品高清在线| 亚洲图片久久| 久久久久久久久久久福利| 菠萝蜜视频在线观看一区| 欧美交换国产一区内射| 欧美一区国产二区| 日本美女高清在线观看免费| 国产欧美久久一区二区| 成人激情电影在线| 亚州精品一二三区| 国产欧美视频在线观看| 9i精品福利一区二区三区| 精品性高朝久久久久久久| 激情视频网站在线播放色| 国产成人精品日本亚洲11| 欧美日一区二区三区在线观看国产免| 日本黄色三级网站| 亚洲免费av高清| 性色av蜜臀av| 欧美激情啊啊啊| 国产精品45p| 尤物av无码色av无码| 91视视频在线观看入口直接观看www| 亚欧洲精品在线视频| 亚洲国产99精品国自产| 欧美三级网站| 欧美极品一区| 日本不卡免费在线视频| 四虎影视1304t| 欧美一区二区私人影院日本| gogo在线高清视频| 国产厕所精品在线观看| 99av国产精品欲麻豆| 亚洲区自拍偷拍| 欧美乱妇一区二区三区不卡视频| av中文字幕在线观看| 国产激情一区二区三区在线观看| 一区二区高清| 青青草自拍偷拍| 欧美一激情一区二区三区| heyzo在线| 欧美视频1区| 精品一区二区三区av| 午夜精品视频在线观看一区二区 | 亚洲日韩视频| 丰腴饱满的极品熟妇| 欧美天堂亚洲电影院在线播放| 日本精品在线| 狠狠爱一区二区三区| 日韩精品一二区| 69av.com| 精品中文字幕久久久久久| 婷婷久久免费视频| 和岳每晚弄的高潮嗷嗷叫视频| 久久久久9999亚洲精品| 国产孕妇孕交大片孕| 久久久女人电视剧免费播放下载| 免费视频一区三区| 欧美色图校园春色| 日韩人体视频一二区| 久久久久久国产精品免费无遮挡| 国产精品免费在线播放| 青青草国产成人99久久| 毛片a片免费观看| 国产一区二区三区直播精品电影| 欧美a在线观看| 无遮挡又爽又刺激的视频| 日韩毛片一二三区| 深夜福利免费在线观看| 亚洲iv一区二区三区| 日日夜夜免费精品视频| 久久精品国产亚洲av高清色欲|