요 근래 C#으로 데이터 베이스를 다루는 작업이 늘어가고 있다.

그런데, 단순히 Select / Insert / Update / Delete의 문제는 단순 쿼리 작성으로 해결은 할 수 있었는데,
문제는 변경된 사항만 적용하는 점이다.

 

Select를 해서 가져온 DataTable을 Grid 같은 곳에 DataSource로 붙인 것 까지는 좋은데,
이 Grid에서 데이터를 변경해서 나온 값을 가지고 뭔가 하려고 하니, 도데체 무엇이 변경되었는지를
찾는게 하나의 일이 되버렸다.

 

그렇다고, 전수 검사하는 것도 웃기기도 하고...

 

하지만, 태생이 DB 출신인 DataTable이 변경점 하나 없이 동작한다는 것은 말이 안될 것 같고 해서 이리 저리 사이트 돌아다면서 확인해보니, 역시 있었다.

 

DataTable.GetChanges()

 

이 메소드를 사용하면, DataTable 내의 데이터 변경 점을 모두 가져올 수 있었다.

 

사용방법은 아래와 같다.

 

준비

 

먼저 GetChanges를 호출하는 방법부터 확인하자.

 

이를 위해서 데이터를 준비한다. (여기서의 모든 예제는 SQLite를 사용한다. )

 

 

 

SQLiteConnectionStringBuilder sbConnectionString = new SQLiteConnectionStringBuilder();
sbConnectionString.DataSource = "test.db";
string sConnectionString = sbConnectionString.ToString();
SQLiteConnection conn = new SQLiteConnection(sConnectionString);
conn.Open();

// DB에 테이블을 생성
SQLiteCommand cmd = new SQLiteCommand(conn);
cmd.CommandType = CommandType.Text;
cmd.CommandText = "CREATE TABLE  IF NOT EXISTS T_CATEGORIES ( ID INTEGER PRIMARY KEY NOT NULL, NAME TEXT )";
cmd.ExecuteNonQuery();

// 테이블에 테스트 데이터 추가.
cmd = new SQLiteCommand(conn);
cmd.Transaction = transaction;
for (int i = 0; i < 100; i++)
{
	cmd.CommandType = CommandType.Text;
	cmd.CommandText = "INSERT INTO T_CATEGORIES(NAME) VALUES ( 'Category" + i.ToString() + "');";
	cmd.ExecuteNonQuery();
}

// 테이블의 내용을 Select
DataTable dtResult = new DataTable();
dtResult.AcceptChanges();
cmd = new SQLiteCommand(conn);
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT * FROM T_CATEGORIES";

// DataTable에 Select한 내용을 채우기.
SQLiteDataAdapter atapter = new SQLiteDataAdapter();
atapter.SelectCommand = cmd;
atapter.Fill(dtResult);

코드 상에서는 SQLite 개체를 쓰기는 했지만, OleDB가 되었던 MS SQL Server Client가 되었던,
요점은 ConnectionString을 구해와서 Connection을 맺고, Command를 만든 뒤, Adapter 개체를 생성해, DataTable에 Fill 하는 것이다.

 

일단 위의 코드를 사용하면, SQLite를 이용하여 test.db 안에 있는 T_CATEGORIES 라는 테이블의 모든 데이터를 dtResult 라는 곳에 채우는 것이다.

 

일단, dtResult 라는 DataTable을 만들었으면 조작을 시작해보자.

 

 

INSERT

 

데이터를 추가해보자.

DataRow newRow = dtResult.NewRow();
newRow["ID"] = dtResult.Rows.Count;
newRow["NAME"] = "New Category!";
dtResult.Rows.Add(newRow);

데이터 추가 로직은 간단하다. NewRow() 메소드로 DataTable의 Column에 맞게 구성된 DataRow를 확보한 뒤, 그 안에다 적절한 데이터를 넣는다. 그리고 난 뒤, RowCollection 에다 Add 를 한다.

 

이렇게 하면 dtResult라는 DataTable안에 새로운 데이터가 추가된다.

 

그렇다면 변경된 정보는 어떻게 확보할까?

그 때 GetChanges() 라는 메소드를 쓴다. GetChanges() 메소드를 사용하면, 전체 데이터 중에 변경점에 해당하는 값들을 모두 가져올 수 있다.

DataTable dtChanged = dtResult.GetChanges(DataRowState.Added);
foreach (DataRow row in dtChanged.Rows)
{
	if(row.RowState == DataRowState.Added)
	{
		foreach (DataColumn col in dtChanged.Columns)
		{
			Debug.Write(row[col].ToString());
			// 여기서 INSERT 구문을 작성한다.
	        }
		System.Diagnostics.Debug.WriteLine(" ");
	}
}

GetChanges()를 실행할 때, 파라미터를 하나 넣는데, 그 안에 넣는게 바로 DataRowStatus다.
만일 Insert 된 사항이 있으면 Added를 Update된 사항은 Modified, Delete된 사항이 있으면 Deleted를 아예 변경된적이 없는 경우에는 Unchanged를 넣으면 된다. 물론 복합적으로 사용도 된다. "|" 표시를 하면 복합적으로 검색해서 넣어준다.

DataTable dtChanged = dtResult.GetChanges(DataRowState.Added|DataRowState.Deleted| DataRowState.Modified);

변경 점들이 담긴 Table에서 Row들을 하나씩 꺼낸다. 이 정보를 이용해 INSERT 구문을 만들어 DataBase 상에 INSERT 해주면 된다.

 

 

UPDATE

 

INSERT와 매우 유사하다.

다만 틀린 것은 새로 추가된 사항이 아니고, 변경된 점 대비 이전 값이라는 것도 한번 더 체크해야 된다는 점이 다르다.

DataTable dtChanged = dtResult.GetChanges(DataRowState.Modified);
foreach (DataRow row in dtChanged.Rows)
{
    if (row.RowState == DataRowState.Modified)
    {
        object beforevalue = null;
        object aftervalue = null;
        foreach (DataColumn col in dtChanged.Columns)
        {
            beforevalue = row[col, DataRowVersion.Original];
            aftervalue = row[col, DataRowVersion.Current];

            // UPDATE 구문을 작성한다.
        }
        System.Diagnostics.Debug.WriteLine(" ");
    }                                
}

이전 값을 가져올 때는 DataRow의 배열에서 DataRowVersion 값까지 같이 넣어주면 된다. 
이 때, Original을 선택하면 변경 전의 값을 가져온다. 

단, 이 구문은 INSERT된 항목에서는 사용하면 안된다.
INSERT에서는 Original 값이라는 것이 없기 때문이다!

 

 

DELETE

 

이번에는 삭제된 값이다.
삭제된 값을 얻어오는 것도 위의 UPDATE 와 별다르지는 않다.
다만! 현재 값이 존재하지 않는다는게 문제다.

 

만일 아래와 같이 짜게 되면 Exception을 발견하게 된다.

DataTable dtChanged = dtResult.GetChanges(DataRowState.Deleted);
foreach (DataRow row in dtChanged.Rows)
{
    if (row.RowState == DataRowState.Deleted)
    {
        object beforevalue = null;
        object aftervalue = null;
        foreach (DataColumn col in dtChanged.Columns)
        {
            beforevalue = row[col, DataRowVersion.Original];
            aftervalue = row[col];    // 에러 발생!
            aftervalue = row[col, DataRowVersion.Current];    // 에러 발생!

            // DELETE 구문을 작성한다.
        }
        System.Diagnostics.Debug.WriteLine(" ");
    }                                
}

주석 중 "에러 발생!" 이라고 적은 부분이 있는데, row[col] 혹은 row[col, DataRowVersion.Current]를 하게 되면,

"Deleted row information cannot be accessed through the row" 라는 Exception이 발생한다.
이미 삭제했기 때문에, 현재 값이라는 것이 존재하지 않기 때문이다.

 

즉 aftervalue 라는 것 대신 before value(DataRowVersion.Original)을 이용해서 이전 값을 이용해 DELETE 문을 위한 구문을 작성하면 된다.

 

 

 

정리

 

.NET Framework에서는 DataSet(DataTable)을 이용하여 OldDB 개체를 통해 직접 수정하면 데이터베이스 상에서도 반영되게 만들어 줄 수 있다. 즉 DataTable 혹은 DataSet을 Select 등을 활용해 가져온 뒤, DataGrid에서 값을 변경하면, 자동적으로 데이터베이스와 연동해서 바뀐 값이 바로 바로 변경되게 구성할 수 있다는 점이다.

매우 DB 친화적인 것처럼 보일 수 있겠지만, 실제로는 거의 써먹질 못한다.

그 이유 중 하나가 Select를 할 때, 보통 Join을 해서 여러 개의 테이블을 복합적으로 사용하는 경우가 많은데,
이 경우 복합적으로 만들어지는 DataSet(DataTable)은 자동적으로 Update, Insert, Delete를 해줄 수 없다.
결국 개발자가 변경 점에 맞추어 SQL Query문을 생성해서 실행해야 된다는 것이다.

 

이런 변경 점을 즉시 즉시 찾을 수 있다면, 별도로 Dirty Flag를 만들어 변경 점이 있는지 없는지 늘 체크할 필요도 없고,
이전 데이터와 비교한다고 생쑈를 부릴 필요가 없어진다.

( 필자의 프로젝트 수행한 것 중, 무식하게 비교 점을 찾는 프로그램이 있었다. 틈 나면 그 부분도 갈아 엎어야 할 듯...)

728x90

+ Recent posts