C# - 닷넷 응용 프로그램에서 Sybase DB 사용
역시 유료 DBMS인 Sybase도 무료 버전인 "SQL Anywhere Developer Edition"을 배포하고 있습니다. 다음의 경로를 통해서 다운로드 받고,
SQL Anywhere 12.0.1 Developer Edition
; http://www.sybase.com/detail?id=1055872
설치하면 관리도구까지 모두 함께 설치됩니다. 특이하게도, 관리자 암호를 묻는 창도 안 뜹니다. 이것 역시 DB2와 유사하게 DB 인스턴스에 접속해서 관리를 하는 것이 아니라 데이터베이스를 생성하면서 계정 정보를 입력하고 관리하는 식입니다.
함께 설치된 관리도구 - Sybase Central을 실행하고 "Tools" / "Create Database..." 메뉴를 선택해 새로운 데이터베이스를 생성합니다. 나머지는 그냥... ^^ 감각적으로 DB 테이블 생성하고 해주시면 됩니다.
재미있는 것은, DB를 생성한 것과 서비스를 시작하는 것은 전혀 다르다는 점입니다. DB 생성은 단지 파일을 생성한 것 뿐입니다. 이 DB 파일을 호스팅하는 것은 완전히 별개로 나뉩니다.
서비스 시작은 별도로 "시작" 메뉴에 등록된 "Network Server"라는 프로그램을 실행시켜야 합니다. 그럼 다음과 같이 DB 파일을 선택하는 창이 뜨고,
파일 경로만 입력한 후 나머지 입력 상자는 무시하고 그냥 "OK" 버튼을 누릅니다. 그럼 다음과 같은 창이 뜨면서 로그를 출력하고 서비스가 시작됩니다.
무료인 "Developer Edition"이라 그런지 제약이 많습니다. Connection Limit도 3이고, 서비스도 이렇게 Interactive 프로세스로 뜨는 것만 기본 제공되고 있습니다.
명령행에서 곧바로 실행시키려면 다음과 같이 실행하면 됩니다.
C:\Program Files\SQL Anywhere 12\Bin64>dbsrv12 d:\sadb\mytestdb.db
스케쥴러에 컴퓨터 시작 시 실행되는 프로그램으로 등록해 두면 그나마 NT 서비스 처럼 동작시킬 수 있습니다.
참고로,
테스트를 위한 DB 테이블은 지난번 MySQL 과 동일하게 해주었습니다.
구성된 DB 서버로 .NET 클라이언트에서 접속을 해봐야 하는데요.
Sybase 용 ADO.NET Data Provider 역시 다음의 다운로드를 통해 쉽게 설치할 수 있습니다. 설치 도중 "SQL Anywhere Client" 구성 요소가 64-bit, 32-bit 중에 하나만 선택되어 있기 때문에 필요하다면 2개 모두 명시적으로 설치 표시를 해야 합니다.
SQL Anywhere Database Client Download
; http://www.sybase.com/detail?id=1087327
설치 후 C:\Program Files\SQL Anywhere 12\Assembly 폴더를 보면 .NET 2.0, 3.5, 4.0 각각의 버전에 맞게 하위 폴더가 나뉘고 다음의 3가지 어셈블리 파일을 구할 수 있습니다.
- iAnywhere.Data.SQLAnywhere.dll
- iAnywhere.Data.SQLAnywhere.v3.5.dll
- iAnywhere.Data.SQLAnywhere.v4.0.dll
iAnywhere.Data.SQLAnywhere도 MySQL처럼 완벽한 하나의 Managed Provider 역할을 합니다. 따라서 지난번 DB2처럼 별도의 Native 모듈을 배포할 필요는 없습니다.
프로그램은 대충 다음의 글을 참고해서 만들 수 있습니다.
Connecting to a SQL Anywhere Database Using ADO.NET and the SQL Anywhere .NET Data Provider
; https://wiki.scn.sap.com/wiki/display/SQLANY/Connecting+to+a+SQL+Anywhere+Database+Using+ADO.NET+and+the+SQL+Anywhere+.NET+Data+Provider
위의 글에는 ODBC에 등록된 DSN을 이용한 연결문자열을 사용했지만, ODBC 경유 없이 곧바로 접속하는 것도 가능합니다. 다음은 제 테스트 환경의 연결 문자열입니다.
ENG=mytestdb;LINKS=tcpip(Host=192.168.100.21;ServerPort=2638);UID=DBA;PWD=sql
그리고
테스트 예제는 지난번과 다름없이 만들었습니다. Parameter 대체 문자로 '?'를 쓴 것이 다를 뿐 거의 비슷합니다.
string connectionString = "ENG=mytestdb;LINKS=tcpip(Host=192.168.0.11;ServerPort=2638);UID=DBA;PWD=dba@2008";
using (SAConnection myConnection = new SAConnection(connectionString))
{
myConnection.Open();
// Create
SACommand insertCommand = new SACommand();
insertCommand.Connection = myConnection;
insertCommand.CommandText = "INSERT INTO mytable(id, NAME, age, DESCRIPTION) VALUES (?, ?, ?, ?)";
insertCommand.Parameters.Add("@id", SADbType.Integer);
insertCommand.Parameters.Add("@NAME", SADbType.VarChar, 50);
insertCommand.Parameters.Add("@age", SADbType.Integer);
insertCommand.Parameters.Add("@DESCRIPTION", SADbType.VarChar, 150);
string nameValue = "Name" + Guid.NewGuid().ToString();
insertCommand.Parameters[0].Value = (int)DateTime.Now.Ticks;
insertCommand.Parameters[1].Value = nameValue;
insertCommand.Parameters[2].Value = 10;
insertCommand.Parameters[3].Value = nameValue + "_Description";
int affected = insertCommand.ExecuteNonQuery();
Console.WriteLine("# of affected row: " + affected);
// Update
SACommand updateCommand = new SACommand();
updateCommand.Connection = myConnection;
updateCommand.CommandText = "UPDATE mytable SET DESCRIPTION=? WHERE NAME=?";
updateCommand.Parameters.Add("@NAME", SADbType.VarChar, 50);
updateCommand.Parameters.Add("@DESCRIPTION", SADbType.VarChar, 150);
updateCommand.Parameters[0].Value = nameValue;
updateCommand.Parameters[1].Value = nameValue + "_Description2";
affected = updateCommand.ExecuteNonQuery();
Console.WriteLine("# of affected row: " + affected);
// Select - ExecuteScalar
SACommand selectCommand = new SACommand();
selectCommand.Connection = myConnection;
selectCommand.CommandText = "SELECT count(*) FROM mytable";
object result = selectCommand.ExecuteScalar();
Console.WriteLine("# of records: " + result);
// Select - DataTable
DataSet ds = new DataSet();
SADataAdapter da = new SADataAdapter("SELECT * FROM mytable", myConnection);
da.Fill(ds, "mytable");
DataTable dt = ds.Tables["mytable"];
foreach (DataRow dr in dt.Rows)
{
Console.WriteLine(string.Format("Name = {0}, Desc = {1}", dr["NAME"], dr["DESCRIPTION"]));
}
// Delete
SACommand deleteCommand = new SACommand();
deleteCommand.Connection = myConnection;
deleteCommand.CommandText = "DELETE FROM mytable WHERE NAME=?";
deleteCommand.Parameters.Add("@NAME", SADbType.VarChar, 50);
deleteCommand.Parameters[0].Value = nameValue;
affected = deleteCommand.ExecuteNonQuery();
Console.WriteLine("# of affected row: " + affected);
}
배포는 어떻게 해야 할까요? 위에서 언급한 것처럼 "iAnywhere.Data.SQLAnywhere.dll" 파일이 관리 제공자로써 동작하기 때문에 위의 프로그램을 다른 컴퓨터에 배포할 때 "SQL Anywhere Client"를 굳이 설치할 필요는 없습니다.
하지만, 다소 복잡한 문제가 발생했습니다. 알고 보니 iAnywhere.Data.SQLAnywhere.dll 파일은 내부적으로 dbdata12.dll과 dbdata12.dll.x64라는 네이티브 DLL을 리소스로 들고 있습니다. SAConnection은 최초 실행 시에 임시 폴더에 현재 프로세스와 관련된 dbdata12 DLL 파일을 임시 폴더에 풀어놓습니다. 예를 들어 다음과 같은 경로입니다.
C:\Windows\Temp\{16AA8FB8-4A98-4757-B7A5-0FF22C0A6E33}_1201.x64_1\dbdata12.dll
하지만, dbdata12.dll은 내부적으로 오류 메시지를 담은 또 다른 네이티브 DLL을 로드합니다. 그래서, iAnywhere.Data.SQLAnywhere.dll만 참조해서 그냥 실행하면 SAConnection 생성 시점에 다음과 같은 식의 예외가 발생합니다.
System.TypeInitializationException occurred
HResult=-2146233036
Message=The type initializer for 'iAnywhere.Data.SQLAnywhere.SAConnection' threw an exception.
Source=mscorlib
TypeName=iAnywhere.Data.SQLAnywhere.SAConnection
StackTrace:
at System.Runtime.Remoting.RemotingServices.AllocateUninitializedObject(RuntimeType objectType)
at System.Runtime.Remoting.Activation.ActivationServices.CreateInstance(RuntimeType serverType)
InnerException: iAnywhere.Data.SQLAnywhere.SAException
HResult=-2147467259
Message=언어 리소스 파일(dblgko12.dll, dblgen12.dll)을 찾을 수 없습니다.
Cannot find the language resource file (dblgko12.dll, dblgen12.dll).
Source=SQL Anywhere .NET Data Provider
ErrorCode=-2147467259
NativeError=0
StackTrace:
at iAnywhere.Data.SQLAnywhere.SAUnmanagedDll.get_Instance()
at iAnywhere.Data.SQLAnywhere.SAConnection..cctor()
InnerException:
이를 해결하기 위해서는 exe 파일과 같은 위치에 dblgen12.dll 파일만 함께 복사해 주면 됩니다. 문제는 이 파일이 네이티브 DLL이기 때문에 x86/x64로 나뉜다는 점입니다. 각각의 언어 모듈은 "SQL Anywhere Client"를 설치한 경우 다음의 폴더에 위치하고 있습니다.
C:\Program Files\SQL Anywhere 12\Bin32\dblgen12.dll
C:\Program Files\SQL Anywhere 12\Bin64\dblgen12.dll
그래서, 닷넷 프로그램이 x86인 경우에는 \Bin32\dblgen12.dll 파일을 복사해서 EXE와 같은 폴더에 넣어 두어야 하고, x64인 경우에는 \Bin64\dblgen12.dll 파일을 사용해야 합니다. 따라서, AnyCPU로 닷넷 프로그램을 만들 수 없습니다.
어쨌든, 필요한 파일은 iAnywhere.Data.SQLAnywhere.dll 파일과 각각의 플랫폼에 따른 dblgen12.dll 파일이므로 배포하는 데 크게 불편함은 없습니다. XCopy 식의 배포가 된다는 것입니다.
그런데, 콘솔 유형의 프로그램이야 EXE 파일을 생성하는 것이니 dblgen12.dll 파일의 배포가 자유롭지만, 웹 애플리케이션인 경우는 그렇지 않습니다.
w3wp.exe가 C:\Windows\System32\inetsrv 폴더에 위치하고 있기 때문에 dblgen12.dll 파일을 그곳에 복사하기에는 좀 그렇습니다. iAnywhere.Data.SQLAnywhere.dll 파일을 역어셈블해보니, 다행히 dbdata12.dll 파일 위치가 거의 고정적입니다.
C:\Windows\Temp\{16AA8FB8-4A98-4757-B7A5-0FF22C0A6E33}_1201.x64_1\dbdata12.dll
{16AA8FB8-4A98-4757-B7A5-0FF22C0A6E33}: 고정된 값
1201: iAnywhere.Data.SQLAnywhere.dll 파일의 Major, Minor, Build 버전숫자
x64: 플랫폼
1: 1부터 시작해서 기존의 dbdata12.dll 파일을 삭제 시도, 불가능하면 +1씩 증가해서 될 때 까지 반복.
그런데... 이것도 좀 그렇습니다. 공식적인 것은 아니기 때문입니다.
그다음으로 생각해 볼 것이, %PATH%에 지정된 경로입니다. 아쉽게도, 이런 경로에 집어넣었다가는 x64/x86 프로세스에 상관없이 로드가 될 것이기 때문에 해당 컴퓨터에서 하나의 Sybase 접속을 하는 응용 프로그램만 실행된다면 상관없지만 그 외의 경우에는 자칫 원인모를 충돌을 발생시킬 수 있습니다. (이런 건... 시간 지나면 찾기 힘듭니다.)
다행인 것은, %PATH%에 지정된 경로 중에서도 부가적으로 \bin64 또는 \bin32 폴더를 붙여서도 검색한다는 점입니다. 즉, C:\SYTEST라는 폴더가 %PATH%에 추가되어 있으면 C:\SYTEST\dblgen12.dll뿐만 아니라 64비트 프로세스의 경우 C:\SYTEST\BIN64\dblgen12.dll에서도 검색한다는 것입니다.
그럼, 힌트는 모두 나왔습니다. %PATH% 경로는 프로세스가 시작된 이후에도 바꿀 수 있다는 점을 감안해서 Global.asax.cs에 다음의 코드를 추가하면 경로 문제는 해결됩니다.
void Application_Start(object sender, EventArgs e)
{
string curPath = Server.MapPath("/");
curPath = Path.Combine(curPath, "bin");
string path = Environment.GetEnvironmentVariable("PATH");
path = curPath + ";" + path;
Environment.SetEnvironmentVariable("PATH", path);
}
그다음 웹 애플리케이션 프로젝트의 하위에 \Bin64, \Bin32 폴더를 만들고 각각 dblgen12.dll 파일을 추가해 줍니다. 그다음 Solution 탐색기에서 해당 파일을 선택해서 속성창의 "Build Action"은 "None"으로 설정하고, "Copy to Output Directory"에는 "Copy if newer" 설정을 해주면 됩니다.
첨부된 파일은 위의 설정이 포함된 예제 프로젝트입니다.
역시... XCopy 배포가 되어야! ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]