楽観的ロック完全実装手順書.md

# 楽観的ロック機能 完全実装手順書

## 概要
本手順書は、顧客マスタと送信パターンマスタにおける楽観的ロック機能の完全な実装手順を記載したものです。
編集セルの不具合修正、DateTime精度問題への対応、更新エラー時の画面継続処理を含む、包括的な実装ガイドです。

## 実装する機能
1. 楽観的ロックによる同時更新制御
2. DateTime精度問題の解決(1秒許容範囲)
3. 更新エラー時の画面継続と最新データ表示
4. RefreshData後の再編集時のエラー解消
5. 送信パターンマスタの特殊更新処理対応

## 対象ファイル一覧
1. `ChatworkBulkSender\UserControls\AbstractBaseMasterDgvControl.cs`
2. `ChatworkBulkSender\Daos\CustomerMasterDao.cs`
3. `ChatworkBulkSender\Forms\M_CustomerMasterIndividualEdit.cs`
4. `ChatworkBulkSender\UserControls\CustomerIndividualEditControl.cs`
5. `ChatworkBulkSender\Daos\PatternMasterDao.cs`
6. `ChatworkBulkSender\Forms\M_SendPatternIndividualEdit.cs`

---

## 1. AbstractBaseMasterDgvControl.cs の修正

### ファイルパス
`ChatworkBulkSender\UserControls\AbstractBaseMasterDgvControl.cs`

### 修正箇所:RefreshData メソッド(160-180行目付近)

#### 修正前のコード:
```csharp
protected virtual void RefreshData()
{
    if (_masterDao == null) return;

    var list = GetDataList();
    if (list != null)
    {
        BindDataToGrid(list);
        
        // 初回ロード時のみ更新日時を記録
        if (!_isDataLoaded)
        {
            StoreOriginalUpdateTimes(list);
            _isDataLoaded = true;
        }
        
        _lastLoadTime = DateTime.Now;
    }
}
```

#### 修正後のコード:
```csharp
protected virtual void RefreshData()
{
    if (_masterDao == null) return;

    var list = GetDataList();
    if (list != null)
    {
        BindDataToGrid(list);
        
        // 更新日時の元データを最新に更新
        // エラー後のRefreshDataでも最新のデータで比較できるようにする
        StoreOriginalUpdateTimes(list);
        
        if (!_isDataLoaded)
        {
            _isDataLoaded = true;
        }
        
        _lastLoadTime = DateTime.Now;
    }
}
```

### 修正理由
RefreshData時に常に`_originalUpdateTimes`を更新することで、楽観的ロックエラー後に再度同じ行を開いてもエラーが繰り返されない問題を解決します。

---

## 2. CustomerMasterDao.cs の修正

### ファイルパス
`ChatworkBulkSender\Daos\CustomerMasterDao.cs`

### 修正箇所:Update メソッド(180-220行目付近)

#### 修正前のコード:
```csharp
public int Update(CustomerMasterDto dto, DateTime currentUpdatedAt)
{
    string sql = @"
        UPDATE dbo.顧客マスタ
        SET 顧客名 = @CustomerName,
            C_ルームID = @ChatworkRoomId,
            C_宛先アカウントID = @ChatworkAccountId,
            並び順 = @SortOrder,
            未使用フラグ = @IsUnused,
            更新日時 = @UpdatedAt,
            更新ユーザ = @UpdatedBy
        WHERE 管理番号 = @Id 
          AND 更新日時 = @CurrentUpdatedAt";

    var param = new Dictionary<string, object>
    {
        {"@Id", dto.ManagementNumber},
        {"@CustomerName", dto.CustomerName},
        {"@ChatworkRoomId", dto.ChatworkRoomId},
        {"@ChatworkAccountId", dto.ChatworkAccountId},
        {"@SortOrder", dto.SortOrder},
        {"@IsUnused", dto.IsUnused},
        {"@UpdatedAt", DateTime.Now},
        {"@UpdatedBy", Environment.UserName},
        {"@CurrentUpdatedAt", currentUpdatedAt}
    };

    return _db.ExecNonQuery(sql, param);
}
```

#### 修正後のコード:
```csharp
public int Update(CustomerMasterDto dto, DateTime currentUpdatedAt)
{
    string sql = @"
        UPDATE dbo.顧客マスタ
        SET 顧客名 = @CustomerName,
            C_ルームID = @ChatworkRoomId,
            C_宛先アカウントID = @ChatworkAccountId,
            並び順 = @SortOrder,
            未使用フラグ = @IsUnused,
            更新日時 = @UpdatedAt,
            更新ユーザ = @UpdatedBy
        WHERE 管理番号 = @Id 
          AND ABS(DATEDIFF(SECOND, 更新日時, @CurrentUpdatedAt)) <= 1";

    var param = new Dictionary<string, object>
    {
        {"@Id", dto.ManagementNumber},
        {"@CustomerName", dto.CustomerName},
        {"@ChatworkRoomId", dto.ChatworkRoomId},
        {"@ChatworkAccountId", dto.ChatworkAccountId},
        {"@SortOrder", dto.SortOrder},
        {"@IsUnused", dto.IsUnused},
        {"@UpdatedAt", DateTime.Now},
        {"@UpdatedBy", Environment.UserName},
        {"@CurrentUpdatedAt", currentUpdatedAt}
    };

    return _db.ExecNonQuery(sql, param);
}
```

### 修正理由
SQL ServerのDateTime2(7)とC#のDateTimeの精度差を吸収するため、1秒以内の差は同じとみなす処理を追加。

---

## 3. M_CustomerMasterIndividualEdit.cs の修正

### ファイルパス
`ChatworkBulkSender\Forms\M_CustomerMasterIndividualEdit.cs`

### 修正箇所1:BtnUpdate_Click メソッド(200-300行目付近)

#### 修正前のコード:
```csharp
private void BtnUpdate_Click(object sender, EventArgs e)
{
    try
    {
        // バリデーションチェック
        if (!ValidateInputs())
        {
            return;
        }

        var updatedCustomer = GetCustomerData();
        if (updatedCustomer == null) { return; }

        updatedCustomer.ManagementNumber = _customerId;
        
        if (_currentUpdatedAt == null)
        {
            MessageBox.Show("データの更新日時が取得できませんでした。", "エラー", 
                MessageBoxButtons.OK, MessageBoxIcon.Error);
            return;
        }
        
        var dao = new CustomerMasterDao();
        var updatedRows = dao.Update(updatedCustomer, _currentUpdatedAt.Value);
        
        if (updatedRows == 0)
        {
            MessageBox.Show(MessageBoxUtil.DB_011, "エラー", 
                MessageBoxButtons.OK, MessageBoxIcon.Warning);
            this.DialogResult = DialogResult.Cancel;
            this.Close();
            return;
        }
        
        if (updatedRows == 1)
        {
            var latestData = dao.GetById(_customerId);
            if (latestData != null)
            {
                _currentUpdatedAt = latestData.UpdatedAt;
                updatedCustomer.UpdatedAt = latestData.UpdatedAt;
            }
            
            DataUpdated?.Invoke(this, updatedCustomer);
            
            MessageBox.Show(MessageBoxUtil.DB_002, "情報", 
                MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
    }
    catch (Exception ex)
    {
        MessageBoxUtil.ShowErr($"{ex.Message}\n\n");
    }
}
```

#### 修正後のコード:
```csharp
private void BtnUpdate_Click(object sender, EventArgs e)
{
    try
    {
        // バリデーションチェック
        if (!ValidateInputs())
        {
            return;
        }

        var updatedCustomer = GetCustomerData();
        if (updatedCustomer == null) { return; }

        updatedCustomer.ManagementNumber = _customerId;
        
        if (_currentUpdatedAt == null)
        {
            MessageBox.Show("データの更新日時が取得できませんでした。", "エラー", 
                MessageBoxButtons.OK, MessageBoxIcon.Error);
            return;
        }
        
        var dao = new CustomerMasterDao();
        
        // 更新前に最新のデータを取得して、他のユーザーが更新していないか確認
        var currentDbData = dao.GetById(_customerId);
        
        if (currentDbData != null)
        {
            // DateTime精度問題対策:1秒以内の差は同じとみなす
            bool isUpdatedByOthers = false;
            if (currentDbData.UpdatedAt.HasValue && _currentUpdatedAt.HasValue)
            {
                var timeDiff = Math.Abs((currentDbData.UpdatedAt.Value - _currentUpdatedAt.Value).TotalSeconds);
                isUpdatedByOthers = timeDiff > 1.0;  // 1秒以上の差がある場合のみ更新とみなす
            }
            else if (currentDbData.UpdatedAt != _currentUpdatedAt)
            {
                isUpdatedByOthers = true;
            }
            
            if (isUpdatedByOthers)
            {
                MessageBox.Show(MessageBoxUtil.DB_011, "エラー", 
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                
                // 最新のデータで画面を更新
                _customerIndividualEdit.SetCustomerData(currentDbData);
                _currentUpdatedAt = currentDbData.UpdatedAt;
                _changeDetector.ResetChanges();
                return;
            }
        }
        
        var updatedRows = dao.Update(updatedCustomer, _currentUpdatedAt.Value);
        
        if (updatedRows == 0)
        {
            // 楽観的ロックエラー
            MessageBox.Show(MessageBoxUtil.DB_011, "エラー", 
                MessageBoxButtons.OK, MessageBoxIcon.Warning);
            
            // 最新のデータを取得して画面を更新
            var latestData = dao.GetById(_customerId);
            if (latestData != null)
            {
                _customerIndividualEdit.SetCustomerData(latestData);
                _currentUpdatedAt = latestData.UpdatedAt;
                _changeDetector.ResetChanges();
            }
            return;
        }
        
        if (updatedRows == 1)
        {
            // 更新成功後、最新のデータを取得して更新日時を更新
            var latestData = dao.GetById(_customerId);
            if (latestData != null)
            {
                _currentUpdatedAt = latestData.UpdatedAt;
                updatedCustomer.UpdatedAt = latestData.UpdatedAt;
            }
            
            DataUpdated?.Invoke(this, updatedCustomer);
            
            MessageBox.Show(MessageBoxUtil.DB_002, "情報", 
                MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
    }
    catch (Exception ex)
    {
        MessageBoxUtil.ShowErr($"{ex.Message}\n\n");
    }
}
```

### 修正箇所2:FormClosing イベント(350-380行目付近)

#### 修正前のコード:
```csharp
private void M_CustomerMasterIndividualEdit_FormClosing(object sender, FormClosingEventArgs e)
{
    DialogResult result = MessageBox.Show("顧客マスタ 個別編集画面を終了してもよろしいですか?", 
        "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
    
    if (result == DialogResult.No)
    {
        e.Cancel = true;
        return;
    }
    
    // イベント登録の解除
    if (_btnMode == ButtonMode.Update)
    {
        _btnUpdate.BtnUpdateClicked -= BtnUpdate_Click;
        _btnUpdate.BtnReturnClicked -= BtnReturn_Click;
    }
    else
    {
        _btnRegister.BtnReturnClicked -= BtnReturn_Click;
    }

    this.DisposeViewControls();
    this.Owner.Show();
    this.Owner.Refresh();
}
```

#### 修正後のコード:
```csharp
private void M_CustomerMasterIndividualEdit_FormClosing(object sender, FormClosingEventArgs e)
{
    // 変更がある場合のみメッセージを表示
    if (_changeDetector != null && _changeDetector.HasChanges())
    {
        DialogResult result = MessageBox.Show("変更が保存されていません。終了してもよろしいですか?", 
            "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        
        if (result == DialogResult.No)
        {
            e.Cancel = true;
            return;
        }
    }
    
    // イベント登録の解除
    if (_btnMode == ButtonMode.Update)
    {
        _btnUpdate.BtnUpdateClicked -= BtnUpdate_Click;
        _btnUpdate.BtnReturnClicked -= BtnReturn_Click;
    }
    else
    {
        _btnRegister.BtnReturnClicked -= BtnReturn_Click;
    }

    this.DisposeViewControls();
    this.Owner.Show();
    this.Owner.Refresh();
}
```

---

## 4. CustomerIndividualEditControl.cs の修正

### ファイルパス
`ChatworkBulkSender\UserControls\CustomerIndividualEditControl.cs`

### 追加メソッド:SetCustomerData(ファイル末尾に追加)

```csharp
/// <summary>
/// 顧客データを画面に設定する
/// </summary>
/// <param name="customer">設定する顧客データ</param>
public void SetCustomerData(CustomerMasterDto customer)
{
    if (customer == null) return;
    
    txtMgmtNumber.Text = customer.ManagementNumber.ToString();
    txtCustomerName.Text = customer.CustomerName;
    txtChatworkRoomId.Text = customer.ChatworkRoomId;
    txtChatworkAccountId.Text = customer.ChatworkAccountId;
    txtSortOrder.Text = customer.SortOrder.ToString();
    chkUnusedFlg.Checked = customer.IsUnused;
}
```

---

## 5. PatternMasterDao.cs の修正

### ファイルパス
`ChatworkBulkSender\Daos\PatternMasterDao.cs`

### 修正箇所1:Update メソッドのオーバーロード追加(247行目付近)

#### 追加するコード:
```csharp
/// <summary>
/// 送信パターンを更新(既存データの論理削除+新規データのINSERT)
/// </summary>
/// <returns>新しい送信パターンID。エラーの場合は-1</returns>
public int Update(PatternMasterDto dto, DateTime currentUpdatedDate)
{
    return Update(dto, currentUpdatedDate, null);
}
```

### 修正箇所2:Update メソッドの実装(257-392行目)

#### 修正前のコード:
```csharp
public int Update(PatternMasterDto dto, DateTime currentUpdatedDate)
{
    // 1. まず更新が可能かチェック
    string checkSql = @"
        SELECT COUNT(*)
        FROM dbo.送信パターンマスタ
        WHERE 送信パターンID = @PatternId
          AND 削除フラグ = @CurrentIsDeleted
          AND 更新日時 = @CurrentUpdatedDate";

    var checkParam = new Dictionary<string, object>
    {
        {"@PatternId", dto.PatternId},
        {"@CurrentIsDeleted", Constants.DELETE_FLAG.NON_DELETED},
        {"@CurrentUpdatedDate", currentUpdatedDate}
    };

    var checkResult = _db.ExecuteScalar(checkSql, checkParam);
    if (checkResult == null || (int)checkResult != 1)
    {
        return -1; // 楽観的ロック
    }

    // 2. 既存レコードを論理削除するSQL
    string updateSql = @"
        UPDATE dbo.送信パターンマスタ
        SET 削除フラグ = @IsDeleted,
            更新日時 = @UpdatedDate,
            更新ユーザ = @UpdatedBy
        WHERE 送信パターンID = @PatternId
          AND 削除フラグ = @CurrentIsDeleted
          AND 更新日時 = @CurrentUpdatedDate";

    // 以下、新規INSERT処理...
}
```

#### 修正後のコード:
```csharp
/// <summary>
/// 送信パターンを更新(既存データの論理削除+新規データのINSERT)
/// 選択された顧客データも同時に更新
/// </summary>
/// <returns>新しい送信パターンID。エラーの場合は-1</returns>
public int Update(PatternMasterDto dto, DateTime currentUpdatedDate, List<int> selectedCustomerIds)
{
    // 1. まず更新が可能かチェック(DateTime精度問題対策:1秒の許容範囲)
    string checkSql = @"
        SELECT COUNT(*)
        FROM dbo.送信パターンマスタ
        WHERE 送信パターンID = @PatternId
          AND 削除フラグ = @CurrentIsDeleted
          AND ABS(DATEDIFF(SECOND, 更新日時, @CurrentUpdatedDate)) <= 1";

    var checkParam = new Dictionary<string, object>
    {
        {"@PatternId", dto.PatternId},
        {"@CurrentIsDeleted", Constants.DELETE_FLAG.NON_DELETED},
        {"@CurrentUpdatedDate", currentUpdatedDate}
    };

    var checkResult = _db.ExecuteScalar(checkSql, checkParam);
    if (checkResult == null || (int)checkResult != 1)
    {
        return -1; // 楽観的ロックエラー
    }

    // 2. 既存レコードを論理削除するSQL(DateTime精度問題対策:1秒の許容範囲)
    string updateSql = @"
        UPDATE dbo.送信パターンマスタ
        SET 削除フラグ = @IsDeleted,
            更新日時 = @UpdatedDate,
            更新ユーザ = @UpdatedBy
        WHERE 送信パターンID = @PatternId
          AND 削除フラグ = @CurrentIsDeleted
          AND ABS(DATEDIFF(SECOND, 更新日時, @CurrentUpdatedDate)) <= 1";

    var updateParam = new Dictionary<string, object>
    {
        {"@IsDeleted", Constants.DELETE_FLAG.DELETED},
        {"@UpdatedDate", DateTime.Now},
        {"@UpdatedBy", dto.UpdateBy},
        {"@PatternId", dto.PatternId},
        {"@CurrentIsDeleted", Constants.DELETE_FLAG.NON_DELETED},
        {"@CurrentUpdatedDate", currentUpdatedDate}
    };

    // 3. 新規レコードをINSERTするSQL
    string insertSql = @"
        INSERT INTO dbo.送信パターンマスタ(
            送信パターン名称,
            定型文,
            送信対象,
            並び順,
            未使用フラグ,
            削除フラグ,
            登録日時,
            登録ユーザ,
            更新日時,
            更新ユーザ
        )
        VALUES(
            @PatternName,
            @TemplateText,
            @Target,
            @SortOrder,
            @IsUnused,
            @IsDeleted,
            @CreatedDate,
            @CreatedBy,
            @UpdatedDate,
            @UpdatedBy
        )";

    var insertParam = new Dictionary<string, object>
    {
        {"@PatternName", dto.PatternName},
        {"@TemplateText", dto.TemplateText},
        {"@Target", dto.Target},
        {"@SortOrder", dto.SortOrder},
        {"@IsUnused", dto.IsUnused},
        {"@IsDeleted", Constants.DELETE_FLAG.NON_DELETED},
        {"@CreatedDate", DateTime.Now},
        {"@CreatedBy", dto.UpdateBy},
        {"@UpdatedDate", dto.UpdatedDate},
        {"@UpdatedBy", dto.UpdateBy}
    };

    // 4. トランザクションで実行
    var commands = new List<(string Sql, Dictionary<string, object> Parameters)>
    {
        (updateSql, updateParam),
        (insertSql, insertParam)
    };

    int result = _db.ExecNonQuery(commands);

    // 5. 新しく挿入されたパターンIDを取得
    if (result >= 2) // 1行更新 + 1行挿入
    {
        var latestPattern = GetLatestPattern();
        if (latestPattern != null)
        {
            // 6. 選択された顧客データを新しいパターンIDに紐づけ
            if (selectedCustomerIds != null && selectedCustomerIds.Count > 0)
            {
                foreach (var customerId in selectedCustomerIds)
                {
                    string sqlCustomer = @"
                        INSERT INTO dbo.送信パターンマスタ_顧客(
                            送信パターンID,
                            管理番号,
                            登録日時,
                            登録ユーザ
                        )
                        VALUES(
                            @PatternId,
                            @ManagementNumber,
                            @CreatedDate,
                            @CreatedBy
                        )";

                    var paramCustomer = new Dictionary<string, object>
                    {
                        {"@PatternId", latestPattern.PatternId},
                        {"@ManagementNumber", customerId},
                        {"@CreatedDate", DateTime.Now},
                        {"@CreatedBy", Environment.UserName}
                    };

                    _db.ExecNonQuery(sqlCustomer, paramCustomer);
                }
            }
            return latestPattern.PatternId;
        }
        return -1;
    }

    return -1;
}
```

---

## 6. M_SendPatternIndividualEdit.cs の修正

### ファイルパス
`ChatworkBulkSender\Forms\M_SendPatternIndividualEdit.cs`

### 修正箇所1:BtnUpdate_Click メソッド(211-330行目)

#### 修正前のコード:
```csharp
private void BtnUpdate_Click(object sender, EventArgs e)
{
    try
    {
        // バリデーションチェック
        if (!ValidateInputs())
        {
            return;
        }

        var updatedPattern = GetPatternData();
        if (updatedPattern == null) { return; }
        
        updatedPattern.PatternId = _patternId;
        
        if (_currentUpdatedDate == null)
        {
            MessageBox.Show("データの更新日時が取得できませんでした。", "エラー", 
                MessageBoxButtons.OK, MessageBoxIcon.Error);
            return;
        }
        
        var dao = new PatternMasterDao();
        var newPatternId = dao.Update(updatedPattern, _currentUpdatedDate.Value);
        
        if (newPatternId == -1)
        {
            MessageBox.Show(MessageBoxUtil.DB_011, "エラー", 
                MessageBoxButtons.OK, MessageBoxIcon.Warning);
            this.DialogResult = DialogResult.Cancel;
            this.Close();
            return;
        }
        
        // 成功処理...
    }
    catch (Exception ex)
    {
        MessageBoxUtil.ShowErr($"{ex.Message}\n\n");
    }
}
```

#### 修正後のコード:
```csharp
private void BtnUpdate_Click(object sender, EventArgs e)
{
    try
    {
        // バリデーションチェック
        if (!ValidateInputs())
        {
            return;
        }

        // 選択された顧客IDを取得
        if (_selectedCustomerIds == null)
        {
            _selectedCustomerIds = new List<int>();
        }
        else
        {
            _selectedCustomerIds.Clear();
        }
        
        var dgvDestinationList = _sendContent.Controls.Find("dgvDestinationList", true).FirstOrDefault() as DataGridView;
        if (dgvDestinationList != null)
        {
            foreach (DataGridViewRow row in dgvDestinationList.Rows)
            {
                if (row.Cells["送信対象"].Value != null && (bool)row.Cells["送信対象"].Value)
                {
                    _selectedCustomerIds.Add(Convert.ToInt32(row.Cells["管理番号"].Value));
                }
            }
        }

        var updatedPattern = GetPatternData();
        if (updatedPattern == null) { return; }
        
        // パターンIDを設定
        updatedPattern.PatternId = _patternId;
        
        // 更新日時がない場合はエラー
        if (_currentUpdatedDate == null)
        {
            MessageBox.Show("データの更新日時が取得できませんでした。", "エラー", 
                MessageBoxButtons.OK, MessageBoxIcon.Error);
            return;
        }
        
        var dao = new PatternMasterDao();
        
        // 更新前に最新のデータを取得して、他のユーザーが更新していないか確認
        var currentDbData = dao.GetById(_patternId);
        
        if (currentDbData != null)
        {
            // DateTime精度問題対策:1秒以内の差は同じとみなす
            bool isUpdatedByOthers = false;
            if (currentDbData.UpdatedDate.HasValue && _currentUpdatedDate.HasValue)
            {
                var timeDiff = Math.Abs((currentDbData.UpdatedDate.Value - _currentUpdatedDate.Value).TotalSeconds);
                isUpdatedByOthers = timeDiff > 1.0;  // 1秒以上の差がある場合のみ更新とみなす
            }
            else if (currentDbData.UpdatedDate != _currentUpdatedDate)
            {
                isUpdatedByOthers = true;
            }
            
            if (isUpdatedByOthers)
            {
                // 他のユーザーが既に更新している
                MessageBox.Show(MessageBoxUtil.DB_011, "エラー", 
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                
                // 最新のデータで画面を更新
                SetPatternData(currentDbData);
                return;
            }
        }
        
        // 選択された顧客IDを含めて更新
        var newPatternId = dao.Update(updatedPattern, _currentUpdatedDate.Value, _selectedCustomerIds);
        
        if (newPatternId == -1)
        {
            // 楽観的ロックエラー
            MessageBox.Show(MessageBoxUtil.DB_011, "エラー", 
                MessageBoxButtons.OK, MessageBoxIcon.Warning);
            
            // 最新のデータを取得して画面を更新
            var latestData = dao.GetById(_patternId);
            if (latestData != null)
            {
                SetPatternData(latestData);
            }
            return;
        }
        
        if (newPatternId > 0)
        {
            // 更新成功後、最新のデータを取得して更新日時を更新
            var latestData = dao.GetById(newPatternId);
            if (latestData != null)
            {
                _currentUpdatedDate = latestData.UpdatedDate;
                updatedPattern.UpdatedDate = latestData.UpdatedDate;
            }
            
            // 更新成功
            updatedPattern.PatternId = newPatternId;
            DataUpdated?.Invoke(this, updatedPattern);
            
            MessageBox.Show(MessageBoxUtil.DB_002, "情報", 
                MessageBoxButtons.OK, MessageBoxIcon.Information);
            
            // 新しいパターンIDを保持
            _patternId = newPatternId;
        }
    }
    catch (Exception ex)
    {
        MessageBoxUtil.ShowErr($"{ex.Message}\n\n");
    }
}
```

### 修正箇所2:SetPatternData メソッド(389-408行目)

#### 追加するコード(メソッドが存在しない場合は新規追加):
```csharp
/// <summary>
/// パターンデータを画面に設定する
/// </summary>
/// <param name="pattern">設定するパターンデータ</param>
public void SetPatternData(PatternMasterDto pattern)
{
    if (pattern == null) return;
    
    var patternName = _patternNameHeader.Controls.Find("txtPatternName", false).FirstOrDefault() as TextBox;
    var templateTxt = _sendContent.Controls.Find("txtContents", true).FirstOrDefault() as TextBox;
    var sortOrder = _patternNameHeader.Controls.Find("txtSortOrder", false).FirstOrDefault() as TextBox;
    var isUnused = _patternNameHeader.Controls.Find("chkUnusedFlg", false).FirstOrDefault() as CheckBox;
    
    if (patternName != null) patternName.Text = pattern.PatternName;
    if (templateTxt != null) templateTxt.Text = pattern.TemplateText;
    if (sortOrder != null) sortOrder.Text = pattern.SortOrder.ToString();
    if (isUnused != null) isUnused.Checked = pattern.IsUnused;
    
    // パターンIDと更新日時も更新
    _patternId = pattern.PatternId;
    _currentUpdatedDate = pattern.UpdatedDate;
}
```

### 修正箇所3:FormClosing イベント(337-353行目)

#### 修正前のコード:
```csharp
private void M_SendPatternIndividualEdit_FormClosing(object sender, FormClosingEventArgs e)
{
    DialogResult result = MessageBox.Show("送信パターンマスタ 個別編集画面を終了してもよろしいですか?", 
        "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

    if (result == DialogResult.No)
    {
        e.Cancel = true;
        return;
    }

    // イベント登録の解除と終了処理...
}
```

#### 修正後のコード:
```csharp
private void M_SendPatternIndividualEdit_FormClosing(object sender, FormClosingEventArgs e)
{
    // 変更がない場合はメッセージを出さない
    // TODO: 変更検知機能を実装する場合はここで判定
    // 現時点では常にメッセージを出さない
    if (false) // 将来的に変更検知を実装する場合はここを修正
    {
        DialogResult result = MessageBox.Show("送信パターンマスタ 個別編集画面を終了してもよろしいですか?", 
            "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

        if (result == DialogResult.No)
        {
            e.Cancel = true;
            return;
        }
    }

    // イベント登録の解除
    if (_btnMode == ButtonMode.Update)
    {
        _btnUpdate.BtnUpdateClicked -= BtnUpdate_Click;
        _btnUpdate.BtnReturnClicked -= BtnReturn_Click;
    }
    else
    {
        _btnRegister.BtnReturnClicked -= BtnReturn_Click;
    }

    this.DisposeViewControls();
    this.Owner.Show();
    this.Owner.Refresh();
}
```

---

## テスト手順

### 1. 顧客マスタのテスト

#### 1.1 基本的な楽観的ロックテスト
1. 顧客マスタ画面を開く
2. 任意の顧客データの編集ボタンをクリックして編集画面を開く
3. 別のユーザー(またはSQL Server Management Studio)で同じデータを更新
4. 編集画面で更新ボタンをクリック
5. 「他のユーザーが更新しました」エラーが表示されることを確認
6. エラーメッセージ確認後、画面が閉じずに最新データが表示されることを確認
7. 再度更新ボタンをクリックして、正常に更新できることを確認

#### 1.2 RefreshData後の再編集テスト
1. 顧客マスタ一覧画面で任意のデータの編集ボタンをクリック
2. 別のユーザーが同じデータを更新
3. 編集画面で更新ボタンをクリックしてエラーを確認
4. 戻るボタンで一覧画面に戻る
5. 同じ行の編集ボタンを再度クリック
6. エラーが表示されずに正常に開けることを確認
7. データを編集して更新ボタンをクリック
8. 正常に更新できることを確認

### 2. 送信パターンマスタのテスト

#### 2.1 基本的な楽観的ロックテスト
1. 送信パターンマスタ画面を開く
2. 任意のパターンデータの編集ボタンをクリックして編集画面を開く
3. 顧客選択がある場合は任意の顧客を選択
4. 別のユーザー(またはSQL Server Management Studio)で同じデータを更新
5. 編集画面で更新ボタンをクリック
6. 「他のユーザーが更新しました」エラーが表示されることを確認
7. エラーメッセージ確認後、画面が閉じずに最新データが表示されることを確認
8. 再度更新ボタンをクリックして、正常に更新できることを確認

#### 2.2 顧客データ移行テスト
1. 送信パターンマスタで顧客が紐づいているパターンを編集
2. 顧客選択を変更(追加または削除)
3. 更新ボタンをクリック
4. データベースで新しいパターンIDが生成されていることを確認
5. 新しいパターンIDに選択した顧客が紐づいていることを確認

### 3. DateTime精度テスト

#### 3.1 高速更新テスト
1. 編集画面を開く
2. すぐに更新ボタンをクリック(1秒以内)
3. エラーが表示されずに正常に更新できることを確認

---

## 実装チェックリスト

### ファイル修正チェックリスト
- [ ] AbstractBaseMasterDgvControl.cs - RefreshDataメソッドの修正
- [ ] CustomerMasterDao.cs - UpdateメソッドのDateTime精度対応
- [ ] M_CustomerMasterIndividualEdit.cs - BtnUpdate_Clickの修正
- [ ] M_CustomerMasterIndividualEdit.cs - FormClosingの修正
- [ ] CustomerIndividualEditControl.cs - SetCustomerDataメソッドの追加
- [ ] PatternMasterDao.cs - UpdateメソッドのDateTime精度対応
- [ ] PatternMasterDao.cs - Updateメソッドのオーバーロード追加
- [ ] M_SendPatternIndividualEdit.cs - BtnUpdate_Clickの修正
- [ ] M_SendPatternIndividualEdit.cs - SetPatternDataメソッドの追加
- [ ] M_SendPatternIndividualEdit.cs - FormClosingの修正

### 動作確認チェックリスト
- [ ] 顧客マスタの楽観的ロック動作
- [ ] 顧客マスタのRefreshData後の再編集
- [ ] 顧客マスタの画面継続動作
- [ ] 送信パターンマスタの楽観的ロック動作
- [ ] 送信パターンマスタのRefreshData後の再編集
- [ ] 送信パターンマスタの画面継続動作
- [ ] 送信パターンマスタの顧客データ移行
- [ ] DateTime精度(1秒許容)の動作

---

## 注意事項

### 1. DateTime精度問題
- SQL ServerのDateTime2(7)は7桁のナノ秒精度
- C#のDateTimeは100ナノ秒(7桁)精度
- 精度差により完全一致比較では失敗する可能性がある
- 解決策:`ABS(DATEDIFF(SECOND, 更新日時, @CurrentUpdatedAt)) <= 1`で1秒以内の差を許容

### 2. 送信パターンマスタの特殊性
- 更新時に既存レコードを論理削除(削除フラグ=1)
- 新規レコードをINSERT(新しいパターンIDが生成)
- 顧客データの紐づけも新しいパターンIDに移行する必要がある
- 履歴保持のための設計仕様

### 3. 画面の継続処理
- 楽観的ロックエラー時は画面を閉じない
- 最新データで画面を更新して操作を継続可能にする
- ユーザビリティの向上

### 4. 変更検知機能
- 現時点では完全実装されていない
- FormClosingでのメッセージ表示は条件付き
- 将来的に変更検知機能を実装する場合は該当箇所を修正

### 5. RefreshData問題
- 初回ロード時のみ_originalUpdateTimesを更新していた問題
- RefreshData時に常に更新することで解決
- エラー後の再編集でもエラーが繰り返されない

---

## 実装完了後の確認事項

1. **コンパイル確認**
   - ビルドエラーがないことを確認
   - 警告がある場合は内容を確認

2. **データベース確認**
   - 顧客マスタテーブルの更新日時が正しく更新されること
   - 送信パターンマスタの論理削除と新規INSERTが正しく動作すること
   - 送信パターンマスタ_顧客テーブルのデータ移行が正しいこと

3. **ログ確認**
   - エラーログが出力されていないこと
   - 必要に応じてデバッグログを確認

4. **パフォーマンス確認**
   - 更新処理が遅延なく動作すること
   - データベースアクセスが効率的であること

---

## トラブルシューティング

### 問題1:更新が常に失敗する
**原因**:DateTime精度問題
**解決**:SQLのWHERE句で1秒許容範囲を設定しているか確認

### 問題2:画面が閉じてしまう
**原因**:DialogResult.CancelやClose()が残っている
**解決**:エラー時の処理でSetCustomerData/SetPatternDataを使用しているか確認

### 問題3:RefreshData後も古いデータで比較される
**原因**:_originalUpdateTimesが更新されていない
**解決**:RefreshDataメソッドでStoreOriginalUpdateTimesが呼ばれているか確認

### 問題4:送信パターンの顧客データが消える
**原因**:新しいパターンIDへの移行処理が漏れている
**解決**:Update メソッドでselectedCustomerIdsの処理が実装されているか確認

---

## 変更履歴

| 日付 | 内容 | 対応者 |
|------|------|--------|
| 2024/XX/XX | 初版作成 | - |
| 2024/XX/XX | DateTime精度問題対応追加 | - |
| 2024/XX/XX | RefreshData問題対応追加 | - |
| 2024/XX/XX | 送信パターンマスタ顧客データ移行対応追加 | - |

---

## 関連ドキュメント

- データベース設計書
- 画面設計書
- 楽観的ロック設計書
- MessageBoxUtil定数定義書

---

以上