DAOパターンの実装サンプル（VB.NET）

## ことわり

期待してこの記事を開いてくれた方のために予め断っておくと筆者は`VB.Net`なんか**大嫌い**です。[^1]
目に余るほど悲惨なクオリティのコードが世の中に溢れかえっているからです。
こんな開発言語はこの世界から一日でも早く消えてなくなれと願っています。
その願いは叶いそうもないので悲惨なコードがちょっとでもマシになればいいなという気持ちでこの記事を書いています。

コード全文を掲載しているので若干長い記事になっているのはご容赦ください。[^2]
あくまでパターンを見せたいだけであって、トランザクション処理はこのサンプルのやり方ではマズいかもしれないので鵜呑みにしないようにしてください。

## 想定読者

- SQL文の発行とデータソースへの問い合わせとビジネスロジックが同じプロシージャに書いてある上にデータオブジェクトという考え方すらないレガシィでカオティックなプログラムを見て眩暈を起こしている人[^me]
- データソースからのデータ取得とビジネスロジックを分離しない人
    - …のうち、ちょっとは分離すべきだと思ってる人
- オブジェクト指向とか言われてもよく分かんないしコワい！な人
    - …のうち、分かりたい気持ちはある人

## 想定してない読者

- 「EntityFrameworkとか当たり前に使ってにバリバリ書いてるぜ」という人達にはこの記事の内容は物足りないかと思います。
- 「DAOパターンなんか時代遅れだよ、もっといい〇〇パターンがあるよ」と考える人には時代遅れな記事です。

## サンプルプログラム

ソリューションの構成は以下の様になっています。
役割ごとに名前空間を切って、名前空間ごとにプロジェクトを分けています。

- SampleApp
    - エントリポイントのあるプロジェクト
- DatabaseAccess
    - データアクセス層
    - DB（PostgreSQL）に問い合わせにいく役割
    - NuGetで`Npgsql`を依存性に追加
- Domain
    - いわゆるドメイン層（ビジネスロジックとかそこで使うデータオブジェクトとか）
    - このプロジェクトは他のものに依存しない[^3]
- Domain.Impl
    - ドメイン層のインタフェースに対する実装クラス
    - 実装のためにDatabaseAccessを参照している[^4]

![ソリューションイメージ][dao-pattern-sample.png]

### 前提のデータ定義

サンプル用のユーザーとDBとスキーマを作成するコマンド（PostgreSQL）

```
postgres=# create user dotnet with encrypted password 'mylonglonglongpassword';
postgres=# create database dotnetdb owner dotnet;
postgres=# \c dotnetdb dotnet
dotnetdb=> create schema dotnet;
```

サンプル用のテーブルとデータ

```sql

create table persons (
  id    int           primary key,
  name  varchar(100)  not null
);

insert into persons
(id, name)
values
(1, 'Moira'),
(2, 'Mike'),
(3, 'Masahiro'),
(4, 'Natt'),
(5, 'Nancy'),
(6, 'Jake'),
(7, 'Mary'),
(8, 'Jabsco');
```

#### Persons表

以下のデータが入ってればOK

|id |name     |
|:--|:--------|
|1  |Moira    |
|2  |Mike     |
|3  |Masahiro |
|4  |Natt     |
|5  |Nancy    |
|6  |Jake     |
|7  |Mary     |
|8  |Jabsco   |

### SampleApp 名前空間

エントリポイントとなる`Main()`プロシージャのあるモジュール

```vb:Program.vb
Module Program
    Sub Main()
        Using connection = DatabaseAccess.GetConnection(dbName:="dotnetdb", user:="dotnet", pass:="mylonglonglongpassword")
            Call connection.Open()
            Dim transaction = connection.BeginTransaction()
            Try
                Dim personsDao = DatabaseAccess.GetPersonsDao(connection, transaction)

                Dim GetPersonList = Domain.Impl.GetInstance_GetPersonList(personsDao)
                Console.WriteLine("--- all")
                For Each p In GetPersonList.Execute
                    Console.WriteLine(p.ToString)
                Next
                Console.WriteLine("---")

                Dim SearchPersonsByNamePrefix = Domain.Impl.GetInstance_SearchPersonsByNamePrefix(personsDao)
                Console.WriteLine("--- Name prefix 'Ma'")
                For Each p In SearchPersonsByNamePrefix.Execute("Ma")
                    Console.WriteLine(p.ToString)
                Next
                Console.WriteLine("---")

                Dim SearchPersonsByNameSuffix = Domain.Impl.GetInstance_SearchPersonsByNameSuffix(personsDao)
                Console.WriteLine("--- Name suffix 'y'")
                For Each p In SearchPersonsByNameSuffix.Execute("y")
                    Console.WriteLine(p.ToString)
                Next
                Console.WriteLine("---")

                transaction.Commit()
            Catch e As Exception
                transaction.Rollback()
            End Try
        End Using
    End Sub
End Module
```

実行するとコンソールに以下の内容で表示されます。

```
--- all
Person[Id=1,Name=Moira]
Person[Id=2,Name=Mike]
Person[Id=3,Name=Masahiro]
Person[Id=4,Name=Natt]
Person[Id=5,Name=Nancy]
Person[Id=6,Name=Jake]
Person[Id=7,Name=Mary]
Person[Id=8,Name=Jabsco]
---
--- Name prefix 'Ma'
Person[Id=3,Name=Masahiro]
Person[Id=7,Name=Mary]
---
--- Name suffix 'y'
Person[Id=5,Name=Nancy]
Person[Id=7,Name=Mary]
---
```

### Domain 名前空間

#### Person.vb

何かの名簿的なもののデータ（Persons表に対応する）[^5]

```vb:Person.vb
Public Class Person
    Public Property Id As Integer
    Public Property Name As String

    Public Sub New(Optional ByVal id As Integer = 0, Optional ByVal name As String = "")
        Me.Id = id
        Me.Name = name
    End Sub

    Public Overrides Function ToString() As String
        Return $"{Me.GetType.Name}[Id={Me.Id},Name={Me.Name}]"
    End Function
End Class
```

#### IGetPersonList.vb

ビジネスロジック：（条件なしに）名簿データを取る

```vb:IGetPersonList.vb
Public Interface IGetPersonList
    Function Execute() As IList(Of Person)
End Interface
```

#### ISearchPersonsByNamePrefix.vb

ビジネスロジック：前方一致で名簿を検索する

```vb:ISearchPersonsByNamePrefix.vb
Public Interface ISearchPersonsByNamePrefix
    Function Execute(ByVal namePrefix As String) As IList(Of Person)
End Interface
```

#### ISearchPersonsByNameSuffix.vb

ビジネスロジック：後方一致で名簿を検索する

```vb:ISearchPersonsByNameSuffix.vb
Public Interface ISearchPersonsByNameSuffix
    Function Execute(ByVal nameSuffix As String) As IList(Of Person)
End Interface
```

### Domain.Impl 名前空間

Domain名前空間のインタフェースを実装する

#### GetPersonList.vb

```vb:GetPersonList.vb
Class GetPersonList
    Implements Domain.IGetPersonList

    Private Property PersonsDao As DatabaseAccess.IPersonsDao

    Sub New(ByVal personsDao As DatabaseAccess.IPersonsDao)
        Me.PersonsDao = personsDao
    End Sub

    Public Function Execute() As IList(Of Person) Implements IGetPersonList.Execute
        Return Me.PersonsDao.GetPersons
    End Function
End Class
```

#### SearchPersonsByNamePrefix.vb

前方一致検索！

```vb:SearchPersonsByNamePrefix.vb
Class SearchPersonsByNamePrefix
    Implements Domain.ISearchPersonsByNamePrefix

    Private Property PersonsDao As DatabaseAccess.IPersonsDao

    Sub New(ByVal personsDao As DatabaseAccess.IPersonsDao)
        Me.PersonsDao = personsDao
    End Sub

    Public Function Execute(ByVal namePrefix As String) As IList(Of Person) Implements ISearchPersonsByNamePrefix.Execute
        Return Me.PersonsDao.GetPersonsByNamePattern(pattern:=$"{namePrefix}%")
    End Function
End Class
```

#### SearchPersonsByNameSuffix.vb

こっちは後方一致検索！

```vb:SearchPersonsByNameSuffix.vb
Class SearchPersonsByNameSuffix
    Implements Domain.ISearchPersonsByNameSuffix

    Private Property PersonsDao As DatabaseAccess.IPersonsDao

    Sub New(ByVal personsDao As DatabaseAccess.IPersonsDao)
        Me.PersonsDao = personsDao
    End Sub

    Public Function Execute(ByVal nameSuffix As String) As IList(Of Person) Implements ISearchPersonsByNameSuffix.Execute
        Return Me.PersonsDao.GetPersonsByNamePattern(pattern:=$"%{nameSuffix}")
    End Function
End Class
```

#### DomainImpl.vb

実装クラスのインスタンス生成を行うモジュールです。
利用側にはなるべく実装クラスは直接見せない方がいいのでワンクッション置いています。
このサンプルの構成なら参照側からは`Domain.Impl.DomainImpl`モジュールとそれが持つ関数しか見えません。

```vb:DomainImpl.vb
Public Module DomainImpl
    Function GetInstance_GetPersonList(ByVal personsDao As DatabaseAccess.IPersonsDao) As Domain.IGetPersonList
        Return New GetPersonList(personsDao)
    End Function

    Function GetInstance_SearchPersonsByNamePrefix(ByVal personsDao As DatabaseAccess.IPersonsDao) As Domain.ISearchPersonsByNamePrefix
        Return New SearchPersonsByNamePrefix(personsDao)
    End Function

    Function GetInstance_SearchPersonsByNameSuffix(ByVal personsDao As DatabaseAccess.IPersonsDao) As Domain.ISearchPersonsByNameSuffix
        Return New SearchPersonsByNameSuffix(personsDao)
    End Function
End Module
```

### DatabaseAccess 名前空間

いわゆるデータアクセス層

#### IPersonsDao.vb

DAOもインタフェースで利用者から実装を隠します。[^6]

```vb:IPersonsDao.vb
Public Interface IPersonsDao
    Function GetPersons() As IList(Of Domain.Person)
    Function GetPersonsByNamePattern(ByVal pattern As String) As IList(Of Domain.Person)
End Interface
```

#### PersonsDao.vb

DAOクラスの実装は部分クラスとして宣言して複数のファイルで実装するやり方にすると保守しやすくなります。
カラム数の多いテーブルや色々な検索条件がある場合、部分クラスにしておかないと1つのファイルが大きくなりすぎるし、修正も衝突しやすいので。

コンストラクタや共通処理（データ行からデータオブジェクトに詰め替える）は部分クラスの最初の断片にまとめましょう。

```vb:PersonsDao.vb
Imports Npgsql

Partial Class PersonsDao
    Implements IPersonsDao

    Private Property Connection As NpgsqlConnection
    Private Property Transaction As NpgsqlTransaction

    Sub New(Optional ByVal connection As NpgsqlConnection = Nothing, Optional ByVal transaction As NpgsqlTransaction = Nothing)
        Me.Connection = connection
        Me.Transaction = transaction
    End Sub

    Protected Function ExecuteQueryForList(ByVal queryCommnad As NpgsqlCommand) As IList(Of Domain.Person)
        Dim persons As IList(Of Domain.Person) = New List(Of Domain.Person)
        Using reader = queryCommnad.ExecuteReader()
            While reader.HasRows And reader.Read
                persons.Add(ToPerson(reader))
            End While
        End Using
        Return persons
    End Function

    Protected Function ToPerson(ByVal reader As NpgsqlDataReader) As Domain.Person
        Dim id As Integer = CInt(reader("id"))
        Dim name As String = CStr(reader("name"))
        Return New Domain.Person(id, name)
    End Function
End Class
```

#### PersonsDao_GetPersons.vb

断片その2
ファイル名はクラス名＋実装するメソッド名という形にすると分かりやすいです。
うっかりするとファイル名と中身が乖離するので別のファイルからコピーして作る時は特に注意しましょう。

```vb:PersonsDao_GetPersons.vb
Partial Class PersonsDao
    Public Function GetPersons() As IList(Of Domain.Person) Implements IPersonsDao.GetPersons
        Dim sqlText = "
select
    id
    ,name
from
    persons
order by
    id asc
"
        Using command = MakeCommand(sqlText, Me.Connection, Me.Transaction)
            Return ExecuteQueryForList(command)
        End Using
    End Function
End Class
```

#### PersonsDao_GetPersonsByNamePattern.vb

断片その3。パラメータ化クエリ。
パラメータを入れる位置にプレースホルダーを書き、コマンドオブジェクトのAPI経由で引数の値を埋め込みます。
**SQLインジェクションの危険があるのでパラメータを直接SQLに埋め込んではいけません**

```vb:PersonsDao_GetPersonsByNamePattern.vb
Imports NpgsqlTypes

Partial Class PersonsDao
    Public Function GetPersonsByNamePattern(
        ByVal pattern As String
    ) As IList(Of Domain.Person) Implements IPersonsDao.GetPersonsByNamePattern
        Dim sqlText = "
select
    id
    ,name
from
    persons
where
    name like @pattern
order by
    id asc
"
        Using command = MakeCommand(sqlText, Me.Connection, Me.Transaction)
            command.Parameters.AddWithValue(
                parameterType:=NpgsqlDbType.Varchar,
                parameterName:="pattern",
                value:=pattern)
            Return ExecuteQueryForList(command)
        End Using
    End Function
End Class
```

#### DatabaseAccess.vb

実装クラスのインスタンス生成を行うモジュール。
このサンプルではDBへのConnectionを取る関数もここに書いています。
DataAccess名前空間に特定のDBに依存する内容を書きたくないならGetConnection関数はエントリポイント側に移してもよいです。[^7]

```vb:DatabaseAccess.vb
Imports Npgsql

Public Module DatabaseAccess
    Function GetConnection(
            ByVal dbName As String,
            ByVal user As String,
            ByVal pass As String,
            Optional ByVal host As String = "localhost",
            Optional ByVal port As Integer = 5432
    ) As NpgsqlConnection
        Dim connectionString As String = $"Host={host};Port={port};Database={dbName};Username={user};Password={pass};"
        Dim connection = New NpgsqlConnection(connectionString)
        Return connection
    End Function

    Function GetPersonsDao(
            Optional ByVal connection As NpgsqlConnection = Nothing,
            Optional ByVal transaction As NpgsqlTransaction = Nothing
    ) As IPersonsDao
        Return New PersonsDao(connection, transaction)
    End Function

    Friend Function MakeCommand(
            ByVal cmdText As String,
            Optional ByVal connection As NpgsqlConnection = Nothing,
            Optional ByVal transaction As NpgsqlTransaction = Nothing) As NpgsqlCommand
        Return New NpgsqlCommand(cmdText, connection, transaction)
    End Function
End Module
```

[^1]: 開発言語としては手に馴染む感じがあってむしろ気に入っていますが、仕事では使いたくありません。
[^2]: 気が向いたらGitHubにアップします。多分。そのうち。
[^3]: データクラスとインタフェースしか置いてない。
[^4]: うっすらクリーンアーキテクチャ的に書いてるけど、DAOパターンを書くという本題と外れるので突き詰めない。
[^5]: サンプルは単純な表なので完全対応しているが、実際にはDBのカラム名とは違ってもいい。データ型も違っててもいい。予約語とか文字数の都合でDBにそのまま入らないこともあるし、DBに日付型で入れたくなかったりもするだろう。
[^6]: 少なくともインタフェースのレベルではなるべく実装の詳細に触れない方がいい。
[^7]: `Npgsql`名前空間に依存している部分を`System.Data`名前空間のインタフェースで置き換えればDB製品を差し替え可能になる。
[^me]: 私だ。
[dao-pattern-sample.png]: https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83175/d0a62050-ae97-35fa-7279-de9dff6ff16f.png