フレクトのクラウドblog re:newal

http://blog.flect.co.jp/cloud/からさらに引っ越しています

Apexからレポートを実行しCSVで出力する

エンジニアの藤野です。

Salesforceの主要な機能の一つであるレポートは、Apexから実行することもできます。 公式のドキュメントとしては下記のものなどがあります。

この記事では一例として、レポートの実行結果をCSVとして出力するコードを実装し、解説します。

実行環境は下記のとおりです。

使用するレポート

レポートの題材として、Trailheadのレポートの形式設定の「マトリックスレポート」で作成している「Revenue Trend by Type」レポートを使用します。

これは行、列ともにレベル1のグルーピングを行うマトリックスレポートです。

ゴールと出力例

適当なレコードを作成し、「Revenue Trend by Type」レポートを実行しました。

レポート実行結果
レポート実行結果

これに対して、ゴールは下記のCSVを出力することです。 CSVファイル名はレポート名をそのまま使うものとします。 また、詳細は出力しません。

Revenue+Trend+by+Type.csv:

完了予定月,種別,-,Existing Customer - Upgrade,Existing Customer - Replacement,Existing Customer - Downgrade,New Customer,合計
2019/01/01,金額 合計:,0.0,0.0,0.0,0.0,1.0E7,1.0E7
2019/02/01,金額 合計:,0.0,0.0,0.0,2000000.0,0.0,2000000.0
2019/03/01,金額 合計:,880000.0,0.0,0.0,0.0,0.0,880000.0
2019/05/01,金額 合計:,0.0,0.0,5000000.0,0.0,0.0,5000000.0
2019/07/01,金額 合計:,0.0,0.0,0.0,0.0,0.0,0.0
2019/08/01,金額 合計:,0.0,0.0,0.0,100000.0,0.0,100000.0
2019/09/01,金額 合計:,0.0,2.0E7,0.0,0.0,0.0,2.0E7
合計,金額 合計:,880000.0,2.0E7,5000000.0,2100000.0,1.0E7,3.798E7

また、このCSVExcelでそのまま表示できるものとします。

Excelで表示した状態:

CSVをExcelで表示
CSVExcelで表示

実装の構成

CSVの出力にはVisualforce Pageを使う方法を採用しました。(参考

今回は下記3コンポーネントで構成しています。

  • Report2CsvDownload.vfp (入口となるVisualforceページ)
  • Report2CsvDownloadController.apxc (カスタムコントローラ)
  • Report2Csv.apxc (レポートの実行、CSV化を行うクラス)

コンポーネントの関係は下記のようになっています。

実装の構成
実装の構成

なお、Report2CsvDownload.vfpはクエリパラメータにレポートのIDを受け取ることを想定しています。 例えば、下記のように生成されたリンクから遷移します。

public PageReference download(reportId) {
    PageReference pr = new PageReference('/apex/Report2CsvDownload');
    pr.getParameters().put('id',reportId);
    pr.setRedirect(false);
    return pr;
}

実装

実際のコードは下記のとおりです。

Report2CsvDownload.vfp

<apex:page controller="Report2CsvDownloadController" cache="true"
    contentType="text/csv;charset=Shift-JIS" readOnly="true">
    <apex:repeat value="{!rows}" var="row">
        <apex:outputText value="{!row}" />
    </apex:repeat>
</apex:page>

rows変数に格納されているコレクションを出力します。 今回はStringのListを想定しています。

また、Excelで表示できるCSVとするためにcontentType="text/csv;charset=Shift-JIS"を指定します。

Report2CsvDownloadController.apxc

public class Report2CsvDownloadController {
    public List<String> rows { get; set; }
    
    public Report2CsvDownloadController() {
        String reportId = apexpages.currentpage().getparameters().get('id');
        Report2Csv runner = new Report2Csv(reportId);
        
        String reportName = runner.metadata.getName();
        String encoded = EncodingUtil.urlEncode(reportName + '.csv', 'UTF-8');
        ApexPages.currentPage().getHeaders().put('Content-Disposition', 'attachment; filename=' + encoded);
        
        List<String> rows = runner.generate();
        this.rows = rows;
    }
}

ページのパラメータから取得したレポートIDを使ってReport2Csvインスタンスを作成し、CSC(StringのList)を取得します。 また、レポートメタデータからレポート名を取得し、出力に使用しています。

Report2Csv.apxc

コードを展開

public with sharing class Report2Csv {
    
    public Reports.ReportMetadata metadata; // レポート集計項目の定義
    public Reports.ReportResults results;
    public Reports.ReportExtendedMetadata extendedMetadata; // レポート集計項目の、実際に集計されたレコードのラベル
    
    public Report2Csv(String reportId) {
        this.metadata = Reports.ReportManager.describeReport(reportId).getReportMetadata();
        this.results = Reports.ReportManager.runReport(reportId, metadata, false);
        this.extendedMetadata = results.getReportExtendedMetadata();
    }
    
    public List<String> generate() {
        List<List<String>> matrix = new List<List<String>>();
        
        matrix.addAll(makeHeader());
        matrix.addAll(makeMatrix());
        return matrixToRow(escapeMatrix(matrix));
    }
    
    // 見出し部
    public List<List<String>> makeHeader() {
        List<List<String>> matrix = new List<List<String>>();
        List<String> row = new List<String>();
        
        // 行グルーピング
        List<Reports.GroupingInfo> groupingDown = this.metadata.getGroupingsDown();
        for(Reports.GroupingInfo grouping : groupingDown) {
            String groupingName = grouping.getName();
            Reports.GroupingColumn column = this.extendedMetadata.getGroupingColumnInfo().get(groupingName);
            row.add(column.getLabel());
        }
        // 列グルーピング
        List<Reports.GroupingInfo> groupingsAcross = this.metadata.getGroupingsAcross();
        for(Reports.GroupingInfo grouping : groupingsAcross) {
            String groupingName = grouping.getName();
            Reports.GroupingColumn column = this.extendedMetadata.getGroupingColumnInfo().get(groupingName);
            row.add(column.getLabel());
        }
        for(Reports.GroupingValue colmunGroupingValue: this.results.getGroupingsAcross().getGroupings()) {
            row.add(colmunGroupingValue.getLabel());
        }
        row.add('合計');
        matrix.add(row);
        return matrix;
    }
    
    public List<List<String>> makeMatrix() {
        List<List<String>> matrix = new List<List<String>>();
        List<Reports.GroupingValue> rowGroupings = this.results.getGroupingsDown().getGroupings();
        for(Reports.GroupingValue rowGrouping : rowGroupings) {
            String rowKey = rowGrouping.getKey();
            for(Integer i = 0; i < this.metadata.getAggregates().size(); i++) {
                List<String> row = new List<String>();
                if(i == 0) {
                    row.add(rowGrouping.getLabel());
                } else {
                    row.add('');
                }
                String aggregateKey= this.metadata.getAggregates()[i];
                row.add(this.extendedMetadata.getAggregateColumnInfo().get(aggregateKey).getLabel());
                List<Reports.GroupingValue> columnGroupings = this.results.getGroupingsAcross().getGroupings();
                for(Integer j = 0; j < columnGroupings.size() + 1; j++) {
                    String colmunKey;
                    if(j == columnGroupings.size()) { // 総計列
                        colmunKey = 'T';
                    } else {
                        colmunKey = columnGroupings[j].getKey();
                    }
                    String factKey = rowKey + '!' + colmunKey;
                    Reports.ReportFact rf = this.results.getFactMap().get(factKey);
                    List<Reports.SummaryValue> summaryValues = rf.getAggregates();
                    String summaryValue = formatSummaryValue(summaryValues[i]);
                    row.add(summaryValue);
                }
                matrix.add(row);
            }
        }
        
        // 総計行
        for(Integer i = 0; i < this.metadata.getAggregates().size(); i++) {
            List<String> row = new List<String>();
            if(i == 0){
                row.add('合計');
            } else {
                row.add('');
            }
            String aggregateKey= this.metadata.getAggregates()[i];
            row.add(this.extendedMetadata.getAggregateColumnInfo().get(aggregateKey).getLabel());
            List<Reports.GroupingValue> columnGroupings = this.results.getGroupingsAcross().getGroupings();
            for(Integer j = 0; j < columnGroupings.size() + 1; j++) {
                String colmunKey;
                if(j == columnGroupings.size()) { // 総計列
                    colmunKey = 'T';
                } else {
                    colmunKey = columnGroupings[j].getKey();
                }
                String factKey =  'T!' + colmunKey;
                Reports.ReportFact rf = this.results.getFactMap().get(factKey);
                List<Reports.SummaryValue> summaryValues = rf.getAggregates();
                String summaryValue = formatSummaryValue(summaryValues[i]);
                row.add(summaryValue);
            }
            matrix.add(row);
        }
        return matrix;
    }
    
    // Reports.SummaryValue インスタンスをStringに変換する
    private String formatSummaryValue(Reports.SummaryValue summaryValue) {
        String ret;
        if(summaryValue == null) {
            return '';
        }
        if(summaryValue.getValue() instanceof Double) {
            Double rounded = Math.roundToLong(Double.valueOf(summaryValue.getValue()) * 100) / 100;
            ret = String.valueOf(rounded);
        } else {
            ret = String.valueOf(summaryValue.getValue());
        }
        return ret;
    }

    // CSV出力のため、値をエスケープする
    private List<List<String>> escapeMatrix(List<List<String>> matrix) {
        List<List<String>> escapedMatrix = new List<LIst<String>>();
        for(List<String> row : matrix) {
            List<String> escapedRow = new List<String>();
            for(String value: row) {
                if(value == null){
                    escapedRow.add('');
                } else {
                    escapedRow.add(value.escapeCsv());
                }
            }
            escapedMatrix.add(escapedRow);
        }
        return escapedMatrix;
    }
    
    // リストのリストを、Stringのリストにする
    private List<String> matrixToRow(List<List<String>> matrix) {
        List<String> joinedRows = new List<String>();
        for(List<String> row : matrix) {
            String joinedRow = String.join(row, ',') + '\n';
            joinedRows.add(joinedRow);
        }
        return joinedRows;
    }
}

コードが長いため、メソッドごとに解説します。

コンストラク

public Report2Csv(String reportId) {
    this.metadata = Reports.ReportManager.describeReport(reportId).getReportMetadata();
    this.results = Reports.ReportManager.runReport(reportId, metadata, false);
    this.extendedMetadata = results.getReportExtendedMetadata();
}

レポートIDを受け取り、レポートを実行します。

    this.metadata = Reports.ReportManager.describeReport(reportId).getReportMetadata();

レポートIDからReportMetadataを取得します。 ReportMetadataは大まかに言うと、レポート実行前に取得可能なレポートの定義・絞り込み条件などの情報が収められています。

    this.results = Reports.ReportManager.runReport(reportId, metadata, false);

レポートを実行します。 第3引数のincludeDetailsfalseを指定して詳細を出力しないようにしています。

    this.extendedMetadata = results.getReportExtendedMetadata();

実行結果からReportExtendedMetadataを取得します。

ReportExtendedMetadataReportMetadataに対して、レポート実行後の情報を持っています。 今回は主に、グルーピング項目の実行後の値を取得するために使います。

generateメソッド

public List<String> generate() {
    List<List<String>> matrix = new List<List<String>>();
    
    matrix.addAll(makeHeader());
    matrix.addAll(makeMatrix());
    return matrixToRow(escapeMatrix(matrix));
}

レポート実行結果からCSVを組み立てます。 makeHeaderメソッドで見出し部分を、makeMatrixメソッドで値を行列として組み立て、matrixToRowでCSVの形式にします。

makeHeaderメソッド

// 見出し部
public List<List<String>> makeHeader() {
    List<List<String>> matrix = new List<List<String>>();
    List<String> row = new List<String>();
    
    // 行グルーピング
    List<Reports.GroupingInfo> groupingDown = this.metadata.getGroupingsDown();
    for(Reports.GroupingInfo grouping : groupingDown) {
        String groupingName = grouping.getName();
        Reports.GroupingColumn column = this.extendedMetadata.getGroupingColumnInfo().get(groupingName);
        row.add(column.getLabel());
    }
    // 列グルーピング
    List<Reports.GroupingInfo> groupingsAcross = this.metadata.getGroupingsAcross();
    for(Reports.GroupingInfo grouping : groupingsAcross) {
        String groupingName = grouping.getName();
        Reports.GroupingColumn column = this.extendedMetadata.getGroupingColumnInfo().get(groupingName);
        row.add(column.getLabel());
    }
    for(Reports.GroupingValue colmunGroupingValue: this.results.getGroupingsAcross().getGroupings()) {
        row.add(colmunGroupingValue.getLabel());
    }
    row.add('合計');
    matrix.add(row);
    return matrix;
}

見出し部分、つまり下記の部分を取得します。

見出し部分
見出し部分

    // 行グルーピング
    List<Reports.GroupingInfo> groupingDown = this.metadata.getGroupingsDown();
    for(Reports.GroupingInfo grouping : groupingDown) {
        String groupingName = grouping.getName();
        Reports.GroupingColumn column = this.extendedMetadata.getGroupingColumnInfo().get(groupingName);
        row.add(column.getLabel());
    }

ここで取得するのは行グルーピングの表示ラベルです。

行グルーピング
行グルーピング

ほしいのはローカライズされた表示名であり、これはReportExtendedMetadataクラスのgetDetailColumnInfoメソッドでとして取得できます。 ただし、これは{グルーピングに使用した項目のAPI名:GroupingColumnクラスインスタンス}のMapになっており、行・列両方のグルーピング情報が入っています。

例:

{CLOSE_MONTH=Reports.GroupingColumn[dataType=DATE_DATA, groupingLevel=0, label=完了予定月, name=CLOSE_MONTH], TYPE=Reports.GroupingColumn[dataType=PICKLIST_DATA, groupingLevel=0, label=種別, name=TYPE]}

ここから行グルーピングの表示ラベルを取得するには項目のAPI名が必要です。 そのため、ReportMetadataクラスのgetGroupingsDownメソッドで行グルーピングの設定を取得しています。

列グルーピングも同様です。

    // 列グルーピング
    List<Reports.GroupingInfo> groupingsAcross = this.metadata.getGroupingsAcross();
    for(Reports.GroupingInfo grouping : groupingsAcross) {
        String groupingName = grouping.getName();
        Reports.GroupingColumn column = this.extendedMetadata.getGroupingColumnInfo().get(groupingName);
        row.add(column.getLabel());
    }

列グルーピング
列グルーピング

ReportMetadataクラスのgetGroupingsAcrossメソッドで列グルーピングの設定を取得し、その後、表示ラベルを取得します。

次に、実際に列のグルーピングに使われた値の表示ラベルを取得します。

    for(Reports.GroupingValue colmunGroupingValue: this.results.getGroupingsAcross().getGroupings()) {
        row.add(colmunGroupingValue.getLabel());
    }

列グルーピングの値
列グルーピングの値

総計列は固定値でラベルを追加しています。ここもローカライズされているので取得する方法がありそうなのですが、今回は見つけられませんでした。

    row.add('合計');

合計列のラベル
総計列のラベル

makeMatrixメソッド

レポートの行グルーピングラベルおよび、レコードデータ、総計列、総計行を埋めます。

コードを展開

public List<List<String>> makeMatrix() {
    List<List<String>> matrix = new List<List<String>>();
    List<Reports.GroupingValue> rowGroupings = this.results.getGroupingsDown().getGroupings();
    for(Reports.GroupingValue rowGrouping : rowGroupings) {
        String rowKey = rowGrouping.getKey();
        for(Integer i = 0; i < this.metadata.getAggregates().size(); i++) {
            List<String> row = new List<String>();
            if(i == 0) {
                row.add(rowGrouping.getLabel());
            } else {
                row.add('');
            }
            String aggregateKey= this.metadata.getAggregates()[i];
            row.add(this.extendedMetadata.getAggregateColumnInfo().get(aggregateKey).getLabel());
            List<Reports.GroupingValue> columnGroupings = this.results.getGroupingsAcross().getGroupings();
            for(Integer j = 0; j < columnGroupings.size() + 1; j++) {
                String colmunKey;
                if(j == columnGroupings.size()) { // 総計列
                    colmunKey = 'T';
                } else {
                    colmunKey = columnGroupings[j].getKey();
                }
                String factKey = rowKey + '!' + colmunKey;
                Reports.ReportFact rf = this.results.getFactMap().get(factKey);
                List<Reports.SummaryValue> summaryValues = rf.getAggregates();
                String summaryValue = formatSummaryValue(summaryValues[i]);
                row.add(summaryValue);
            }
            matrix.add(row);
        }
    }
    
    // 総計行
    for(Integer i = 0; i < this.metadata.getAggregates().size(); i++) {
        List<String> row = new List<String>();
        if(i == 0){
            row.add('合計');
        } else {
            row.add('');
        }
        String aggregateKey= this.metadata.getAggregates()[i];
        row.add(this.extendedMetadata.getAggregateColumnInfo().get(aggregateKey).getLabel());
        List<Reports.GroupingValue> columnGroupings = this.results.getGroupingsAcross().getGroupings();
        for(Integer j = 0; j < columnGroupings.size() + 1; j++) {
            String colmunKey;
            if(j == columnGroupings.size()) { // 総計列
                colmunKey = 'T';
            } else {
                colmunKey = columnGroupings[j].getKey();
            }
            String factKey =  'T!' + colmunKey;
            Reports.ReportFact rf = this.results.getFactMap().get(factKey);
            List<Reports.SummaryValue> summaryValues = rf.getAggregates();
            String summaryValue = formatSummaryValue(summaryValues[i]);
            row.add(summaryValue);
        }
        matrix.add(row);
    }
    return matrix;
}

データ部分
データ部分

レポートのデータおよび総計はファクトマップという形式で扱います。 マトリックスレポートでは0始まりのインデックスを組み合わせてデータを取得します。ただし、総計行および総系列のインデックスはTです。

    List<Reports.GroupingValue> rowGroupings = this.results.getGroupingsDown().getGroupings();

ReportResultsクラスのgetGroupingsDownメソッドから行グルーピングの情報を取得します。 具体的には行をグルーピングした値、表示ラベル、ファクトマップで使える行の識別子(キー)などです。 今回のレポートでは下記のような情報が取得できます。

(Reports.GroupingValue[groupings=null, key=0, label=2019/01/01, value=2019-01-01 00:00:00], Reports.GroupingValue[groupings=null, key=1, label=2019/02/01, value=2019-02-01 00:00:00], ...

それでは、行ごとの処理に入っていきます。

    for(Integer i = 0; i < this.metadata.getAggregates().size(); i++) {
        List<String> row = new List<String>();
        if(i == 0) {
            row.add(rowGrouping.getLabel());
        } else {
            row.add('');
        }
        String aggregateKey= this.metadata.getAggregates()[i];
        row.add(this.extendedMetadata.getAggregateColumnInfo().get(aggregateKey).getLabel());;

集計項目の表示ラベル
集計項目の表示ラベル

ここでは集計項目の表示ラベルを取得します。 行・列グルーピング項目の表示ラベルと同様に、ReportMetadataクラスにAPI項目名が、ReportExtendedMetadataクラスに表示ラベルが入っています。 イテレーションしている理由は、グルーピングレベルが1より大きい場合getAggregates()の結果が複数になるためです。今回はレベル1なのでiは0で固定ですが……。この集計項目のインデックスは、値を表示するときにも必要になります。

        List<Reports.GroupingValue> columnGroupings = this.results.getGroupingsAcross().getGroupings();
        for(Integer k = 0; k < columnGroupings.size() + 1; k++) {
            String colmunKey;
            if(k == columnGroupings.size()) { // 総計列
                colmunKey = 'T';
            } else {
                colmunKey = columnGroupings[k].getKey();
            }
            String factKey = rowKey + '!' + colmunKey;
            Reports.ReportFact rf = this.results.getFactMap().get(factKey);
            List<Reports.SummaryValue> summaryValues = rf.getAggregates();
            String summaryValue = formatSummaryValue(summaryValues[j]);
            row.add(summaryValue);
        }
        matrix.add(row);

集計項目の値
集計項目の値

列の識別子(キー)も取得し、ファクトマップから値を取得します。 ファクトマップのキーは行のキーと列のキーを!で連結した形になります。

    // 総計行
    for(Integer i = 0; i < this.metadata.getAggregates().size(); i++) {
        List<String> row = new List<String>();
        if(i == 0){
            row.add('合計');
        } else {
            row.add('');
        }
        String aggregateKey= this.metadata.getAggregates()[i];
        row.add(this.extendedMetadata.getAggregateColumnInfo().get(aggregateKey).getLabel());
        List<Reports.GroupingValue> columnGroupings = this.results.getGroupingsAcross().getGroupings();
        for(Integer j = 0; j < columnGroupings.size() + 1; j++) {
            String colmunKey;
            if(j == columnGroupings.size()) { // 総計列
                colmunKey = 'T';
            } else {
                colmunKey = columnGroupings[j].getKey();
            }
            String factKey =  'T!' + colmunKey;
            Reports.ReportFact rf = this.results.getFactMap().get(factKey);
            List<Reports.SummaryValue> summaryValues = rf.getAggregates();
            String summaryValue = formatSummaryValue(summaryValues[i]);
            row.add(summaryValue);
        }
        matrix.add(row);

総計行
総計行

総計行の値の取得は他の行とほぼ同じです。分岐が複雑になるのを避けるために処理を分けています。が、最終的には処理をまとめたほうがいいと思います。

他のメソッド

コードを展開

    // Reports.SummaryValue インスタンスをStringに変換する
    private String formatSummaryValue(Reports.SummaryValue summaryValue) {
        String ret;
        if(summaryValue == null) {
            return '';
        }
        if(summaryValue.getValue() instanceof Double) {
            Double rounded = Math.roundToLong(Double.valueOf(summaryValue.getValue()) * 100) / 100;
            ret = String.valueOf(rounded);
        } else {
            ret = String.valueOf(summaryValue.getValue());
        }
        return ret;
    }

    // CSV出力のため、値をエスケープする
    private List<List<String>> escapeMatrix(List<List<String>> matrix) {
        List<List<String>> escapedMatrix = new List<LIst<String>>();
        for(List<String> row : matrix) {
            List<String> escapedRow = new List<String>();
            for(String value: row) {
                if(value == null){
                    escapedRow.add('');
                } else {
                    escapedRow.add(value.escapeCsv());
                }
            }
            escapedMatrix.add(escapedRow);
        }
        return escapedMatrix;
    }
    
    // リストのリストを、Stringのリストにする
    private List<String> matrixToRow(List<List<String>> matrix) {
        List<String> joinedRows = new List<String>();
        for(List<String> row : matrix) {
            String joinedRow = String.join(row, ',') + '\n';
            joinedRows.add(joinedRow);
        }
        return joinedRows;
    }

いずれもCSV出力のためにデータを整えるためのメソッドです。

formatSummaryValueメソッド

Reports.SummaryValueとして取得したレポートの値をStringに変換します。 小数は末尾の余分な0を削除するために四捨五入しています。

escapeMatrixメソッド

StringクラスのescapeCsvメソッドで2重リストをエスケープします。

matrixToRowメソッド

VFpageにStringのListで渡すために、二重リストの内側のリストを,で連結します。

所感

今回紹介した実装は行列1レベルのマトリックス形式以外には対応していません。 実際、他のレポート形式にも対応させたコードも書いてみたのですが、なかなか共通化できず、個別の処理が必要になりました。

本機能でレポート機能を汎用的に使えるようにするのはかなり大変だと思います。 それよりも特定のレポートの値が必要な場合に決め打ちで実行する、という使い方で力を発揮するものだと思いました。

以上です。