Apex

VisualforceのcontentType指定でCSVを出力する実装

検索結果を絞り込んでからダウンロードする2ページ構成で、Visualforceのcontent属性を使ったCSV出力を説明します。cacheとビューステートの注意点も扱います。

2021.06.04

Visualforceだけで作る理由

一覧をCSVで持ち出すだけなら、データローダかレポートのCSVエクスポートで足ります。レポートのCSVエクスポートに件数の上限はありません。

それでも専用ページを作るのは、検索条件を業務ごとに固定したい、出力する項目を絞りたい、特定のプロファイルだけに使わせたい、といった事情があるときです。LWCなどの画面部品を使わず、URLを知っていれば開けるページだけで完結させたいときにも向いています。

検索とダウンロードを処理するApexクラス

CSVDownloadController.cls

public inherited sharing class CSVDownloadController {
    public List<Contact> contacts {get; set;}
    public CSVDownloadController() {
        this.contacts = new List<Contact>();
    }
    public void doSearchClick() {
        this.contacts = [SELECT Id, LastName, FirstName, Phone, MailingState FROM Contact ORDER BY CreatedDate DESC LIMIT 4];
    }
    public PageReference doDownloadClick() {
        return Page.CSVDownloadPage.setRedirect(false);
    }
}

取引先責任者を検索し、ダウンロードページへのPageReferenceを返します。SOQLはループの外で1回だけ発行しています。

検索結果を表示するVisualforceページ

CSVSearchPage.page

<apex:page controller="CSVDownloadController" apiVersion="67.0" title="CSVSearchPage" showHeader="true" sidebar="false" id="page">
    <apex:form id="form">
        <apex:pageBlock id="block">
            <apex:pageBlockButtons>
                <apex:commandButton value="CSV Search" action="{!doSearchClick}" reRender="form" />
                <apex:commandButton value="CSV Download" action="{!doDownloadClick}" rendered="{!contacts.size > 0}" />
            </apex:pageBlockButtons>
            <apex:pageBlockTable value="{!contacts}" var="item">
                <apex:column headerValue="{!$ObjectType.Contact.Fields.LastName.Label}">
                    <apex:outputText value="{!item.LastName}" />
                </apex:column>
                <apex:column headerValue="{!$ObjectType.Contact.Fields.FirstName.Label}">
                    <apex:outputText value="{!item.FirstName}" />
                </apex:column>
                <apex:column headerValue="{!$ObjectType.Contact.Fields.Phone.Label}">
                    <apex:outputText value="{!item.Phone}" />
                </apex:column>
                <apex:column headerValue="{!$ObjectType.Contact.Fields.MailingState.Label}">
                    <apex:outputText value="{!item.MailingState}" />
                </apex:column>
            </apex:pageBlockTable>
        </apex:pageBlock>
    </apex:form>
</apex:page>

検索ボタンで一覧を表示し、1件以上あるときだけダウンロードボタンを出します。apiVersionを67.0で明示しています。

このページのビューステートには、コントローラのcontactsがそのまま乗ります。Visualforceのビューステートには170KBの上限があるため、項目数や件数を絞らずに使い回すと、このページ単体でも上限に近づきます。

CSVを生成してダウンロードさせるVisualforceページ

CSVDownloadPage.page

<apex:page controller="CSVDownloadController" apiVersion="67.0" cache="false" contentType="text/csv#Extract_Contact.csv" language="en-US">
"LastName","FirstName","Phone","MailingState"
    <apex:repeat value="{!contacts}" var="item">
        "{!item.LastName}","{!item.FirstName}","{!item.Phone}","{!item.MailingState}"
    </apex:repeat>
</apex:page>

contentTypeを「MIMEタイプ#ファイル名」の形にすると、ダウンロード時のファイル名を指定できます。

contentTypeはどのMIMEタイプでも受け付けますが、Visualforceが実際に変換してくれるのはPDFだけです。CSVのようにそれ以外の形式では、レンダリング結果のテキストをそのままそのMIMEタイプとして流すだけなので、タグや空白が混じらないよう本文を書く必要があります。cacheのデフォルトはfalseです。検索結果は都度変わるので、ここでもfalseのままにしています。

出力はBOMなしのUTF-8です。姓名に日本語が入っていると、Excelでダブルクリックして開いたときに文字化けすることがあります。日本語を含むCSVをExcelで確実に開くには、Excel側のテキストインポートウィザードで文字コードをUTF-8に指定してください。

この記事では、Summer '26の最新であるAPI 67.0を両方のページに明示しています。

ページを公開する

  1. アプリケーションランチャーやタブに登録する

    「設定」の「タブ」からVisualforceページ用のカスタムタブを作成し、対象のページを割り当てます。

  2. アクセス権限を絞る

    プロファイルまたは権限セットで、このタブとページへのアクセスを必要な範囲だけに絞ります。

  3. 動作を確認する

    作成したタブを開き、検索とダウンロードが想定どおり動くか確認します。

タブを開くとCSVSearchPageが表示されます。「CSV Search」を押すと、作成日の新しい順で取引先責任者が4件まで一覧に出て、「CSV Download」ボタンが現れます。押すとCSVDownloadPageへ遷移し、ブラウザがExtract_Contact.csvという名前でファイルをダウンロードします。

CSVSearchPageの検索結果。姓・名・電話・都道府県(郵送先)の4列に4件が並んでいる
「CSV Search」を押した直後です。赤枠の一覧に件数が出ると「CSV Download」が使えます。
ブラウザの保存画面。ファイル名がExtract Contact、ファイルの種類がCSVになっている
保存画面です。contentTypeの「#」の後ろに書いた名前がファイル名になる点を見てください。
出力されたCSVの中身。1行目が項目名で、2行目以降に4件分のデータが並んでいる
出力されたCSVです。1行目の見出しと、各値がダブルクォートで囲まれていることを確かめてください。

ここで間違えやすい

間違い何が起きるか
ダウンロードページのcacheをtrueにする検索条件を変えても古いCSVが落ちてくることがあります
コントローラのプロパティに不要な項目を増やすビューステートが膨らみ、170KBの上限に達しやすくなります
contentTypeにCSV以外の変換を期待するVisualforceが変換してくれるのはPDFだけなので、レンダリング結果がそのままファイルの中身になります

大量件数を出力するとき

この構成は数件から数十件の「決まった条件での取り出し」向きです。Apex同期処理のヒープサイズ上限は6MB、ビューステートの上限は170KB、SOQLが1クエリで返せる行数は最大50,000行です。数千件を超える全件エクスポートには、この仕組みを広げるのではなく、データローダかBulk APIを使ってください。

確認した環境

  • 2026年9月 / Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、contentTypeの書き方、ヒープサイズとビューステートの上限を確認しています
  • コードはAPIバージョン67.0で書いています

まとめ

  • 条件を固定しない全件エクスポートなら、データローダかレポートのCSVエクスポートで足ります
  • contentTypeを「MIMEタイプ#ファイル名」の形にすると、ダウンロード時のファイル名を指定できます
  • Visualforceが変換してくれるのはPDFだけです。CSVはレンダリング結果がそのままファイルの中身になります
  • ビューステートの上限は170KB、Apex同期処理のヒープサイズ上限は6MBです。項目数や件数を絞って使ってください
  • 数千件を超える出力には向きません。データローダかBulk APIへ切り替えてください

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。