ことわり
期待してこの記事を開いてくれた方のために予め断っておくと筆者はVB.Net
なんか大嫌いです。1
目に余るほど悲惨なクオリティのコードが世の中に溢れかえっているからです。
こんな開発言語はこの世界から一日でも早く消えてなくなれと願っています。
その願いは叶いそうもないので悲惨なコードがちょっとでもマシになればいいなという気持ちでこの記事を書いています。
コード全文を掲載しているので若干長い記事になっているのはご容赦ください。2
あくまでパターンを見せたいだけであって、トランザクション処理はこのサンプルのやり方ではマズいかもしれないので鵜呑みにしないようにしてください。
想定読者
- SQL文の発行とデータソースへの問い合わせとビジネスロジックが同じプロシージャに書いてある上にデータオブジェクトという考え方すらないレガシィでカオティックなプログラムを見て眩暈を起こしている人3
- データソースからのデータ取得とビジネスロジックを分離しない人
- …のうち、ちょっとは分離すべきだと思ってる人
- オブジェクト指向とか言われてもよく分かんないしコワい!な人
- …のうち、分かりたい気持ちはある人
想定してない読者
- 「EntityFrameworkとか当たり前に使ってにバリバリ書いてるぜ」という人達にはこの記事の内容は物足りないかと思います。
- 「DAOパターンなんか時代遅れだよ、もっといい〇〇パターンがあるよ」と考える人には時代遅れな記事です。
サンプルプログラム
ソリューションの構成は以下の様になっています。
役割ごとに名前空間を切って、名前空間ごとにプロジェクトを分けています。
- SampleApp
- エントリポイントのあるプロジェクト
- DatabaseAccess
- データアクセス層
- DB(PostgreSQL)に問い合わせにいく役割
- NuGetで
Npgsql
を依存性に追加
- Domain
- いわゆるドメイン層(ビジネスロジックとかそこで使うデータオブジェクトとか)
- このプロジェクトは他のものに依存しない4
- Domain.Impl
- ドメイン層のインタフェースに対する実装クラス
- 実装のためにDatabaseAccessを参照している5
![ソリューションイメージ][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;
サンプル用のテーブルとデータ
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()
プロシージャのあるモジュール
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表に対応する)6
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
ビジネスロジック:(条件なしに)名簿データを取る
Public Interface IGetPersonList
Function Execute() As IList(Of Person)
End Interface
ISearchPersonsByNamePrefix.vb
ビジネスロジック:前方一致で名簿を検索する
Public Interface ISearchPersonsByNamePrefix
Function Execute(ByVal namePrefix As String) As IList(Of Person)
End Interface
ISearchPersonsByNameSuffix.vb
ビジネスロジック:後方一致で名簿を検索する
Public Interface ISearchPersonsByNameSuffix
Function Execute(ByVal nameSuffix As String) As IList(Of Person)
End Interface
Domain.Impl 名前空間
Domain名前空間のインタフェースを実装する
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
前方一致検索!
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
こっちは後方一致検索!
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
モジュールとそれが持つ関数しか見えません。
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もインタフェースで利用者から実装を隠します。7
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つのファイルが大きくなりすぎるし、修正も衝突しやすいので。
コンストラクタや共通処理(データ行からデータオブジェクトに詰め替える)は部分クラスの最初の断片にまとめましょう。
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
ファイル名はクラス名+実装するメソッド名という形にすると分かりやすいです。
うっかりするとファイル名と中身が乖離するので別のファイルからコピーして作る時は特に注意しましょう。
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に埋め込んではいけません
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関数はエントリポイント側に移してもよいです。8
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
-
開発言語としては手に馴染む感じがあってむしろ気に入っていますが、仕事では使いたくありません。 ↩
-
気が向いたらGitHubにアップします。多分。そのうち。 ↩
-
私だ。
[dao-pattern-sample.png]: https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83175/d0a62050-ae97-35fa-7279-de9dff6ff16f.png ↩ -
データクラスとインタフェースしか置いてない。 ↩
-
うっすらクリーンアーキテクチャ的に書いてるけど、DAOパターンを書くという本題と外れるので突き詰めない。 ↩
-
サンプルは単純な表なので完全対応しているが、実際にはDBのカラム名とは違ってもいい。データ型も違っててもいい。予約語とか文字数の都合でDBにそのまま入らないこともあるし、DBに日付型で入れたくなかったりもするだろう。 ↩
-
少なくともインタフェースのレベルではなるべく実装の詳細に触れない方がいい。 ↩
-
Npgsql
名前空間に依存している部分をSystem.Data
名前空間のインタフェースで置き換えればDB製品を差し替え可能になる。 ↩