none
Gridviewの行数が一定数を超える場合にレンダリングを止めたい RRS feed

  • 質問

  • SqlDataSourceを介してGridViewにデータバインドしている状況で、データ行数が1,000行を超える場合に、処理を止めてメッセージを表示したいと考えています。
    環境は、Visual Studio Community 2019で、.NET Framework4.8でWEBフォームを作成)

    「データが1000行を超えています。絞り込んでください」とのLabelMsgを用意し、以下のようなコードを書きました。

    Protected Sub GridView1_DataBound(sender As Object, e As EventArgs) Handles GridView1.DataBound
        If GridView1.Rows.Count > 1000 Then
            LabelMsg.Visible = True
            GridView1.Visible = False
        Else
            LabelMsg.Visible = False
            GridView1.Visible = True
        End If
    End Sub
    しかし、これでは、ユーザーにGridViewを見せていないだけで、レンダリングは行われている(大きな負荷はかかっている)状態と思われます。負荷をかけずに処理を止めるより良い方法はありますでしょうか?
    GridViewにバインドする前に、SqlDataSourceの行数を得る方法などありましたら、ご教授お願いいたします。

    2020年7月6日 7:44

回答

  • ページングしてはどうですか?

    ObjectDataSoutce と表示するレコードのみ DB から取得するメソッドを組み合わせれば、ページに表示するレコードだけを DB から取得しながらページングするという実装ができます。詳しくは以下の記事を見てください。

    ObjectDataSource でページング
    http://surferonwww.info/BlogEngine/post/2010/08/26/Paging-with-ObjectDataSource.aspx

    他に、ストアドプロシージャを作ってそれとカスタムページャーを組み合わせるという手もあります。以下の記事を見てください。

    カスタムページャー
    http://surferonwww.info/BlogEngine/post/2010/08/25/Custom-pager.aspx

    注: SqlDataSource を使ってもページングできますが、デフォルトでは全行取得してから指定されたページのみ表示するという実装になるため、ページングの都度全レコードを取得することになってしまい、目的は果たせません。
    2020年7月6日 8:28
  • その要件であれば、私なら以下のようにすると思います。

    ストアドプロシージャで以下のロジックを組みます。
    1000件より多く検索された場合、selectの結果を空で返す。空で返すにはwhere 1=0 とでも書けばOK。
    そして、outputパラメータに1000件超えたフラグを立てる。

    SqlDataSourceのSelectedイベントでこのフラグを見て、フラグが立っていれば、1000件を超えましたのメッセージを表示する。Selectedイベントでフラグを見るには、
    e.Command.Parameters("出力パラメータ名").Value
    で、OK。

    上記だと、データベースからWebサーバーに余計なデータは一切流れません。基本的にデータベース側で無駄なデータを抽出してWebサーバーに流さないのが最善です。

    #ストアドプロシージャの組み方がわからなければ聞いて下さい。


    ★良い回答には質問者は回答済みマークを、閲覧者は投票を!


    2020年7月7日 4:58
    モデレータ
  • あ~、折角返す出力パラメータは検索した件数の方がいいですね。
    簡単なサンプルを以下に示しますが、基本的にはこれで大丈夫です。

    CREATE PROCEDURE dbo.データ抽出
    	@パラメータ1		tinyint = 0,
    	@検索件数		int output						
    AS
    BEGIN
    	SET NOCOUNT ON;
    
    	select @検索件数 = count(*) from dbo.Table1 where column1 = @パラメータ1;
    
    	if @検索件数 > 1000
    		select * from dbo.Table1 where 1 = 0 --1000件超えたら空で返す
    	else
    		select * from dbo.Table1 where column1 = @パラメータ1;
    
    END
    
    GO


    ★良い回答には質問者は回答済みマークを、閲覧者は投票を!



    2020年7月7日 5:15
    モデレータ

すべての返信

  • ページングしてはどうですか?

    ObjectDataSoutce と表示するレコードのみ DB から取得するメソッドを組み合わせれば、ページに表示するレコードだけを DB から取得しながらページングするという実装ができます。詳しくは以下の記事を見てください。

    ObjectDataSource でページング
    http://surferonwww.info/BlogEngine/post/2010/08/26/Paging-with-ObjectDataSource.aspx

    他に、ストアドプロシージャを作ってそれとカスタムページャーを組み合わせるという手もあります。以下の記事を見てください。

    カスタムページャー
    http://surferonwww.info/BlogEngine/post/2010/08/25/Custom-pager.aspx

    注: SqlDataSource を使ってもページングできますが、デフォルトでは全行取得してから指定されたページのみ表示するという実装になるため、ページングの都度全レコードを取得することになってしまい、目的は果たせません。
    2020年7月6日 8:28
  • SurferOnWww様

    いつもアドバイスいただき、ありがとうございます。
    手軽にSqlDataSourceでと思ったのですが、SqlDatasourceを使う限りは、やはり、どうしても全レコードを取得して処理してしまう形ということですね・・・。

    2020年7月6日 8:40
  • > 手軽にSqlDataSourceでと思ったのですが、

    どうしても SqlDataSource ということであれば、クエリを工夫して、例えば SELECT TOP 1000 ... として 1,000 レコード以上は取得しないようにするとか、SELECT COUNT(*) ... としてレコード数をあらかじめ取得してから対応するとかぐらいでしょうか。

    ただ、1,000 行以上あることが分かって「データが1000行を超えています。絞り込んでください」と表示したとして、絞り込む手段をどのようにユーザーに提供するかが問題だと思うのですが・・・
    2020年7月6日 9:25
  • さらなるアドバイス、ありがとうございます。

    Select TOP 1000 は、実際は1000行以上のデータがあるのに、1000行分で全てと誤解されることになり、今回の場面では困難でした。

    > SELECT COUNT(*) ... としてレコード数をあらかじめ取得してから対応するとかぐらいでしょうか。

    この方法を取る場合は、GridView1.Prerenderに
    1. Select count(*) でレコード数を取得する
    2. レコード数が1000件超なら、Response.end という感じでしょうか。
    試してみたいと思います。

    > 1,000 行以上あることが分かって「データが1000行を超えています。絞り込んでください」と表示したとして、絞り込む手段をどのようにユーザーに提供するかが問題だと思うのですが・・・

    はい、これについては、TextBoxなど多数を配置し、その入力内容により様々な条件で検索するページになっております。
    データベースとしては数十万レコードありますが、検索条件により1,000件以内に絞り込んだ場合に、GridViewに表示されるようにしたい状況でした。
    仮にTextBox全てを空欄で検索ボタン押されると、その数十万レコードを全て読み込んでしまうので、今回の相談に至ったものでした。

    ※ これは余談ですが、自社内の限られた者が使うシステムを作っており、負荷といってもイントラネット内で完結しているので、数十万レコード表示させても大きな問題ではないのですが、それでもGridViewの使い方として行儀が悪いと思いまして・・・

    2020年7月6日 9:52
  • 最初の質問の、

    > If GridView1.Rows.Count > 1000 Then

    とか、

    > 2. レコード数が1000件超なら、Response.end という感じでしょうか。

    とか。

    > 数十万レコード表示させても大きな問題ではないのですが

    とか言っているところから想像するに、質問者さんは初心者としか思えません。であれば、見栄を張らずに、素直に人の意見を聞いてページングしてはどうですか?


    ページング案ができないのであれば、その理由を教えてください。


    • 編集済み SurferOnWww 2020年7月6日 21:14 追記
    2020年7月6日 11:30
  • 初心者の前段階、エンジニアですらないので、レベルはご容赦いただくとして・・・
    (いち経理社員が本業のかたわら、空いた時間で自部署で使うシステムを作ろうとしている状況)

    > ページング案ができないのであれば、その理由を教えてください。

    今回の目的は「検索条件によって1,000件以下にデータを絞らせる」ことです。「大量のデータを見せるために負荷を軽減すること」ではない状況です。ページング処理をするなら、SqlDataSourceではなくObjectDataSourceを使うことは、私にも理解できました。
    しかし今回は、1,000件超ヒットしたら、そもそも結果を表示させたくないのです。表示させない目的ですので、If GridView1.Rows.Count > 1000 Then・・・でも、目的は満たされています。
    ただし、これでは余計な負荷がかかっていることも理解していますので、GridViewにデータバインドする前にデータ件数を確認し、処理を止める方法がありますでしょうか、というのが当初の質問になります。

    今回アドバイスいただいて、CustomValidatorを使うのがいいのかな、と思いついております。Select Count (*)で件数を取得し、1,000件超であれば検証失敗にする。これが最適解かはわかりませんが、少しもがいてみます。

    2020年7月7日 0:48
  • > しかし今回は、1,000件超ヒットしたら、そもそも結果を表示させたくないのです。表示させない目的ですので

    業務上必要な情報を提供しているのですよね。だとすると、どうしても業務上 1,000 件以上の情報が必要という人に対しで「表示させたくない」という話が通じるのでしょうか?

    業務遂行に支障が出ている人から「アプリを作った責任者出てこい!」とか言われたりしないですか? そういうクレームは問答無用で却下できる立場なんですかね。

    であれば、クエリを SELECT TOP 1000 ... にして、とにかく最大 1,000 レコードまでは表示するようにし、1,000 レコード取得できてしまった場合は「今の検索条件では表示限度の 1,000 件を超えている可能性があります。 条件を絞って再度検索してください」というメッセージを表示するようにしたらどうですか?

    その方が「問答無用で 1,000 件を超えたら表示しない」よりは、はるかにユーザーに親切ですし、サーバー・通信路・クライアントのリソースの無駄遣いを抑えるという目的も果たせると思ますが、違いますかね?

    SELECT COUNT(*) ... 案は「問答無用で 1,000 件を超えたら表示しない」と同じで、「アプリを作った責任者出てこい!」とか言われそうなので自分的には NG です。
    2020年7月7日 3:33
  • 1,000件以上は問答無用で表示させないことに対するご心配いただいた点は、問題ございません。なぜ問題ないのかは業務に係わることなので、ここでは割愛させていただきます。

    ひとまず1,000件を表示させて、メッセージを表示する。これは、なるほどそういう方法もあるかと感じました。今回の場面では、やはり業務上それができないのですが、他の場面では考慮していきたいと思います。

    いろいろアドバイスいただき、ありがとうございました。

    2020年7月7日 4:25
  • その要件であれば、私なら以下のようにすると思います。

    ストアドプロシージャで以下のロジックを組みます。
    1000件より多く検索された場合、selectの結果を空で返す。空で返すにはwhere 1=0 とでも書けばOK。
    そして、outputパラメータに1000件超えたフラグを立てる。

    SqlDataSourceのSelectedイベントでこのフラグを見て、フラグが立っていれば、1000件を超えましたのメッセージを表示する。Selectedイベントでフラグを見るには、
    e.Command.Parameters("出力パラメータ名").Value
    で、OK。

    上記だと、データベースからWebサーバーに余計なデータは一切流れません。基本的にデータベース側で無駄なデータを抽出してWebサーバーに流さないのが最善です。

    #ストアドプロシージャの組み方がわからなければ聞いて下さい。


    ★良い回答には質問者は回答済みマークを、閲覧者は投票を!


    2020年7月7日 4:58
    モデレータ
  • あ~、折角返す出力パラメータは検索した件数の方がいいですね。
    簡単なサンプルを以下に示しますが、基本的にはこれで大丈夫です。

    CREATE PROCEDURE dbo.データ抽出
    	@パラメータ1		tinyint = 0,
    	@検索件数		int output						
    AS
    BEGIN
    	SET NOCOUNT ON;
    
    	select @検索件数 = count(*) from dbo.Table1 where column1 = @パラメータ1;
    
    	if @検索件数 > 1000
    		select * from dbo.Table1 where 1 = 0 --1000件超えたら空で返す
    	else
    		select * from dbo.Table1 where column1 = @パラメータ1;
    
    END
    
    GO


    ★良い回答には質問者は回答済みマークを、閲覧者は投票を!



    2020年7月7日 5:15
    モデレータ
  • trapemiya様

    アドバイスいただき、ありがとうございます。ストアドプロシージャは、現時点で全く知識ないのですが、これから勉強していきたい内容でした。これを機にチャレンジしてみょうと思います。

    サンプルまで作成いただき、大変恐縮です。ありがとうございました。

    2020年7月7日 5:41
  • ごめんなさい。ストアドプロシージャを使わなくても、SQLだけで行けそうですね・・・
    しかもファントムリードも無いし・・・

    select *
    from dbo.Table1 t
    where t.column1 = @パラメータ1 and
    		(select count(*) from dbo.Table1 t1 where t1.column1 = @パラメータ1) <= 1000;

    #追記
    上記だと検索件数がわからないので(1000件を超えたのか0件なのかわからない)、やっぱりダメですね。
    上記のストアドプロシージャで厳密にはファントムリードなどを防ぐために、セッションレベルで分離レベル設定かなぁ。どこまで厳密にやるかですが、そこまでの厳密さを求めないのであれば、ストアドプロシージャの最後のselect文で、保険でtop(1000)を付けちゃってもいいかも。というか、これで十分ですね。
    select top(1000) * from dbo.Table1 where column1 = @パラメータ1;


    ★良い回答には質問者は回答済みマークを、閲覧者は投票を!




    2020年7月7日 7:06
    モデレータ
  • trapemiya さん>

    ストアドを使うより、SqlDataSource.Selecting イベントで、イベントが発生した SqlDataSource の SELECT クエリと同じ抽出条件で SELECT COUNT(*) ... クエリを投げて、1,000 を超えていたらその旨メッセージを出してキャンセルするという方法が良いのではないですか?
    2020年7月7日 9:53
  • SqlDataSource.Selecting イベントハンドラでSQLCommandでSQLを実行してcount(*)を取るということですよね?それでもいいと思いますよ。
    どちらが良いかではなく、それぞれ利点や欠点がありますし、それぞれの事情やポリシーがありますので、それぞれが状況に応じて決めるのが現実です。いろいろな方法を知っておくのは大事なことであり、それだけ解決への選択候補が広がります。


    ★良い回答には質問者は回答済みマークを、閲覧者は投票を!

    2020年7月8日 1:33
    モデレータ
  • 上の terapemiya さんへのレスで、

    > ストアドを使うより、SqlDataSource.Selecting イベントで、イベントが発生した SqlDataSource の SELECT クエリと同じ抽出条件で SELECT COUNT(*) ... クエリを投げて、1,000 を超えていたらその旨メッセージを出してキャンセルするという方法が良いのではないですか?

    と書きましたが、それを以下に書いておきます。

    DB は Microsoft が提供する SQL Server サンプルデータベース Northwind の Orders テーブルを使います。

    やり方は上に書いた通り。Orders テーブルの CustomerID を LIKE 句を使ってあいまい検索し、上限は 20 レコードとします。

    .aspx.cs

    using System.Linq;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Data;
    
    namespace WebApplication1
    {
        public partial class WebForm2 : System.Web.UI.Page
        {
            protected void Page_Load(object sender, EventArgs e)
            {
                  
            }
    
            protected void SqlDataSource1_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
            {
                var view = (DataView)SqlDataSource2.Select(DataSourceSelectArguments.Empty);
                if (view != null)
                {
                    int numberOfRecords = (int)view[0][0];
                    if (numberOfRecords > 20)
                    {
                        Label1.Text = "表示限度の 20 件を超えています。 条件を絞って再度検索してください";
                        e.Cancel = true;
                    }
                    else
                    {
                        Label1.Text = $"ヒットしたレコード数 {numberOfRecords} 件";
                    }                
                }            
            }
        }
    }

    .aspx (マスターページ利用)

    <%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" 
        AutoEventWireup="true" CodeBehind="WebForm2.aspx.cs" 
        Inherits="WebApplication1.WebForm2" %>
    
    <asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="server">
    </asp:Content>
    
    <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
        <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
        <asp:Button ID="Button1" runat="server" Text="Button" />
    
        <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
            ConnectionString="<%$ ConnectionStrings:NORTHWINDConnectionString %>" 
            SelectCommand="SELECT [OrderID], [CustomerID], [EmployeeID], [OrderDate], [RequiredDate] 
                           FROM [Orders] 
                           WHERE ([CustomerID] LIKE '%' + @CustomerID + '%') 
                           ORDER BY [CustomerID]" 
            OnSelecting="SqlDataSource1_Selecting">
            <SelectParameters>
                <asp:ControlParameter ControlID="TextBox1" 
                    Name="CustomerID" PropertyName="Text" Type="String" />
            </SelectParameters>
        </asp:SqlDataSource>
    
        <asp:GridView ID="GridView1" runat="server" 
            AutoGenerateColumns="False" DataKeyNames="OrderID" 
            DataSourceID="SqlDataSource1">
            <Columns>
                <asp:BoundField DataField="OrderID" HeaderText="OrderID" 
                    InsertVisible="False" ReadOnly="True" SortExpression="OrderID" />
                <asp:BoundField DataField="CustomerID" HeaderText="CustomerID"
                    SortExpression="CustomerID" />
                <asp:BoundField DataField="EmployeeID" HeaderText="EmployeeID"
                    SortExpression="EmployeeID" />
                <asp:BoundField DataField="OrderDate" HeaderText="OrderDate" 
                    SortExpression="OrderDate" />
                <asp:BoundField DataField="RequiredDate" HeaderText="RequiredDate" 
                    SortExpression="RequiredDate" />
            </Columns>
        </asp:GridView>
    
        <asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
    
        <asp:SqlDataSource ID="SqlDataSource2" runat="server" 
            ConnectionString="<%$ ConnectionStrings:NORTHWINDConnectionString %>" 
            SelectCommand="SELECT COUNT(*)
                           FROM [Orders] 
                           WHERE ([CustomerID] LIKE '%' + @CustomerID + '%')">
            <SelectParameters>
                <asp:ControlParameter ControlID="TextBox1" 
                    Name="CustomerID" PropertyName="Text" Type="String" />
            </SelectParameters>
        </asp:SqlDataSource>
    </asp:Content>

    検索条件 AL では 20 レコードを超えるので以下の結果:

    検索条件を ALF まで絞ると 6 レコードになるので以下の結果:

    SqlDataSource.Selecting イベントでレコード数をチェックするのには、GridView が使っている SqlDataSource をコピペして SelectCommand を SELECT COUNT(*) ... に書き換えるだけ済みます。ストアドを作るより良さそうだと思いますが、いかがですか?>terapemiya さん


    #SELECT COUNT(*) ... 案は、上の私のレスで書いたように、「問答無用で 1,000 件を超えたら表示しない」と同じで、「アプリを作った責任者出てこい!」とか言われそうなので自分的には NG なのですが、こういうこともできるということで・・・

    2020年7月8日 1:44
  • 一般的に、どの方法を採用するかは前にも述べた通り、人によって異なります。メリット、デメリットは人によっても、実際の開発の要件によっても変わるからです。

    工数を少なくするSurferOnWwwさんの方法は、開発難易度も下がりますし、質問者さんには適しているかもしれません。ただ、私は質問者さんのことをよく知らないので、質問者さんにとって最適な実装方法を見つけるアドバイスをすることしかできません。

    #人は慣れた方法を安易に採用しがちです。そういう意味で、SqlDataSourceをもう一つ使うという発想は無かったので、世界が広がりました。ありがとうございました。


    ★良い回答には質問者は回答済みマークを、閲覧者は投票を!

    2020年7月8日 8:21
    モデレータ