VB .NET：Oracle のバージョンに左右されない接続方法

この記事は [Visual Basic Advent Calendar 2018](https://qiita.com/advent-calendar/2018/vb) の 23日目の記事です。

# DB のバージョンに左右される現場の例

エンプラ系で VB.NET + Oracle を採用している場合、妙な環境で開発されている方も多いのではないかと思います。
例えば、

 * 開発環境と本番環境で、Oracleのバージョンが違う（bit数も違ってたり）
 * 開発者全員が、同一の環境を用意していない
 
 など。

本番直前のタイミングで
「あ、開発は11でやってもらったけど、本番環境はバージョン12で動かすからねー。」とか言われて、正気を保てなくなったエンジニアも何人か居るんじゃないかと思います。
（開発環境は過去のものを使いまわして、本番環境はアップデートしているというパターン。当然、メンバーには周知されていない。）

そこまでダメな事例を出さずとも、バージョンが上がるたびに ODBCのバージョンを変えたり、
参照設定を変えたりするのが面倒くさいと感じる方も多いのではないかと思います。
ODBC固有の機能を使用しているわけでもなく、ORマッパーも使用しておらず、ゴリゴリと生SQLを書いているだけなら、別に参照設定して Oracle専用のドライバを指定せずとも、DBへの接続が可能です。

具体的には、DbProviderFactories という、抽象化されたクラスを使用します。


## コーディング例：プロバイダー側

```vb
Imports System.Data.Common
Imports System.Data

Public Class DataAccess
    Implements IDisposable

    Private conn As DbConnection = Nothing
    Private factory As DbProviderFactory = Nothing
    Private tran As DbTransaction = Nothing
    Private _hasAccessError As Boolean = False
    Private _hasSQLError As Boolean = False
    Private _errorMessage As String = String.Empty
    Private _sqlContents As String = String.Empty
    Private isDisposed As Boolean = False

    Public Sub New()
        Dim dcsb As DbConnectionStringBuilder

        Try
            _hasAccessError = False

            factory = DbProviderFactories.GetFactory("Oracle.DataAccess.Client")
            dcsb = factory.CreateConnectionStringBuilder
            dcsb("Data Source") = ConfigurationManager.AppSettings("dataSource")
            dcsb("User ID") = ConfigurationManager.AppSettings("userId")
            dcsb("Password") = ConfigurationManager.AppSettings("pass")

            conn = factory.CreateConnection()
            conn.ConnectionString = dcsb.ConnectionString

            conn.Open()
            tran = conn.BeginTransaction()
            _hasSQLError = False
            _errorMessage = String.Empty

        Catch ex As Exception
            _hasAccessError = True
            _errorMessage = ex.Message

        End Try

    End Sub

    Public Function ExcuteQuery(ByVal SQLContents As String) As DataSet

        Dim cmd As DbCommand
        Dim da As DbDataAdapter
        Dim ds As DataSet = New DataSet

        Try
            _hasSQLError = False
            _sqlContents = SQLContents
            cmd = conn.CreateCommand
            cmd.CommandText = SQLContents

            da = factory.CreateDataAdapter()
            da.SelectCommand = cmd
            da.Fill(ds)

        Catch ex As Exception
            _hasSQLError = True
            _errorMessage = ex.Message

        End Try

        Return ds

    End Function

    Public Function ExecuteNonQuery(ByVal SQLContents As String, Optional ByVal isCommit As Boolean = True) As Integer

        Dim cmd As DbCommand
        Dim affectedRows As Integer = 0

        Try
            _hasSQLError = False
            _sqlContents = SQLContents
            cmd = conn.CreateCommand
            cmd.CommandType = CommandType.Text
            cmd.CommandText = SQLContents

            affectedRows = cmd.ExecuteNonQuery()
            If isCommit Then
                Commit()
            End If

        Catch ex As Exception
            _hasSQLError = True
            _errorMessage = ex.Message
            Rollback()

        End Try

        Return affectedRows

    End Function

    Public Sub Commit()
        Try
            tran.Commit()
            tran = conn.BeginTransaction()
        Catch ex As Exception
        End Try
    End Sub

    Public Sub Rollback()
        Try
            tran.Rollback()
            tran = conn.BeginTransaction()
        Catch ex As Exception
        End Try
    End Sub

End Class
```

Dispose や Finalize 、他色々と省略しています。

## コーディング例：プロバイダー側の設定
web.config に設定内容を記述します。

```
<configuration>
	<appSettings>
    <add key="dataSource" value="YOUR_SCHEMA" />
    <add key="userId" value="SYSTEM" />
    <add key="pass" value="SYSTEM" />
  </appSettings>
</configuration>
```

## コーディング例：使用側

```vb
  Using dbAccess As New DataAccess

      ds = dbAccess.ExcuteQuery("SELECT * FROM users")

      affectedRows = dbAccess.ExecuteNonQuery("UPDATE projects SET status=1")

  End Using
```
端折りすぎてますが、雰囲気だけ伝わればいいかなぐらいで書いてます。
ちゃんと書くと長いんで。

## 解説

プロバイダー側の以下のコードにて、DB接続のオブジェクトを生成しています。

```vb
factory = DbProviderFactories.GetFactory("Oracle.DataAccess.Client")
```

端末に OracleClientがインストールされていれば、勝手にデフォルトで使用するバージョンを拾って、
後はよしなにやってくれます。
（10g がインストールされている端末では 10gの Clientを見つけ出し、11g がインストールされている端末では、11g の Clientを探し出す。）
なので、Oracle Client さえインストールされていれば、コネクション設定をこねくり回す必要もなくなっています。

Oracle Server をインストールすると、Oracle Clientも自動でインストールされるので、どちらで動かす場合も問題ないかと思われます。

また、引数に "Oracle.DataAccess.Client" を渡して Oracle接続用のオブジェクトを生成していますが、
恐らく、SQLServer や MySQL でも同じ方法で行けるんじゃないかと思います。

余談ですが、上記の「Factory」は、多分、デザインパターンにおける Facory Method の事。