RDB と連携したページめくり (unibon)

2002年09月01日: 新規作成。
RDB から引っ張ってきたレコード数が多くて ASP で表示するひとつのページに入りきらない場合は、複数のページに分割して表示する必要があります。
そして、これらの複数のページ間を遷移する(ページめくりする)方法を考える必要があります。
これらのページめくりのやりかたについて、今回は 4 とおりのやりかたをサンプルを作って試してみました。
  1. ADO の Page を使う
  2. レコードを空読みする
  3. レコードセットをセッションに格納する
  4. 直近の ID をもとにフィルタをかける
いずれも 1 ページには 15 行を入れて表示します。行は「7桁の郵便番号」をキーにソートします。
それぞれのコードはさほど多くないので、詳細な解説は省いています。

テストデータの作成

今回は、つぎの 4 とおりのサンプルで共通に使用するサンプルデータとして、郵政事業庁が提供する「住所の郵便番号ダウンロードサービス」
http://www.post.yusei.go.jp/newnumber/down_2.htm
を使用します。データ数は「全国一括」の ken_all.lzh を展開した ken_all.csv の場合、およそ 10 万件です。多すぎる場合はひとつの県のデータでもよいです。
これは CSV ファイルなので RDB としてアクセスするために Jet(Access) の MDB ファイルに変換します。
なお CSV ファイルの読み込みに CSV ファイルの読み込み のモジュールを使っています。

WSH による CSV ファイルから MDB ファイルへの変換:

Option Explicit

' ここに CSV ファイルを読み込むモジュール(CSV ファイルの読み込み(GetCSV))を挿入してください。
' http://www.geocities.co.jp/SiliconValley/4334/unibon/sql/getcsv.html

Const adParamInput = 1
Const adInteger = 3
Const adVarChar = 200
Const ForReading = 1
Const TristateUseDefault = -2

Const connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\code\postal.mdb;Persist Security Info=False;"
Dim connection
Set connection = WScript.CreateObject("ADODB.Connection")
Call connection.Open(connectionString)

On Error Resume Next
Call connection.Execute("DROP INDEX code7index")
On Error GoTo 0
If Err.Number = 0 Then
    Call WScript.Echo("code7 のインデックスを drop しました")
End If

On Error Resume Next
Call connection.Execute("DROP TABLE postaltable")
On Error GoTo 0
If Err.Number = 0 Then
    Call WScript.Echo("テーブルを drop しました")
End If

Call connection.Execute("CREATE TABLE postaltable(pubcode varchar(5), code5 varchar(5), code7 varchar(7), " & _
"prefkana varchar(100), citykana varchar(100), areakana varchar(100), prefkanji varchar(100), citykanji varchar(100), areakanji varchar(100), " & _
"flag10 int, flag11 int, flag12 int, flag13 int, flag14 int, flag15 int)")
Call WScript.Echo("テーブルを create しました")

Call connection.Execute("DELETE FROM postaltable")

Call connection.Execute("CREATE INDEX code7index ON postaltable(code7)")
Call WScript.Echo("code7 のインデックスを create しました")

Dim fso
Set fso = WScript.CreateObject("Scripting.FileSystemObject")
Dim file
Set file = fso.GetFile("ken_all.csv")
Dim textStream
Set textStream = file.OpenAsTextStream(ForReading, TristateUseDefault)
Dim i
i = 0
Do Until textStream.AtEndOfStream
    Dim textLine
    textLine = textStream.readLine()
    Dim columnArray
    columnArray = GetCSV(textLine)
    If UBound(columnArray) = 0 Then
        Call WScript.Echo("CSV エラー")
        Exit Do
    ElseIf UBound(columnArray) = 15 Then
        If i Mod 100 = 0 Then
            Call WScript.Echo("" & i & ":" & " " & textLine)
        End If
        Dim command
        Set command = WScript.CreateObject("ADODB.Command")
        Set command.ActiveConnection = connection
        command.commandText = "INSERT INTO postaltable(pubcode, code5, code7, prefkana, citykana, areakana, prefkanji, citykanji, areakanji, flag10, flag11, flag12, flag13, flag14, flag15) " & _
        "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
        Dim param
        Set param = command.CreateParameter(, adInteger, adParamInput, , CLng(columnArray(1)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adVarChar, adParamInput, 5, CStr(columnArray(2)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adVarChar, adParamInput, 7, CStr(columnArray(3)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adVarChar, adParamInput, 100, CStr(columnArray(4)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adVarChar, adParamInput, 100, CStr(columnArray(5)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adVarChar, adParamInput, 100, CStr(columnArray(6)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adVarChar, adParamInput, 100, CStr(columnArray(7)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adVarChar, adParamInput, 100, CStr(columnArray(8)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adVarChar, adParamInput, 100, CStr(columnArray(9)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adInteger, adParamInput, , CLng(columnArray(10)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adInteger, adParamInput, , CLng(columnArray(11)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adInteger, adParamInput, , CLng(columnArray(12)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adInteger, adParamInput, , CLng(columnArray(13)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adInteger, adParamInput, , CLng(columnArray(14)))
        command.Parameters.Append param
        Set param = command.CreateParameter(, adInteger, adParamInput, , CLng(columnArray(15)))
        command.Parameters.Append param
        Call command.Execute()
        Set command.ActiveConnection = Nothing
        i = i + 1
    Else
        Call WScript.Echo("列数不一致エラー")
        Exit Do
    End If
Loop
Call WScript.Echo("" & i & " 行を挿入しました")
Call textStream.close()
Call connection.close()
これで作成した MDB ファイルは ASP のファイルと同じフォルダに置いてください(ASP の中から Server.MapPath でフォルダを指定しています)。
またこれにアクセスするためには ADO および Jet(Access) 用ドライバが必要です。接続文字列を変更すれば ODBC ブリッジでも構いませんが、Jet のドライバは必須ですのでその点に注意してください。 これらのドライバは、
http://www.microsoft.com/japan/msdn/data/download.asp
からダウンロードできます。

ADO の Page を使う

ADO には PageSize および AbsolutePage プロパティが用意されているので、これらを使用すると簡単にページめくりができます。なお、カーソルタイプは adOpenForwardOnly は使えず、adOpenKeyset 以上が必要です。

--- pagepage.asp ここから ---

<%
Option Explicit

Const adOpenForwardOnly = 0
Const adOpenKeyset = 1
Const adOpenDynamic = 2
Const adOpenStatic = 3

Const adLockReadOnly = 1
Const adLockPessimistic = 2
Const adLockOptimistic = 3
Const adLockBatchOptimistic = 4

Response.Buffer = True
%><html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=Shift_JIS">
<title>ページめくり(ADO の Page を使用)</title>
</head>
<body>
<%
Dim pathInfo
pathInfo = Request.ServerVariables("SCRIPT_NAME")
Dim p
p = InStrRev(pathInfo, "/")
Dim thisPage
thisPage = Mid(pathInfo, p + 1)
Dim index
index = Request.QueryString("index")
%><a href=<%=thisPage%>?index=<%=(index - 1)%>>前へ(小さい方へ)</a> <a href=<%=thisPage%>?index=<%=(index + 1)%>>後へ(大きい方へ)</a><br>
<table border=1>
<tr><th>公共コード</th><th>5桁</th><th>7桁</th><th>都道府県</th><th>市町村</th><th>区域</th></tr>
<%
Dim dataSource
dataSource = Server.MapPath("postal.mdb")
Dim connectionString
connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & dataSource & ";Mode=Read;Persist Security Info=False;"
Dim connection
Set connection = Server.CreateObject("ADODB.Connection")
Call connection.Open(connectionString)
Dim rs
Set rs = Server.CreateObject("ADODB.Recordset")
Set rs.ActiveConnection = connection
rs.CursorType = adOpenKeyset
rs.LockType = adLockReadOnly
rs.PageSize = 15
Call rs.Open("SELECT pubcode, code5, code7, prefkana, citykana, areakana, prefkanji, citykanji, areakanji, " & _
"flag10, flag11, flag12, flag13, flag14, flag15 FROM postaltable ORDER BY code7")
rs.AbsolutePage = index + 1
Dim i
For i = 0 To rs.PageSize - 1
    If rs.EOF Then
        Exit For
    End If
    Call Response.Write("<tr>")
    Call Response.Write("<td>" & rs.Fields("pubcode").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("code5").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("code7").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("prefkanji").Value & "(" & rs.Fields("prefkana").Value & ")" & "</td>")
    Call Response.Write("<td>" & rs.Fields("citykanji").Value & "(" & rs.Fields("citykana").Value & ")" & "</td>")
    Call Response.Write("<td>" & rs.Fields("areakanji").Value & "(" & rs.Fields("areakana").Value & ")" & "</td>")
    Call Response.Write("</tr>" & vbNewLine)
    Call rs.MoveNext()
Next
Call rs.Close()
Call connection.Close()
%></table>
</body>
</html>
--- pagepage.asp ここまで ---

実際にこのサンプルどおり動いている例は こちら です(新しいウィンドウが開きます。なお都合によりサーバが動いていない時もあります)。

レコードを空読みする

ASP のページにアクセスされるごとに RDB の先頭行から目的とする行に到達するまで空読みします。
雰囲気としては ADO の PageSize/AbsolutePage と同じことを自前でやるような感じです。
カーソルタイプはなんでも良いのですが、adOpenForwardOnly よりも adOpenKeyset などのほうが速かったです。

--- pageskip.asp ここから ---

<%
Option Explicit

Const adOpenForwardOnly = 0
Const adOpenKeyset = 1
Const adOpenDynamic = 2
Const adOpenStatic = 3

Const adLockReadOnly = 1
Const adLockPessimistic = 2
Const adLockOptimistic = 3
Const adLockBatchOptimistic = 4

Response.Buffer = True
%><html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=Shift_JIS">
<title>ページめくり(空読み)</title>
</head>
<body>
<%
Dim pathInfo
pathInfo = Request.ServerVariables("SCRIPT_NAME")
Dim p
p = InStrRev(pathInfo, "/")
Dim thisPage
thisPage = Mid(pathInfo, p + 1)
Dim index
index = Request.QueryString("index")
%><a href=<%=thisPage%>?index=<%=(index - 1)%>>前へ(小さい方へ)</a> <a href=<%=thisPage%>?index=<%=(index + 1)%>>後へ(大きい方へ)</a><br>
<table border=1>
<tr><th>公共コード</th><th>5桁</th><th>7桁</th><th>都道府県</th><th>市町村</th><th>区域</th></tr>
<%
Dim dataSource
dataSource = Server.MapPath("postal.mdb")
Dim connectionString
connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & dataSource & ";Mode=Read;Persist Security Info=False;"
Dim connection
Set connection = Server.CreateObject("ADODB.Connection")
Call connection.Open(connectionString)
Dim rs
Set rs = Server.CreateObject("ADODB.Recordset")
Set rs.ActiveConnection = connection
rs.CursorType = adOpenKeyset ' adOpenKeyset のほうが adOpenForwardOnly よりも速い。
rs.LockType = adLockReadOnly
Const pageSize = 15
Call rs.Open("SELECT pubcode, code5, code7, prefkana, citykana, areakana, prefkanji, citykanji, areakanji, " & _
"flag10, flag11, flag12, flag13, flag14, flag15 FROM postaltable ORDER BY code7")
Dim i
For i = 0 To index * pageSize - 1
    If rs.EOF Then
        Exit For
    End If
    Call rs.MoveNext()
Next
For i = 0 To pageSize - 1
    If rs.EOF Then
        Exit For
    End If
    Call Response.Write("<tr>")
    Call Response.Write("<td>" & rs.Fields("pubcode").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("code5").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("code7").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("prefkanji").Value & "(" & rs.Fields("prefkana").Value & ")" & "</td>")
    Call Response.Write("<td>" & rs.Fields("citykanji").Value & "(" & rs.Fields("citykana").Value & ")" & "</td>")
    Call Response.Write("<td>" & rs.Fields("areakanji").Value & "(" & rs.Fields("areakana").Value & ")" & "</td>")
    Call Response.Write("</tr>" & vbNewLine)
    Call rs.MoveNext()
Next
Call rs.Close()
Call connection.Close()
%></table>
</body>
</html>
--- pageskip.asp ここまで ---

実際にこのサンプルどおり動いている例は こちら です(新しいウィンドウが開きます。なお都合によりサーバが動いていない時もあります)。

レコードセットをセッションに格納する

これはページめくりというよりは、レコードセットを持続させるやりかたのサンプルです。
上記 2 つのサンプルは ASP のページの中でだけレコードセットが持続していましたが、このサンプルでは ASP の Session にレコードセットを格納しているため、セッションのタイムアウトまでの間(Session.Timeout)は、レコードセットをすぐに再利用できます。
なお、ページめくり自体は ADO の PageSize/AbsolutePage を使うやりかたと同じです。

--- pagesession.asp ここから ---

<%
Option Explicit

Const adOpenForwardOnly = 0
Const adOpenKeyset = 1
Const adOpenDynamic = 2
Const adOpenStatic = 3

Const adLockReadOnly = 1
Const adLockPessimistic = 2
Const adLockOptimistic = 3
Const adLockBatchOptimistic = 4

Session.Timeout = 3
Response.Buffer = True
%><html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=Shift_JIS">
<title>ページめくり(セッション使用)</title>
</head>
<body>
<%
Dim pathInfo
pathInfo = Request.ServerVariables("SCRIPT_NAME")
Dim p
p = InStrRev(pathInfo, "/")
Dim thisPage
thisPage = Mid(pathInfo, p + 1)
Dim index
index = Request.QueryString("index")
%><a href=<%=thisPage%>?index=<%=(index - 1)%>>前へ(小さい方へ)</a> <a href=<%=thisPage%>?index=<%=(index + 1)%>>後へ(大きい方へ)</a><br>
<table border=1>
<tr><th>公共コード</th><th>5桁</th><th>7桁</th><th>都道府県</th><th>市町村</th><th>区域</th></tr>
<%
Dim rs
If IsEmpty(Session("rs")) Then
    Dim dataSource
    dataSource = Server.MapPath("postal.mdb")
    Dim connectionString
    connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & dataSource & ";Mode=Read;Persist Security Info=False;"
    Dim connection
    Set connection = Server.CreateObject("ADODB.Connection")
    Call connection.Open(connectionString)
    Set rs = Server.CreateObject("ADODB.Recordset")
    Set rs.ActiveConnection = connection
    rs.CursorType = adOpenKeyset
    rs.LockType = adLockReadOnly
    rs.PageSize = 15
    Call rs.Open("SELECT pubcode, code5, code7, prefkana, citykana, areakana, prefkanji, citykanji, areakanji, " & _
    "flag10, flag11, flag12, flag13, flag14, flag15 FROM postaltable ORDER BY code7")
    Set Session("rs") = rs

    ' ほんとうは global.asa の Session_OnEnd にて rs と rs.ActiveConnection の close も必要だが、今回のサンプルでは省略する。
Else
    Set rs = Session("rs")
End If

rs.AbsolutePage = index + 1

Dim i
For i = 0 To rs.PageSize - 1
    If rs.EOF Then
        Exit For
    End If
    Call Response.Write("<tr>")
    Call Response.Write("<td>" & rs.Fields("pubcode").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("code5").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("code7").Value & "</td>")
    Call Response.Write("<td>" & rs.Fields("prefkanji").Value & "(" & rs.Fields("prefkana").Value & ")" & "</td>")
    Call Response.Write("<td>" & rs.Fields("citykanji").Value & "(" & rs.Fields("citykana").Value & ")" & "</td>")
    Call Response.Write("<td>" & rs.Fields("areakanji").Value & "(" & rs.Fields("areakana").Value & ")" & "</td>")
    Call Response.Write("</tr>" & vbNewLine)
    Call rs.MoveNext()
Next
%></table>
</body>
</html>
--- pagesession.asp ここまで ---

実際にこのサンプルどおり動いている例は こちら です(新しいウィンドウが開きます。なお都合によりサーバが動いていない時もあります)。

直近の ID をもとにフィルタをかける

上記 3 つのサンプルは、ページの指定は先頭から何ページ目かという指定であったため、ページの指定は行の並びを知っている RDB 任せになりました。
このサンプルではページの指定は、ページ番号ではなくページに含まれる ID を指定してそれをページめくりに利用します。こうすると、SQL 文の WHERE などによるフィルタ機能が使えるので、よりダイレクトな行の指定になるため、場合によっては速く動かせます。
ただし、ID を指定する都合上、ID の重複を許すようなケースでは、ページめくりが厳密ではなくなります。厳密にやろうとすると重複に対応させるための処理の追加が必要でしょう。
なお、Jet(MDB) でやると ORDER BY 〜 ASC に比べて ORDER BY 〜 DESC の処理が遅く、そのため「次ページ(next)」に比べて「前ページ(prev)」が格段に遅くなってしまいました。
あと、小さな欠点としては次回のページめくり用の ID を得るための処理が必要なので、ページめくり用のハイパリンクを書ける場所が、後ろ側になってしまっています(出力をバッファリングすれば簡単に対処はできますが)。

--- pagefilter.asp ここから ---

<%
Option Explicit

Const adOpenForwardOnly = 0
Const adOpenKeyset = 1
Const adOpenDynamic = 2
Const adOpenStatic = 3

Const adLockReadOnly = 1
Const adLockPessimistic = 2
Const adLockOptimistic = 3
Const adLockBatchOptimistic = 4

Response.Buffer = True
%><html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=Shift_JIS">
<title>ページめくり(フィルタ使用)</title>
</head>
<body>
<%
Dim pathInfo
pathInfo = Request.ServerVariables("SCRIPT_NAME")
Dim p
p = InStrRev(pathInfo, "/")
Dim thisPage
thisPage = Mid(pathInfo, p + 1)
Dim index
index = Request.QueryString("index")
%><table border=1>
<tr><th>公共コード</th><th>5桁</th><th>7桁</th><th>都道府県</th><th>市町村</th><th>区域</th></tr>
<%
Dim dataSource
dataSource = Server.MapPath("postal.mdb")
Dim connectionString
connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & dataSource & ";Mode=Read;Persist Security Info=False;"
Dim connection
Set connection = Server.CreateObject("ADODB.Connection")
Call connection.Open(connectionString)
Dim rs
Set rs = Server.CreateObject("ADODB.Recordset")
Set rs.ActiveConnection = connection
rs.CursorType = adOpenKeyset ' adOpenKeyset は adOpenForwardOnly よりも速い。
rs.LockType = adLockReadOnly
If IsEmpty(index) Then
    Call rs.Open("SELECT pubcode, code5, code7, prefkana, citykana, areakana, prefkanji, citykanji, areakanji, " & _
    "flag10, flag11, flag12, flag13, flag14, flag15 FROM postaltable " & _
    "ORDER BY code7 ASC")
ElseIf Request.QueryString("mode") = "next" Then
    Call rs.Open("SELECT pubcode, code5, code7, prefkana, citykana, areakana, prefkanji, citykanji, areakanji, " & _
    "flag10, flag11, flag12, flag13, flag14, flag15 FROM postaltable " & _
    "WHERE code7 > " & "'" & index & "'" & " " & _
    "ORDER BY code7 ASC")
ElseIf  Request.QueryString("mode") = "prev" Then
    Call rs.Open("SELECT pubcode, code5, code7, prefkana, citykana, areakana, prefkanji, citykanji, areakanji, " & _
    "flag10, flag11, flag12, flag13, flag14, flag15 FROM postaltable " & _
    "WHERE code7 < " & "'" & index & "'" & " " & _
    "ORDER BY code7 DESC")
Else
End If

Dim rows
rows = rs.GetRows(15)
Call rs.Close()
Call connection.Close()

Dim prevIndex
Dim nextIndex
Dim i
For i = 0 To UBound(rows, 2)
    Dim j
    If IsEmpty(index) Then
        j = i
    ElseIf Request.QueryString("mode") = "next" Then
        j = i
    ElseIf  Request.QueryString("mode") = "prev" Then
        j = UBound(rows, 2) - i
    Else
    End If
    If i = 0 Then
        prevIndex = rows(2, j)
    End If
    If i = UBound(rows, 2) Then
        nextIndex = rows(2, j)
    End If
    Call Response.Write("<tr>")
    Call Response.Write("<td>" & rows(0, j) & "</td>")
    Call Response.Write("<td>" & rows(1, j) & "</td>")
    Call Response.Write("<td>" & rows(2, j) & "</td>")
    Call Response.Write("<td>" & rows(6, j) & "(" & rows(3, j) & ")" & "</td>")
    Call Response.Write("<td>" & rows(7, j) & "(" & rows(4, j) & ")" & "</td>")
    Call Response.Write("<td>" & rows(8, j) & "(" & rows(5, j) & ")" & "</td>")
    Call Response.Write("</tr>" & vbNewLine)
Next
%></table>
<a href=<%=thisPage%>?mode=prev&index=<%=prevIndex%>>前へ(小さい方へ)</a> <a href=<%=thisPage%>?mode=next&index=<%=nextIndex%>>後へ(大きい方へ)</a><br>
</body>
</html>
--- pagefilter.asp ここまで ---

実際にこのサンプルどおり動いている例は こちら です(新しいウィンドウが開きます。なお都合によりサーバが動いていない時もあります)。


ASP の目次
ホーム
(このページ自身の絶対的な URL)