このヘージと関連するページ一覧

  • SQLite + System.Data.Linq 導入編
  • SQLite + System.Data.Linq 同時実行制御が不十分な場合
  • SQLite + System.Data.Linq 同時実行制御

  • このページの最終更新日 2020/10/25

    SQLite + System.Data.Linq 同時実行制御が不完全な場合

    p> このページは 「SQLite + System.Data.Linq 導入編」の続きのページです。引き続き「データベーススペシャリスト平成26年度春季 午後Ⅰ問2」の会議室の予約システムをもとにして、SQLiteデータベースの演習を行っていきます。

    1、DateTimePickerコントロールとNumericUpDownコントロール

     前回は予約日も予約時刻も適当な文字列が入力されるようになっていました。SQLiteでは時間というデータ型が存在しないので、文字列と時間データの変換を自力で実装していく必要があります。まずは入力値の制限を行うことから始めます。やり方は様々ですが、ここではDateTimePickerコントロールやNumericUpDownコントロールを用いて入力値の制限を設けることにします。

     前回から変更したフォームのコントロールのオブジェクト名をpic.1に示します。時間(Hour)を表すNumericUpDownコントロールの最小値(Mimimum)は0、最大値(Maximum)は23、数値の刻み間隔(Increment)は1とします。分(Minute)を表すNumericUpDownコントロールの最小値は0、最大値は45、数値の刻み間隔は15とします。分の部分は0分、15分、30分、45分のいずれかしか選択出来ない仕様とします。

    DateTimePickerコントロールとNumericUpDownコントロールによる入力制限

    - pic.1 -

     フォームのコントロールの変更に伴ってFormEditクラスのコードもcode.1のように修正します。このように入力値の制限されたコントロールを使用することで、予約日は「yyyy/MM/dd」の形式に、予約開始時刻と予約終了時刻は「HH:mm」の形式に統一出来るようになりました。ここでは、「DecimalValueToStringD2」メソッドでNumericUpDownコントロールで入力された数値が1桁の場合には、頭に0を付加することによって時間・分が常に2桁で表示されるように調整しています。

    - code.1 - ファイル名「FormEdit.cs」
    public partial class FormEdit : Form
    {
        public FormEdit()
        {
            InitializeComponent();
            btnOk.Click += new EventHandler(OKButtonClick);
            btnCancel.Click += new EventHandler(CancelButtonClick);
        }
    
        public void OKButtonClick(object sender, EventArgs e)
        {
            var objDb = new DatabaseWrapper();
            var objReservationConferenceRoomTable = objDb.GetReservationConferenceRoomTable();
    
            string reservationDate = dtpReservationDate.Value.ToString("yyyy/MM/dd");
            string startHHmm = DecimalValueToStringD2(udStartHour.Value) + ":" + DecimalValueToStringD2(udStartMinute.Value);
            string endHHmm = DecimalValueToStringD2(udEndHour.Value) + ":" + DecimalValueToStringD2(udEndMinute.Value);
    
            objReservationConferenceRoomTable.InsertOnSubmit(new 会議室予約()
            {
                会議室番号 = int.Parse(tbConferenceRoomId.Text),
                予約日 = reservationDate,
                予約開始時刻 = startHHmm,
                予約終了時刻 = endHHmm
            });
            objDb.SubmitChanges();
    
            this.DialogResult = DialogResult.OK;
            this.Close();
        }
    
         private string DecimalValueToStringD2(Decimal udValue)
        {
            return ((int)udValue).ToString("D2");
        }
    
        public void CancelButtonClick(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.Cancel;
            this.Close();
        }
    }
    

     ここで、pic.1のように「2018年7月26日」の「9時0分」から「10時0分」までと指定して「OK」ボタンを押すと pic.2のようにデータベースのテーブルが更新されます。

    DateTimePickerコントロールとNumericUpDownコントロールの動作確認

    - pic.2 -

    2、予約したい時間が既に予約されている時間と重なる場合

     入力値を「日付」「時刻」の形式に制御することが出来たので、次は予約したい時間について整合性を検査することにします。検査すべきことは次の2点です。

    • (i)予約終了時刻が予約開始時刻よりも後の時刻であること。
    • (ii)予約したい時刻が既に予約されている時刻と重なる時間帯を含まないこと。
    (i)については、開始時刻も終了時刻も同一の日付であるとし、「2018年7月26日20時~2018年7月27日10時」などのように日付を跨いで予約されることはないものとします。
    (ii)については、pic.3で図示している場合の①②③④のような場合については、新に予約したい時間帯が、既に予約済みの時間帯と重なっているため、新に予約しようとした場合にエラーとしなければいけません。

     この2つの条件をプログラムで書くと、(i)がcode.3の25行目、(ii)がcode.3の30行目に対応します。(ii)については、データベーススペシャリストの試験問題でも問われていました。

    予約できる場合と予約できない場合を図示

    - pic.3 -

    - code.2 - ファイル名「会議室予約.cs」
    [Table(Name = "会議室予約")]
    public class 会議室予約
    {
        [Column(Name = "会議室番号", IsPrimaryKey = true)]
        public int 会議室番号 { get; set; }
    
        [Column(Name = "予約日", IsPrimaryKey = true)]
        public string 予約日 { get; set; }
    
        [Column(Name = "予約開始時刻", IsPrimaryKey = true)]
        public string 予約開始時刻 { get; set; }
    
        [Column(Name = "予約終了時刻")]
        public string 予約終了時刻 { get; set; }
    
         public DateTime StartDateTime()
        {
            return DateTime.Parse(this.予約日 + " " + this.予約開始時刻);
        }
    
        public DateTime EndDateTime()
        {
            return DateTime.Parse(this.予約日 + " " + this.予約終了時刻);
        }
    }
    
    - code.3 - ファイル名「FormEdit.cs」
    public partial class FormEdit : Form
    {
        public FormEdit()
        {
            InitializeComponent();
            btnOk.Click += new EventHandler(OKButtonClick);
            btnCancel.Click += new EventHandler(CancelButtonClick);
        }
    
        public void OKButtonClick(object sender, EventArgs e)
        {
            try
            {
                var objDb = new DatabaseWrapper();
                var objReservationConferenceRoomTable = objDb.GetReservationConferenceRoomTable();
    
                int conferenceRoomId = int.Parse(tbConferenceRoomId.Text);
                string reservationDate = dtpReservationDate.Value.ToString("yyyy/MM/dd");
                string startHHmm = DecimalValueToStringD2(udStartHour.Value) + ":" + DecimalValueToStringD2(udStartMinute.Value);
                string endHHmm = DecimalValueToStringD2(udEndHour.Value) + ":" + DecimalValueToStringD2(udEndMinute.Value);
    
                DateTime startDateTime = DateTime.Parse(reservationDate + " " + startHHmm);
                DateTime endDateTime = DateTime.Parse(reservationDate + " " + endHHmm);
    
                if (endDateTime <= startDateTime)
                {
                    throw new Exception("予約終了時刻は予約開始時刻よりも後の時刻を指定していなければなりません。");
                }
    
                if (objReservationConferenceRoomTable.ToList().Where(m => m.会議室番号 == conferenceRoomId && m.StartDateTime() < endDateTime && startDateTime < m.EndDateTime()).Any())
                {
                    throw new Exception("予約しようとした時間の中に、既に予約されている時間が含まれています。");
                }
    
                objReservationConferenceRoomTable.InsertOnSubmit(new 会議室予約()
                {
                    会議室番号 = conferenceRoomId,
                    予約日 = reservationDate,
                    予約開始時刻 = startHHmm,
                    予約終了時刻 = endHHmm
                });
                objDb.SubmitChanges();
    
                this.DialogResult = DialogResult.OK;
                this.Close();
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    
        private string DecimalValueToStringD2(Decimal udValue)
        {
            return ((int)udValue).ToString("D2");
        }
    
        public void CancelButtonClick(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.Cancel;
            this.Close();
        }
    }

     ここで(i)の条件に反するような時刻を指定した場合はpic.4のエラーメッセージが表示され、(ii)の条件に反する場合はpic.5のエラーメッセージが表示されることを確認してみましょう。あまり深く考えずにいると、これで予約システムの整合性が取れているように思えますが、このままでは同時実行時にトラブルを生じることになります。

    (i)に反するエラー

    - pic.4 -

    (ii)に反するエラー

    - pic.5 -

    3、2人が同時に予約処理を行った場合

     ここでは、同時実行の際に起こるトラブルを解りやすくするため、code.4のようにcode.3の33行目と35行目の間に、「Task.Delay(3000).Wait()」という1行を挿入してみます。この行まで実行されると3秒(=3000ミリ秒)プログラムが待機状態になります。

    - code.4 - ファイル名「FormEdit.cs」
        if (objReservationConferenceRoomTable.ToList().Where(m => m.会議室番号 == conferenceRoomId && m.StartDateTime() < endDateTime && startDateTime < m.EndDateTime()).Any())
        {
            throw new Exception("予約しようとした時間の中に、既に予約されている時間が含まれています。");
        }
    
        Task.Delay(3000).Wait();
    
        objReservationConferenceRoomTable.InsertOnSubmit(new 会議室予約()
        {
            会議室番号 = conferenceRoomId,
            予約日 = reservationDate,
            予約開始時刻 = startHHmm,
            予約終了時刻 = endHHmm
        });
        objDb.SubmitChanges();
    

     次にC#で作成したプログラムの「.exe」ファイルを2回起動して、pic.6のように2つフォーム①②を表示し、片方には「2019年5月13日8時0分~2019年5月13日10時0分」と指定し、もう片方には「2019年5月13日9時0分~2019年5月13日11時0分」と指定します。その後、出来る限り同時に2つのフォームの「OK」ボタンを押下します。

    プログラムを二重起動

    - pic.6 -


     すると、pic.7のようにダブルブッキングが発生してしまいました。

    不整合のある予約テーブル

    - pic.7 -