본문 바로가기
C#/Winform (.Net Framework)

C# - SQL Server 연동하기 (Chapter.2 - DataAdapter 활용)

by 은메달 수집가 2024. 12. 2.
아래 내용은 2015년 1월 9일에 다른 곳에 작성했던 내용을 블로그로 옮겨온 것으로 현재 코드와는 조금 다를 수 있습니다. (일부 테스트하고 변경된 것은 다시 작성해서 업데이트했습니다.)
2024년 현재 테스트 시 사용한 SW 버전
- Visual Studio 2022 Pro 
  ㆍWindows Forms App (.Net Framework 4.7.2)
- SQL Server 2022 Express
- SQL Server 2019 Management Studio

명령 객체는 서버에게 명령을 전달하고 이 명령은 서버 내에서 실행되어 결과 셋으로 리턴되며 클라이언트는 리더를 통해 서버의 결과 셋을 순서대로 읽는다. , SqlConnection, SqlDataReader와 같은 클래스들은 서버와의 연결을 유지한 상태에서 실행하는 연결형이다.

Ado.Net의 특징 중 하나인 비연결형을 지원하는 주체가 바로 DataAdpter이다. 그림으로 설명이 되어 있는데 살펴보면 바로 감이 온다.

이전에 봤을 때 DataSet은 메모리에 존재하는 하나의 DB라고 했는데, ServerDataSet 중간에서 이 어댑터가 실질적으로 필요할 때에 서버와의 통신을 통해 데이터를 갖고 오거나 서버에 명령을 보내는 역할을 수행하는 것이다.

이런 SqlDataAdapter의 생성자는 아래와 같다.

SqlDataAdepter(String SelectCommandText, SqlConnection connection)

첫 번째 인수인 CommandText는 어떤 데이터를 가져올 것인가 지정하는 명령어이다. 비연결형이므로 동작을 수행하기 위해서는 서버에서 어떤 데이터를 가져오라고 명령을 지정해 줘야 가져올 수 있기 때문이다. (쉽게 말해 SQL 명령문)

그리고 두 번째 인수로 Connection 정보를 전달하는데 여기서 한 가지 정보가 있다면 Connection의 정보를 OpenOpen 할 필요 없이 전달해 줘도 된다는 것이다. 똑똑하게도 이 아이가 스스로 Connection이 닫혀 있으면 열고 데이터를 가져와서 다시 바로 닫는다. 내가 불필요하게 컨넥션으로 작업을 일일이 할 부담이 적어지는 것이다.

그리고 이렇게 명령어와 컨넥션 정보를 전달해 생성을 시키면 서버로부터 데이터를 바로 가져오는 것은 아니다. 어댑터는 데이터를 가져올 준비만 해두며 실제로 데이터를 가져올 때는 Fill 메서드를 이용해서 데이터 집합을 채워야 한다.

Int Fill(DataTable tbl)

Int Fill(DataSet dataset)

Int Fill(DataSet dataset, String tblName)

DataTable 객체를 전달하여 해당 객체 안에 서버로부터 가져온 결과를 바로 채우거나, DataSet 객체를 전달하여 그 안에 저장시킬 수도 있다. 여기서 중요한 것은 Fill 메서드를 통해서 DataSet 혹은 DataTable 객체에 서버의 데이터를 채워준다는 개념이다.

이전 포스팅 샘플로 작업했던 코드를 AdapterDataTable을 이용해서 가져온 후 DataGridView 컨트롤에 뿌려주고 추가적으로 유저의 이름을 필터링하여 또 다른 결과를 다른 DataGridView 컨트롤에 뿌려주는 예시를 작성해 보았다..

폼 예시는 이전 포스팅과 같다. 그저 코드만 바뀌었다.

public partial class DataAdapterTestForm : Form
{
    private SqlDataAdapter userAdapter;
    private SqlDataAdapter paymentAdapter;
    private DataTable userTable;
    private DataTable paymentTable;
    private SqlConnection con;

    public DataAdapterTestForm()
    {
        InitializeComponent();
    }

    private void DataAdapterTestForm_Load(object sender, EventArgs e)
    {
        con = new SqlConnection("Server=(local);database=DBConnectTest;Trusted_Connection=True");

        userAdapter = new SqlDataAdapter("Select * From [User]", con);
        userTable = new DataTable("user");

        userAdapter.Fill(userTable);

        this.dgvUserTable.DataSource = userTable;
    }

    /// <summary>
    /// FindPay 버튼 클릭 이벤트
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void btnFindPay_Click(object sender, EventArgs e)
    {
        // 좌측에 있는 user DataGridView 컨트롤에서
        // 선택한 값이 있을 때에만
        if (this.dgvUserTable.SelectedCells.Count > 0)
        {
            int selectedRow = this.dgvUserTable.SelectedCells[0].RowIndex;
            // 등록된 데이터 범위 이내 일 때만
            if (selectedRow < userTable.Rows.Count)
            {
                // 선택된 cell이 위치한 행의 user idx 값을 가져옴.
                int userIdx = (int)this.dgvUserTable[0, selectedRow].Value;

                paymentAdapter = new SqlDataAdapter("Select * From [Payment] where uidx = " + userIdx, con);
                paymentTable = new DataTable("Payment");

                paymentAdapter.Fill(paymentTable);  
                this.dgvPaymentTable.DataSource = paymentTable;
            }
        }
    }
}

Connection 객체를 생성시키는 것은 너무 쉽지만, 보면 DataAdapter 객체를 생성시키기 전에 컨넥션 객체를 Open()하는 것을 찾아볼 수 없다. 위에서도 말했지만, DataAdapter 객체는 컨넥션이 닫혀 있으면 자기가 알아서 스스로 열고, 데이터를 가져온 이후에는 다시 연결 정보를 끊어준다.

그 후, DataTableFill 메서드를 통해서 데이터를 채워주고 Source 원본으로 지정하는 것을 볼 수 있다.

데이터 조회를 위한 DataAdapter를 사용하는 것은 여기까지 살펴보았다. Adapter 객체 생성하고, DB로부터 받아온 데이터를 담을 DataTable 혹은 DataSet에 Fill 메서드로 채우는 것까지 간단하다.  

그렇다면, Adapter를 활용한 데이터 업데이트는 할 수 없는 것일까?

사용자가 암만 데이터를 마구잡이로 수정을 해도 결과론 적으로 서버에 명령을 보내지 않으면, 아무런 소용이 없다. 그 이유는 이전 포스팅과 위에서도 언급했듯이 비연결형이기 때문에 사용자가 변경한 데이터는 사실 메모리에 있는 데이터를 수정하는 것이지 실질적인 서버의 데이터를 수정하는 것이 아니기 때문이다.

그래서 정확하게 서버에 데이터가 업데이트 되었으니 확인해 달라는 명령을 보내주어야 하는데, 이렇게 하기 위해서는 Update 메서드를 호출하면 된다. 그러나 그전에 SelectCommand, InsertCommand, DeleteCommand, UpdateCommand 등의 귀찮은 프로퍼티들을 모두 세팅을 해주어야 올바르게 동작할 수 있는데, 기본키가 있는 테이블에 한해서는 그냥 SqlCommandBuilder 클래스를 이용해서 생성시켜 주면 알아서 업데이트가 되고, 삭제도 된다.

좌측의 DataGridView에서 사용자가 값을 입력(기본키인 idx 컬럼 제외 ^^)하고 [Save] 버튼을 클릭하면 데이터베이스에 저장되게끔 하는 예시를 작성해 봤다. 데이터 수정/삭제/추가 등의 작업을 DB에 반영하기 위해서는 각 명령어에 해당하는 Command Builder 객체를 생성하고 DataAdapter 객체에 할당을 시켜주어야 한다. 그러나 복잡하지 않은 DB 테이블 원본 그대로를 사용하고 있는 상태에서는 SqlCommandBuilder 객체만 생성시켜 줘도 문제없이 동작한다.

private SqlDataAdapter userAdapter;
private SqlDataAdapter paymentAdapter;
private SqlCommandBuilder userBuilder;
private DataTable userTable;
private DataTable paymentTable;
private SqlConnection con;

public DataAdapterTestForm()
{
    InitializeComponent();
}

private void DataAdapterTestForm_Load(object sender, EventArgs e)
{
    con = new SqlConnection("Server=(local);database=DBConnectTest;Trusted_Connection=True");

    userAdapter = new SqlDataAdapter("Select * From [User]", con);
    userBuilder = new SqlCommandBuilder(userAdapter);
    userTable = new DataTable("user");

    userAdapter.Fill(userTable);

    this.dgvUserTable.DataSource = userTable;
}
/// <summary>
/// Save 버튼 클릭
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSave_Click(object sender, EventArgs e)
{
    this.userAdapter.Update(this.userTable);
    MessageBox.Show("Success!", "", MessageBoxButtons.OK, MessageBoxIcon.Information);
}

이렇게 코드를 변경하고 실제로 좌측의 DataGridView에서 이름을 일부 변경하고 [Save] 버튼을 클릭하면 해당 내용이 DB에 정확하게 반영되는 것을 확인할 수 있다. 

그런데 말입니다. 여기서 한 가지 궁금증이 생겨서 테스트를 해봤다. Save 버튼의 클릭 이벤트 코드를 아래와 같이 한 번 바꿨다. 추가된 건 딱 한 줄!

/// <summary>
/// Save 버튼 클릭
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSave_Click(object sender, EventArgs e)
{
    this.userTable.AcceptChanges();
    this.userAdapter.Update(this.userTable);
    MessageBox.Show("Success!", "", MessageBoxButtons.OK, MessageBoxIcon.Information);
}

이렇게 변경하고 실행을 해본 결과 정상적으로 DB에 저장이 됐을까?

서버의 데이터는 변경이 되지 않는다.

그 이유는 서버에 데이터를 변경할 때 변경여부를 처리하는 원리가 DataTable 객체에 있는 각각의 DataRow마다 존재하는 RowState를 이용해서 처리를 하기 때문이다. 상태가 변경이 되었는지, 삭제가 되었는지, 새로 추가된 것인지 등의 모든 상태가 모두 RowState에 저장이 되게 되는데, AcceptChanges를 해버리면 이 상태가 모두 최신 상태로 변경이 되어버리고 결과론적으로는 RowState가 모두 UnChanged. , 변경 사항이 없다고 되어버려 서버에는 데이터 업데이트를 진행하지 않는다.

그렇기 때문에 서버에 데이터를 업데이트할 때는 AcceptChanges를 하지 않고 곧바로 Update 명령을 보내주어야 한다. 어차피 이 명령을 보내도 메모리 DB에서도 알아서 커밋이 되니 상관이 없다.

728x90