モデルとビュー
業務を分析する際には「処理(プロセス)」と「データ」という2つの視点があります。あるデータが業務プロセスを通り過ぎることで別の新しいデータが発生します。そして業務と業務はデータでつながっていて、ある業務のアウトプットが別の業務のインプットになります。その最もポピュラーな例は「伝票」です。
業務アプリケーションを設計する際には、まず業務プロセスとデータのインプット、アウトプットをしっかり認識して、まず総合的なデータのあるべき姿を考慮したデータベース設計を行います。このように業務アプリケーション設計時にまずデータに着目する手法をDOA(データ指向アプローチ)と呼びます。
業務アプリケーションにおいての業務プロセスはプログラム上では画面によって表現されます。このように業務プロセスに着目して作られたもの(ここでは画面)のことを「ビュー」と呼びます。
通常、データを重視して作られたデータベースの持つデータ構造と、業務プロセスを重視して作られた「ビュー」が必要とするデータ構造には大きな違いがあります。そこでプログラム上においてデータ寄りの要素を持つ「モデル」という概念を利用します。「データベース」へのアクセスは「モデル」が受け持ち、データを業務プロセス向けに加工したものを「ビュー」に橋渡しすることで、柔軟性の高い設計が可能になります。
YWorks Software Framework(以下、YWSFと呼びます)では、「モデル」と「ビュー」を切り分けて作成し、それらを結びつけてコントロールするためのフレームワークを提供しています。「モデル」にはリスト形式のデータを取り扱う「YCDbList」クラスと単体(1レコード)のデータを主として取り扱う「YCDbObject」クラスを用意しています。また、「ビュー」はリスト形式表示用の「YCDbListForm」「YCDbListDialog」フォームクラスと単体データ表示用の「YCDbObjectForm」「YCDbObjectDialog」フォームクラスを用意しています。業務アプリケーション作成の際はそれらのクラスを継承してプログラムを作成していきます。
(クラス図)
ここに「モデル」系クラスのベースクラスとなっている「YCDbBase」というクラスのソースコードを示します。このクラスについては特に解説は入れませんが、もし、YWSFが提供する機能の仕様としてこれから使用したい業務アプリケーションに合わない場合はサブクラスにてオーバーライドする等の処置をとってください。またバグ等を発見された方は、お手数ですが訂正コードを合わせてご一報いただけると幸いです。
「YCDbBase」クラス
public class YCDbBase : YCRefCountObject { private YCConnection _connection; private DataTable _dataTable; private DbDataAdapter _adapter; private DbCommandBuilder _commandBuilder; private string _authorityCode; private int _cursor; private bool _loadedSchema; private bool _isLoaded; private bool _makedSql; public YCDbBase(YCConnection connection) : base() { _connection = connection; _dataTable = new DataTable(); _dataTable.Locale = new System.Globalization.CultureInfo("ja-JP"); _dataTable.TableNewRow += new DataTableNewRowEventHandler(this.OnAddNewRow); if(_connection != null) _adapter = _connection.CreateAdapter(); _commandBuilder = null; _authorityCode = string.Empty; _cursor = YCDatabaseConst.DB_BOF; _loadedSchema = false; _isLoaded = false; _makedSql = false; } public override void Dispose() { if(IsDisposable) { try { _dataTable.Dispose(); } catch { } if (_commandBuilder != null) { try { _commandBuilder.Dispose(); } catch { } } try { _adapter.Dispose(); } catch { } _connection = null; _dataTable = null; _commandBuilder = null; _adapter = null; } base.Dispose(); } //新規行追加イベント private void OnAddNewRow(object sender, DataTableNewRowEventArgs e) { this.InitializeNewRow(e.Row); } //新規行の初期化メソッド //必要に応じてオーバーライドして下さい。 protected virtual void InitializeNewRow(DataRow newRow) { } //インデクサ public object this[int pos] { get { try { if(this.Table.Rows[_cursor].RowState == DataRowState.Deleted) return null; return this.Table.Rows[_cursor][pos]; } catch { return null; } } set { try { string message = "項目[" + pos + "]が不正です。"; if(ColumnCheck(pos, value, ref message) == false) throw new YCDbException(message); if(YCConvert.ToString(value) == "") this.Table.Rows[_cursor][pos] = DBNull.Value; else this.Table.Rows[_cursor][pos] = value; } catch(Exception e) { throw new YCDbException("値の設定に失敗しました。", e); } } } public object this[string columnName] { get { try { if(this.Table.Rows[_cursor].RowState == DataRowState.Deleted) return null; return this.Table.Rows[_cursor][columnName]; } catch { return null; } } set { try { string message = "項目「" + columnName + "」が不正です。"; if(ColumnCheck(columnName, value, ref message) == false) throw new YCDbException(message); if(YCConvert.ToString(value) == "") this.Table.Rows[_cursor][columnName] = DBNull.Value; else this.Table.Rows[_cursor][columnName] = value; } catch(Exception e) { throw new YCDbException("値の設定に失敗しました。", e); } } } //値が適正かをチェックする。 protected virtual bool ColumnCheck(int index, object data, ref string errorMessage) { return true; } protected virtual bool ColumnCheck(string columnName, object data, ref string errorMessage) { return true; } //オートSQLを利用しないときはオーバーライドして下さい。 protected virtual bool AutoSQL { get { return true; } } //保存を許可しないときはオーバーライドして下さい。 public virtual bool AllowSave { get { return true; } } //トランザクションを使用するときはオーバーライドして下さい。 protected virtual bool UseTrans { get { return false; } } //トランザクションのネストを許可するときはオーバーライドして下さい。 protected virtual bool AllowNestTrans { get { return false; } } public virtual string AuthorityCode { get { return _authorityCode; } set { _authorityCode = value; } } public YCConnection Connection { get { return _connection; } } public virtual DataTable Table { get { return _dataTable; } } public virtual DataRow NewRow { get { return this.Table.NewRow(); } } public DbDataAdapter Adapter { get { return _adapter; } } protected DbCommandBuilder CommandBuilder { get { return _commandBuilder; } } public virtual int RowCount { get { try { return this.Table.Rows.Count; } catch(Exception e) { throw new YCDbException("行数の取得に失敗しました。", e); } } } public virtual int ColCount { get { try { return this.Table.Columns.Count; } catch (Exception e) { throw new YCDbException("列数の取得に失敗しました。", e); } } } public string GetColumnName(int index) { if (index < 0 || index >= this.ColCount) throw new YCDbException("インデックスが不正です。"); return this.Table.Columns[index].ColumnName; } public string Sort { get { return this.Table.DefaultView.Sort; } set { this.Table.DefaultView.Sort = value; } } public virtual bool IsLoadedSchema { get { return _loadedSchema; } } public virtual bool IsLoaded { get { return _isLoaded; } } public virtual int StringDataToIndex(string columnName, string data) { return StringDataToIndex(columnName, data, 0); } public virtual int StringDataToIndex(string columnName, string data, int start) { if(start < 0 || start >= this.RowCount) throw new YCDbException("インデックスのスタート位置が不正です。"); for(int cnt = start; cnt < this.RowCount; cnt++) { if(this.Table.Rows[cnt].RowState != DataRowState.Deleted) { if(this.Table.Rows[cnt][columnName].ToString() == data) return cnt; } } return YCDatabaseConst.NO_INDEX; } //指定の列に指定の文字列のデータが含まれればTrue public virtual bool StringDataIsExisting(string columnName, string data) { if (this.StringDataToIndex(columnName, data) == YCDatabaseConst.NO_INDEX) return false; else return true; } public virtual void MakeSQL() { if(this.Adapter.SelectCommand == null) { this.Adapter.SelectCommand = this.Connection.CreateCommand(); } this.Adapter.SelectCommand.CommandText = this.MakeSQLSelect(); this.Adapter.SelectCommand.CommandTimeout = this.SelectTimeout; if(this.AllowSave) { if(this.AutoSQL) { try { if(_commandBuilder == null) { _commandBuilder = this.Connection.CreateCommandBuilder(this.Adapter); } else { _commandBuilder.RefreshSchema(); } } catch(Exception e) { try{ _commandBuilder.Dispose(); } catch{ } _commandBuilder = null; throw new YCDbException("SQLコマンドの作成に失敗しました。", e); } } else { if (this.Adapter.InsertCommand == null) { this.Adapter.InsertCommand = this.Connection.CreateCommand(); } if (this.Adapter.UpdateCommand == null) { this.Adapter.UpdateCommand = this.Connection.CreateCommand(); } if (this.Adapter.DeleteCommand == null) { this.Adapter.DeleteCommand = this.Connection.CreateCommand(); } this.Adapter.InsertCommand.CommandText = MakeSQLInsert(this.Adapter.InsertCommand.Parameters); this.Adapter.UpdateCommand.CommandText = MakeSQLUpdate(this.Adapter.UpdateCommand.Parameters); this.Adapter.DeleteCommand.CommandText = MakeSQLDelete(this.Adapter.DeleteCommand.Parameters); } } _loadedSchema = false; _makedSql = true; } protected virtual string MakeSQLSelect() { return string.Empty; } protected virtual string MakeSQLInsert(DbParameterCollection parameters) { return string.Empty; } protected virtual string MakeSQLUpdate(DbParameterCollection parameters) { return string.Empty; } protected virtual string MakeSQLDelete(DbParameterCollection parameters) { return string.Empty; } protected virtual int SelectTimeout { get { return YCDatabaseConst.DEFAULT_SELECT_TIMEOUT; //SELECTコマンドのタイムアウト(秒) } } public virtual void LoadSchema() //スキーマのみ、再読込みしない。 { if(!_makedSql) MakeSQL(); if(!_loadedSchema) { SetTrans(); this.Adapter.FillSchema(this.Table, SchemaType.Mapped); _loadedSchema = true; } } public virtual int Fill() //再読込みはしない { int ret = 0; try { this.BeginTrans(); if(!_isLoaded) { if(!_makedSql) MakeSQL(); ret = FillTable(); } return ret; } catch(Exception e) { try { this.Rollback(); } catch { } try { MoveBOF(); } catch { } _loadedSchema = false; _isLoaded = false; throw new YCDbException("データの取り込みに失敗しました。", e); } } public virtual int Fill(bool allowReload) { if (_isLoaded && allowReload) return this.Refill(); else return this.Fill(); } public virtual int Refill() { return Refill(false); } public virtual int Refill(bool withCommit) //SQLから再読込み { int ret = 0; try { if (_isLoaded) { if(withCommit) { Commit(); } else { Rollback(); } Clear(); } MakeSQL(); BeginTrans(); ret = FillTable(); return ret; } catch(Exception e) { try { this.Rollback(); } catch { } try { MoveBOF(); } catch { } _loadedSchema = false; _isLoaded = false; throw new YCDbException("データの取り込みに失敗しました。", e); } } private int FillTable() { int ret; SetTrans(); ret = this.Adapter.Fill(this.Table); MoveFirst(); _loadedSchema = true; _isLoaded = true; return ret; } public virtual void Clear() { this.Table.Clear(); this.ClearAdapterComands(); _loadedSchema = false; _isLoaded = false; MoveBOF(); } private void ClearAdapterComands() { try{this.Adapter.SelectCommand.Dispose();} catch{} this.Adapter.SelectCommand = null; try { this.Adapter.InsertCommand.Dispose(); } catch{} this.Adapter.InsertCommand = null; try { this.Adapter.UpdateCommand.Dispose(); } catch{} this.Adapter.UpdateCommand = null; try { this.Adapter.DeleteCommand.Dispose(); } catch{} this.Adapter.DeleteCommand = null; } public virtual int Save() { int ret; if(!this.AllowSave) { throw new Exception("このオブジェクトは保存を許可しません。"); } try { if(this.PreSave()) { try { ret = this.Adapter.Update(this.Table); } catch(Exception e) { try{ErrorSave();} catch{} throw e; } PostSave(); return ret; } else { UnSave(); return 0; } } catch(Exception e) { throw new YCDbException("データの保存に失敗しました。", e); } } public virtual bool HasChanges { get { if(this.Connection.HasCommitPoint) { return true; } else { if (this.Table.GetChanges() == null) return false; else return true; } } } public virtual void AcceptChanges() { this.Table.AcceptChanges(); } public virtual void CancelChanges() { this.Table.RejectChanges(); } //Saveの前処理をオーバーライドして下さい。 protected virtual bool PreSave() { return true; } //Saveの後処理をオーバーライドして下さい。 protected virtual void PostSave() { } //PreSaveにてSave不実行時の後処理をオーバーライドして下さい。 protected virtual void UnSave() { } //テーブルのアップデートに失敗した時の後処理をオーバーライドして下さい。 protected virtual void ErrorSave() { } //ユーザグループ権限のチェック public virtual bool CheckAuthority() { return CheckAuthority(YCApplication.Instance.User.AuthorityCode); } //ユーザグループ権限のチェック //管理者ユーザは無条件でデータ権限true //ゲストユーザは無条件でデータ権限false //オブジェクトにデータ権限設定がされていない場合はデータ権限true public virtual bool CheckAuthority(string targetCode) { if(YCApplication.Instance.User.IsAdministrator) { return true; } else if(YCApplication.Instance.User.IsGuest) { return false; } else if (targetCode == this.AuthorityCode) { return true; } else if (this.AuthorityCode == "") { return true; } else { return false; } } //トランザクション protected void SetTrans() { if(!_makedSql) return; try { if(this.Connection.IsTransacting) { this.Adapter.SelectCommand.Transaction = this.Connection.Transaction; if(this.AutoSQL) { if(this.CommandBuilder == null) { _commandBuilder = this.Connection.CreateCommandBuilder(this.Adapter); } else { _commandBuilder.RefreshSchema(); } } else { this.Adapter.InsertCommand.Transaction = this.Connection.Transaction; this.Adapter.UpdateCommand.Transaction = this.Connection.Transaction; this.Adapter.DeleteCommand.Transaction = this.Connection.Transaction; } } } catch(Exception e) { throw new YCDbException("トランザクションのセットに失敗しました。", e); } } protected void ClearTrans() { if(!_makedSql) return; this.Adapter.SelectCommand.Transaction = null; if(this.AutoSQL) { if(_commandBuilder == null) { _commandBuilder = this.Connection.CreateCommandBuilder(this.Adapter); } else { _commandBuilder.RefreshSchema(); } } else { this.Adapter.InsertCommand.Transaction = null; this.Adapter.UpdateCommand.Transaction = null; this.Adapter.DeleteCommand.Transaction = null; } } protected void BeginTrans() //トランザクション開始はFill, Refillで操作する。 { if(!this.UseTrans) return; if(!this.AllowNestTrans && this.Connection.IsTransacting) { throw new YCDbException("トランザクションがネストしています。"); } try { this.Connection.BeginTrans(); } catch { throw new YCDbException("トランザクションの開始に失敗しました。"); } } public void Commit() { if(!this.UseTrans) { if (this.Connection.IsTransacting) { this.Connection.SetCommitPoint(); } return; } try { this.Connection.Commit(); if (!this.Connection.IsTransacting) { ClearTrans(); } } catch { throw new YCDbException("トランザクションのコミットに失敗しました。"); } } public void Rollback() { Rollback(true); } public void Rollback(bool force) { if(!this.UseTrans) return; try { this.Connection.Rollback(force); if (!this.Connection.IsTransacting) { ClearTrans(); } } catch { throw new YCDbException("トランザクションのロールバックに失敗しました。"); } } public void CommitRetrans() { bool lostTrans; if(!this.UseTrans) { if (this.Connection.IsTransacting) { this.Connection.SetCommitPoint(); } return; } try { this.Connection.Commit(); } catch { throw new YCDbException("トランザクションのコミットに失敗しました。"); } if (this.Connection.IsTransacting) { if(!this.AllowNestTrans) { throw new YCDbException("トランザクションがネストしています。"); } else { lostTrans = false; } } else { lostTrans = true; } try { this.Connection.BeginTrans(); if(lostTrans) { ClearTrans(); SetTrans(); } } catch { throw new YCDbException("トランザクションの再開に失敗しました。"); } } public void RollbackRetrans() { bool lostTrans; if(!this.UseTrans) return; try { this.Connection.Rollback(false); } catch { throw new YCDbException("トランザクションのロールバックに失敗しました。"); } if (this.Connection.IsTransacting) { if(!this.AllowNestTrans) { throw new YCDbException("トランザクションがネストしています。"); } else { lostTrans = false; } } else { lostTrans = true; } try { this.Connection.BeginTrans(); if(lostTrans) { ClearTrans(); SetTrans(); } } catch { throw new YCDbException("トランザクションの再開に失敗しました。"); } } //カーソル操作 public virtual bool BOF { get { if(this.Table.Rows.Count < 1) { MoveBOF(); return true; } else if(_cursor == YCDatabaseConst.DB_BOF) { return true; } else if(_cursor < 0) { MoveBOF(); return true; } else { return false; } } } public virtual bool EOF { get { if(this.Table.Rows.Count < 1) { MoveBOF(); return true; } else if(_cursor < this.Table.Rows.Count) { return false; } else if(_cursor == this.Table.Rows.Count) { return true; } else { _cursor = this.Table.Rows.Count; return true; } } } public virtual void MoveBOF() { _cursor = YCDatabaseConst.DB_BOF; } public virtual void MoveEOF() { _cursor = this.Table.Rows.Count; } public virtual void MoveFirst() { _cursor = 0; } public virtual int MoveLast() { _cursor = this.Table.Rows.Count -1; return _cursor; } public virtual int MoveNext() { if(EOF == false) { _cursor++; return _cursor; } else { return _cursor; } } public virtual int MovePrevious() { if(BOF == false) { _cursor--; return _cursor; } else { return _cursor; } } public virtual void Move(int index) { if(index < 0) { _cursor = YCDatabaseConst.DB_BOF; } else if(index >= this.Table.Rows.Count) { index = this.Table.Rows.Count; } else { _cursor = index; } } public virtual bool CurrentRowIsDeleted { get { if(this.Table.Rows[_cursor].RowState == DataRowState.Deleted) return true; else return false; } } //入力エラーチェック(入力エラーがなければtrue) //必要に応じてサブクラスでオーバーライドしてください。 public virtual bool EntryCheck(out string errorString) { errorString = string.Empty; return true; } //ファイル出力 public void WriteToFile(string filePath, YCFileMode mode) { this.WriteToFile(filePath, mode, YCEncodingMode.SJis); } public void WriteToFile(string filePath, YCFileMode mode, YCEncodingMode encoding) { YCFile file = null; if (mode != YCFileMode.Append && mode != YCFileMode.OverWrite) throw new YCFileException("ファイルモードが不正です。"); try { file = new YCFile(filePath, mode, encoding); this.WriteToFile(file); file = null; } catch (Exception e) { if (file != null) { if (file.IsOpened) { try { file.Close(); } catch { } } file = null; } throw e; } } //Fileオブジェクトへの書き込み //サブクラスでオーバーライトしてください。 public virtual void WriteToFile(YCFile file) { } }
データ変更時の権限チェック用としてYWSFではCheckAuthorityというメソッドを用意しています(オーバーライドも可能です)が、フレームワーク内での呼び出しは行っていません。データ権限の確認は扱うデータのセキュリティの重要度によって複雑になることもありますので、モデルやビューのサブクラスで適切なタイミングで権限チェックを行ってください。