달력

52024  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

출처 : http://msdn.microsoft.com/ko-kr/magazine/cc135979.aspx




.NET Framework 3.5의 디렉터리 보안 주체 관리
Joe Kaplan and Ethan Wilansky
코드 다운로드 위치: DSAccountManagement2008_01.exe (160 KB)
Browse the Code Online


 

이 기사는 Visual Studio 2008 시험판 버전을 기준으로 합니다. 여기에 포함된 모든 정보는 변경될 수 있습니다.

이 기사에서 다루는 내용:
  • System.DirectoryServices.AccountManagement 클래스
  • Active Directory 도메인 서비스
  • AD LDS(Active Directory Lightweight Directory Services)
  • 사용자, 컴퓨터 및 그룹 보안 주체 관리
이 기사에서 사용하는 기술:
.NET Framework 3.5, Visual Studio 2008

디렉터리는 엔터프라이즈 응용 프로그램 개발의 중요한 구성 요소이지만 디렉터리에 대해 정통한 개발자는 드뭅니다. Windows® 플랫폼의 경우 Microsoft는 3가지 기본 디렉터리 플랫폼을 제공하고 있습니다. Active Directory® 도메인 서비스, 모든 Windows 컴퓨터의 로컬 SAM(보안 계정 관리자) 데이터 저장소, 그리고 기존에 ADAM(Active Directory Application Mode)이라고 알려진 AD LDS(Active Directory Lightweight Directory Services)가 그것입니다. 엔터프라이즈 개발자라면 대부분 적어도 SQL 프로그래밍의 기본은 알고 있지만 디렉터리 서비스 프로그래밍 경험이 있는 개발자는 많지 않습니다.
Microsoft® .NET Framework의 초기 버전에서는 System.DirectoryServices 네임스페이스에 디렉터리 서비스 프로그래밍을 위한 클래스 집합을 제공했습니다. 이러한 클래스는 기존 COM 기반 OS 구성 요소, 특히 ADSI(Active Directory 서비스 인터페이스)에 대한 관리되는 단순한 interop 계층이었습니다. 프로그래밍 모델은 상당히 강력했지만 완전한 ADSI 모델에 비해 일반화되고 단순화된 형태였습니다.
이어 Microsoft는 .NET Framework 2.0에서 System.DirectoryServices에 기능을 추가하고 System.DirectoryServices.ActiveDirectory와 System.DirectoryServices.Protocols라는 두 가지 새로운 네임스페이스를 제공하기 시작했습니다. 이 기사에서는 두 네임스페이스를 각각 ActiveDirectory 네임스페이스 및 Protocols 네임스페이스라고 하겠습니다. ActiveDirectory 네임스페이스는 서버, 도메인, 포리스트, 스키마, 복제 등 인프라 수준 구성 요소를 강력한 형식으로 관리하는 다양한 새 클래스를 제공합니다. 반면 Protocols 네임스페이스는 LDAP(Lightweight Directory Access Protocol) 프로그래밍을 위한 대체 API를 제공하는 다른 방향을 선택했습니다. 이 네임스페이스는 ADSI COM interop 계층을 전혀 거치지 않고 Windows LDAP 하위 시스템(wldap32.dll)과 직접 작업합니다(그림 1 참조).
그림 1 Microsoft 디렉터리 서비스 프로그래밍 아키텍처 (더 크게 보려면 이미지를 클릭하십시오.)
그러나 개발자들은 여전히 사용자 및 그룹과 같은 이전의 보안 주체를 관리하는 데 사용했던 ADSI의 몇 가지 강력한 형식의 인터페이스를 원했습니다. System.DirectoryServices의 일반적인 클래스를 사용해도 이러한 작업을 대부분 수행할 수 있었지만 사용하기가 다소 까다로웠고 난해한 작업이 많았습니다. 이에 .NET Framework 3.5에는 보안 주체 관리를 위해 특별히 설계된 System.DirectoryServices.AccountManagement라는 새로운 네임스페이스가 추가되었습니다. 이 기사에서는 System.DirectoryServices.AccountManagement를 간단히 AccountManagement 네임스페이스로 지칭하도록 하겠습니다.
이 새로운 네임스페이스에는 3가지 주요 목표가 있습니다. 3가지 디렉터리 플랫폼 전반에 걸친 보안 주체 관리 작업을 단순화하고, 기본 디렉터리에 관계없이 보안 주체 관리 작업이 일관되도록 하며, 주의 사항이나 특수한 상황을 모두 기억하지 않고도 안정적인 작업 결과를 얻을 수 있도록 하는 것이 바로 이러한 목표입니다.
.NET이 자리 잡기까지 수년을 기다린 끝에 Microsoft는 마침내 ADSI에서 제공되던 이전의 방식을 능가하는 솔루션을 내놓을 수 있었습니다. 이는 .NET 기능을 이용하여 동일한 기능을 구현하는 향상된 API를 제공하면서 AD LDS와 같은 새로운 디렉터리 플랫폼도 효과적으로 지원함에 따라 가능했습니다.

디렉터리 서비스 프로그래밍 아키텍처
본론으로 들어가기 전에 이 기사의 코드 다운로드에서 필수 구성 요소를 확인하십시오. 여기서 설명하는 기법을 사용하기 위해서는 이러한 항목이 필요합니다. 준비가 되었으면 본격적으로 시작해 보도록 하겠습니다. 그림 1은 System.DirectoryServices의 전반적인 프로그래밍 아키텍처를 보여 줍니다. AccountManagement 네임스페이스는 ActiveDirectory 네임스페이스와 마찬가지로 System.DirectoryServices 위에 있는 추상화 계층입니다. 그리고 System.DirectoryServices 자체도 ADSI 위에 있는 추상화 계층입니다. 또한 AccountManagement 네임스페이스는 고성능 인증과 같은 몇 가지 기능을 제공하기 위해 Protocols 네임스페이스를 사용합니다. 그림 1의 남색 음영은 디렉터리 서비스 프로그래밍 아키텍처에서 AccountManagement가 사용하는 부분을 나타냅니다.
그림 2에는 AccountManagement 네임스페이스의 핵심 형식이 나와 있습니다. .NET Framework 2.0에 추가된 네임스페이스와는 달리 AccountManagement는 노출 영역이 상대적으로 작습니다. 열거 및 예외 클래스와 같은 몇 가지 지원 형식을 제외하면 AccountManagement는 크게 강력한 형식의 사용자, 그룹 및 컴퓨터 개체를 나타내는 보안 주체 파생 클래스의 트리, 기본 저장소 연결에 사용되는 PrincipalContext 클래스, 디렉터리에서 개체를 찾는 데 사용되는 PrincipalSearcher 클래스(지원 형식과 함께 제공)의 3가지 구성 요소로 이루어져 있습니다.
그림 2 System.DirectoryServices.AccountManagement의 핵심 클래스 (더 크게 보려면 이미지를 클릭하십시오.)
내부적으로 AccountManagement 네임스페이스는 지원되는 3가지 디렉터리 플랫폼에서 작동할 수 있도록 공급자 디자인 패턴을 사용합니다. 결과적으로 기본 디렉터리 저장소에 관계없이 다양한 Principal 클래스의 멤버가 비슷하게 작동합니다. 이는 단순성과 일관성을 유지하는 데 핵심이 되는 설계입니다.

컨텍스트 설정
대상 디렉터리에 연결하고 디렉터리 작업을 수행하는 데 필요한 자격 증명을 지정하는 데는 PrincipalContext 클래스를 사용합니다. 이러한 방식은 ActiveDirectory 네임스페이스에서 DirectoryContext 클래스와의 컨텍스트를 설정하는 방법과 비슷합니다.
PrincipalContext 생성자에는 컨텍스트 설정에 정확히 개발자가 원하는 대로 옵션을 제공하기 위한 다양한 오버로드가 있습니다. System.DirectoryServices의 DirectoryEntry 클래스를 작업에 사용하다 보면 많은 PrincipalContext 옵션이 익숙하게 느껴질 것입니다. 그러나 ContextType, name 및 container의 3가지 PrincipalContext 옵션은 DirectoryEntry 클래스에 사용하는 입력 매개 변수보다 훨씬 한정적입니다. 이렇게 옵션의 용도를 구체화함으로써 항상 올바른 입력 매개 변수 조합을 사용하도록 한 것입니다. System.DirectoryServices 및 ADSI에서 PrincipalContext 생성자의 이 3가지 매개 변수는 경로라는 단일 문자열로 조합됩니다. 이러한 구성 요소를 분리함으로써 경로의 각 부분이 어떠한 기능을 하는지를 쉽게 이해할 수 있습니다.
ContextType 열거형을 사용하여 대상 디렉터리 유형을 Domain(Active Directory 도메인 서비스의 경우), ApplicationDirectory(AD LDS의 경우) 또는 Machine(로컬 SAM 데이터베이스의 경우)으로 지정합니다. 반면 System.DirectoryServices를 사용할 때는 경로 문자열의 공급자 구성 요소(일반적으로 "LDAP" 또는 "WinNT")를 사용하여 대상 저장소를 지정합니다. 그러면 ADSI가 내부적으로 이 값을 읽어 적절한 공급자를 로드합니다.
AccountManagement 네임스페이스의 클래스는 프레임워크에서 지원되는 공급자만 사용하도록 하여 이 작업을 쉽고 일관성 있게 만들 뿐 아니라, ADSI 및 System.DirectoryServices 프로그래밍에서 자주 발생하는 공급자 입력 실수나 잘못된 대/소문자 사용에 따른 성가신 문제도 방지합니다. 바꾸어 말하면 AccountManagement는 IIS 및 Novell Directory Services와 같이 일반적으로 잘 사용되지 않는 ADSI 공급자는 지원하지 않습니다.
연결할 특정 디렉터리의 이름을 제공하려면 PrincipalContext 생성자에 name 매개 변수를 사용해야 합니다. 이 매개 변수의 값은 특정 서버, 시스템 또는 도메인의 이름이 될 수 있습니다. 이 매개 변수가 null이면 AccountManagement가 현재 보안 컨텍스트를 기준으로 연결을 위한 기본 시스템 또는 도메인을 결정합니다. 그러나 AD LDS 저장소에 연결하려면 name 매개 변수의 값을 지정해야 합니다.
container 매개 변수는 컨텍스트를 설정할 디렉터리의 대상 위치를 지정하는 데 사용됩니다. SAM 데이터베이스는 계층 구조가 아니므로 Machine ContextType을 사용할 때는 이 매개 변수를 지정하지 마십시오. 반대로 AD LDS는 디렉터리 루트 개체를 유추할 때 사용할 defaultNamingContext 특성을 게시하지 않기 때문에 ApplicationDirectory를 사용할 때는 반드시 이 매개 변수에 값을 제공해야 합니다. Domain ContextType의 경우 이 매개 변수를 선택적으로 지정할 수 있으며 지정하지 않으면 AccountManagement는 defaultNamingContext를 사용합니다.
필요에 따라 추가 매개 변수(username, password 및 ContextOptions 열거형)를 사용하여 일반 텍스트 자격 증명을 제공하고 사용할 여러 가지 연결 보안 옵션을 지정할 수 있습니다.
모든 디렉터리는 Windows 협상 인증 방법을 지원합니다. Machine 저장소에 대해 옵션을 지정하지 않으면 Windows 협상 인증이 사용됩니다. 그러나 Domain 및 ApplicationDirectory의 경우에는 서명 및 밀봉을 모두 포함한 Windows 협상이 기본으로 사용됩니다.
Active Directory 도메인 서비스와 AD LDS는 LDAP 단순 바인딩도 지원합니다. 일반적으로 Active Directory 도메인 서비스에는 LDAP 단순 바인딩을 사용하지 않겠지만 AD LDS의 경우 AD LDS 저장소의 사용자가 보안 주체 작업을 수행하도록 하려면 LDAP 단순 바인딩이 필요할 수 있습니다.
username 또는 password 매개 변수를 null로 지정하면 AccountManagement가 현재 Windows 보안 컨텍스트를 사용합니다. 자격 증명을 지정할 때 지원되는 사용자 이름 형식으로는 SamAccountName, UserPrincipalName 및 NT4Name이 있습니다. 그림 3은 컨텍스트를 설정하는 3가지 방법을 보여 줍니다.
// TechWriters OU를 가르키고 기본 자격 증명을 사용하는
// Fabrikam이라는 도메인에 대한 컨텍스트 만들기
PrincipalContext domainContext = new PrincipalContext( 
  ContextType.Domain,"Fabrikam","ou=TechWriters,dc=fabrikam,dc=com");

// 현재 보안 컨텍스트를 사용하여
// 현재 시스템 SAM 저장소에 대한 컨텍스트 만들기
PrincipalContext machineContext = new PrincipalContext(
  ContextType.Machine);

//AD LDS 저장소의 사용자 자격 증명 및
// SSL 암호화를 사용하여 파티션 루트를 가리키는
// AD LDS 저장소에 대한 컨텍스트 만들기
PrincipalContext ldsContext = new PrincipalContext(
    ContextType.ApplicationDirectory, "sea-dc-02.fabrikam.com:50001", 
    "ou=ADAM Users,o=microsoft,c=us", 
    ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind, 
    "CN=administrator,OU=ADAM Users,O=Microsoft,C=US ", "pass@1w0rd01");

AccountManagement PrincipalContext 및 System.DirectoryServices DirectoryEntry 클래스 사이에는 바인딩 작업 시에 작지만 중요한 차이가 있습니다. PrincipalContext는 개체 생성 시 기본 디렉터리에 연결하고 바인딩하지만 DirectoryEntry는 연결이 필요한 다른 작업을 수행할 때까지 바인딩하지 않습니다. 결과적으로 PrincipalContext를 사용하면 연결이 디렉터리에 성공적으로 바인딩되었는지 바로 알 수 있습니다.

사용자 계정 만들기
이제 AccountManagement가 PrincipalContext를 사용하여 컨테이너에 연결하고 바인딩하는 과정은 충분히 배웠습니다. 다음은 일반적인 DirectoryServices 작업인 사용자 계정 만들기에 대해 알아보겠습니다. 이 과정에서 각 코드 샘플은 하나의 필수 특성에 값을 할당하고, 두 개의 선택적인 특성을 추가하며, 암호를 설정하고, 사용자 계정을 활성화한 다음, 변경 내용을 디렉터리로 커밋합니다.
여기에서는 그림 3의 예 1에서 소개한 domainContext 변수를 사용하여 새 UserPrincipal을 만듭니다.
// 사용자 보안 주체 개체 만들기
UserPrincipal user = new UserPrincipal(domainContext,
     "User1Acct", "pass@1w0rd01", true);

// 사용자 보안 주체에 일부 속성 할당
user.GivenName = "User";
user.Surname = "One";

// 다음 로그인할 때 암호를 변경하도록 함
user.ExpirePasswordNow();

// 사용자를 디렉터리에 저장
user.Save();
(참고: 프로그래머 주석은 예제 프로그램 파일에는 영문으로 제공되며 기사에는 이해를 돕기 위해 번역문으로 제공됩니다.)
domainContext는 작업 수행에 사용되는 디렉터리에 대한 연결과 보안 컨텍스트를 설정합니다. 그리고 한 줄의 코드로 새 user 개체를 만들고 암호를 설정하여 활성화합니다. 그런 다음 GivenName 및 Surname 속성을 사용하여 기본 저장소의 해당 디렉터리 특성을 설정합니다. 기본 디렉터리 저장소에 개체를 저장하기 전에 암호가 만료되어 사용자가 첫 번째 로그온 시에 암호를 변경하도록 합니다.
그림 4에는 이에 해당하는 System.DirectoryServices에서 사용자 계정을 만드는 단계가 나와 있습니다. 첫 번째 코드 줄의 container 변수는 연결에 경로를 사용하는 DirectoryEntry 클래스 개체입니다. 이 경로는 공급자, 도메인 및 컨테이너(TechWriters OU)를 지정하고 현재 사용자의 보안 컨텍스트를 사용하여 연결합니다. container 변수는 사용자 보안 주체를 만드는 이전 예에서 domainContext와 비슷합니다.
    DirectoryEntry container =
    new DirectoryEntry("LDAP://ou=TechWriters,dc=fabrikam,dc=com");
// 컨테이너에 사용자 디렉터리 엔트리 만들기
DirectoryEntry newUser = container.Children.Add("cn=user1Acct", "user");

// samAccountName 필수 특성 추가
newUser.Properties["sAMAccountName"].Value = "User1Acct";

// 선택적 특성 추가
newUser.Properties["givenName"].Value = "User";
newUser.Properties["sn"].Value = "One";

// 디렉터리에 저장
newUser.CommitChanges();

// Invoke 메서드 및 IadsUser.SetPassword를 사용하여
// 사용자 계정에 대한 암호 설정
newUser.Invoke("SetPassword", new object[] { "pAssw0rdO1" });

// 다음 로그인할 때 암호를 변경하도록 지정
newUser.Properties["pwdLastSet"].Value = 0;

// 사용자 계정을 활성화
// newUser.InvokeSet("AccountDisabled", new object[]{false});
// 또는 ADS_UF_NORMAL_ACCOUNT (512)를 사용하여 효과적으로
// 비활성화된 비트를 설정 해제
newUser.Properties["userAccountControl"].Value = 512;

// 디렉터리에 저장
newUser.CommitChanges();
   
이 코드는 AccountManagement 예보다 훨씬 길고 복잡합니다. 따라서 디렉터리의 기본 데이터 모델에 대해 더 많이 알아야 하고, 초기 암호를 설정하거나 disabled 플래그를 해제하여 user 개체를 활성화하는 등, 특정 계정 관리 기능을 처리하는 방법도 알아야 합니다. 이는 리플렉션 기반 Invoke 및 InvokeSet 메서드를 사용하여 기본 ADSI COM 인터페이스 멤버를 호출해야 하는 경우 까다로울 수 있습니다. 많은 개발자가 디렉터리 서비스 프로그래밍을 어렵게 생각하는 이유도 바로 이 때문입니다.
AD LDS 또는 로컬 SAM 데이터베이스에서 같은 사용자 계정을 만들 때는 차이가 더 커집니다. AD LDS와 Active Directory 도메인 서비스는 사용자 계정을 활성화하는 데 서로 다른 메커니즘(Active Directory 도메인 서비스는 userAccountControl 특성을 사용하지만 AD LDS는 msds-userAccountDisabled 특성을 사용)을 사용하며, SAM 저장소의 경우 ADSI 인터페이스 멤버를 호출해야 합니다. 이렇게 3가지 디렉터리 저장소의 계정 관리 기능이 서로 다르다는 점은 AccountManagement 네임스페이스 설계에 있어서 해결되어야 할 중요한 과제였습니다. 그러나 이제 UserPrincipal 개체를 만드는 데 사용되는 PrincipalContext를 변경함으로써 간단히 디렉터리 저장소 사이를 전환하고 일관성 있는 한 가지 계정 관리 기능 집합을 얻을 수 있게 되었습니다.
.NET Framework 2.0에 도입된 Protocols 네임스페이스는 이러한 문제 해결과는 전혀 관련이 없었습니다. 단지 시스템 수준 LDAP 프로그래머에게 LDAP 기반 응용 프로그램 작성에 사용할 수 있는 보다 강력하고 유연한 API를 제공하기 위한 것이었습니다. System.DirectoryServices보다 LDAP 모델에 대한 추상화 수준이 낮기 때문에 다양한 디렉터리 간의 차이를 줄이는 데는 효과가 없었습니다. 게다가 LDAP 디렉터리가 아닌 로컬 SAM 데이터베이스에는 사용할 수 없었습니다. 이 기사와 함께 제공되는 온라인 코드 샘플에는 그림 3 다음에 나오는 AccountManager 예와 비슷한 샘플이 포함되어 있습니다. 이 샘플은 해당 예와 동일한 작업을 수행하지만 코드 줄 수는 3배에 달합니다.
그림 5의 PrincipalContext는 SAM 저장소를 대상으로 지정하기 위해 시스템을 참조하는 ContextType 열거형을 보여 줍니다. 다음 매개 변수는 실제 시스템을 이름 또는 IP 주소를 기준으로 대상으로 지정하고 마지막 두 값은 계정 생성을 수행할 수 있는 계정의 자격 증명을 제공합니다.
PrincipalContext principalContext = new PrincipalContext(
    ContextType.Machine. "computer01", "adminUser", "adminPassword");
   
UserPrincipal user = new UserPrincipal(principalContext,
    "User1Acct", "pass@1w0rd01", true);

//다른 저장소에 액세스할 때는 특성에 차이가 있으며
//IntelliSense에 나타나는 특성은 기반 저장소에서
//파생되지 않음을 유의하십시오.
user.Name = "User One";
user.Description = "User One";
user.ExpirePasswordNow();
user.Save();
Active Directory 도메인 서비스와는 달리 SAM 저장소에는 givenName, sn 또는 displayName 특성이 없기 때문에 Name 및 Description 속성을 설정해야 합니다. AccountManagement는 3가지 디렉터리 저장소 모두에 일관성 있는 환경을 제공하기는 하지만 기본 모델 자체에 차이가 있습니다. 그러나 기본 저장소에서 제공되지 않는 특성을 가져오려고 하면 InvalidOperationException이 발생합니다.
그림 3과 그림 5는 사용자 계정 생성 방법을 보여 주는 두 가지 예입니다. 이 예에서는 저장소에 관계없이 작업을 수행하는 AccountManagement 네임스페이스의 일관성 있는 고유 프로그래밍 모델을 보여 줍니다. 그림 6의 PrincipalContext는 AD LDS 저장소를 대상으로 지정하기 위해 그림 3과 같이 ContextType.ApplicationDirectory를 사용합니다. 다음 매개 변수는 AD LDS 서버를 보여 줍니다. 이 예에서 sea-dc-02.fabrikam.com은 AD LDS 인스턴스를 호스트하는 서버의 FQDN(정규화된 도메인 이름)이며 인스턴스는 50001 포트에서 SSL 통신을 수신합니다. 코드 다운로드에서는 이와 달리 50000 포트를 통한 비 SSL 통신을 사용합니다. 안전하지는 않지만 테스트 용도로는 문제가 없을 것입니다.
PrincipalContext principalContext = new PrincipalContext(
    ContextType.ApplicationDirectory,
    "sea-dc-02.fabrikam.com:50001",
    "ou=ADAM Users,o=microsoft,c=us",
    ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind,
    "CN=administrator,OU=ADAM Users,O=Microsoft,C=US",
    "P@55w0rd0987");

UserPrincipal user = new UserPrincipal(principalContext,
    "User1Acct", "pass@1w0rd01", true);

user.GivenName = "User";
user.Surname = "One";

user.Save();

다음 매개 변수는 CRUD(Create/Read/Update/Delete) 작업을 수행할 컨테이너를 지정합니다. 이 예에서는 CRUD 작업 수행을 위해 AD LDS에 저장된 사용자를 지정하므로 LDAP 단순 바인딩을 사용해야 하며 이는 보안을 위해 SSL과 결합됩니다. AD LDS는 기본적으로 보안 DIGEST 인증을 지원하지만 ADSI는 이를 지원하지 않습니다. 이번에도 역시 이 예는 PrincipalContext가 크게 다르지만 이전의 두 예와 사실상 동일합니다.
AccountManagement 네임스페이스는 암호 만료, 계정 잠금 해제와 같은 포괄적인 계정 관리 기능 집합을 제공합니다. 이 기사에서 이러한 기능을 모두 소개할 수는 없지만 이러한 기능은 디렉터리 저장소 유형에 관계없이 일관적이고 안정적으로 작동하며 이를 구현하기 위한 복잡한 과정이 없어졌습니다.

그룹 및 컴퓨터 만들기
사용자 계정을 만드는 작업은 DirectoryServices 저장소에 관계없이 간단하고 일관성 있게 수행된다는 것을 살펴보았습니다. 이러한 일관성은 지원되는 다른 두 가지 DirectoryServices 개체인 group 및 computer를 만드는 데도 마찬가지로 유지됩니다. UserPrincipal 클래스와 마찬가지로 GroupPrincipal 및 ComputerPrincipal 클래스도 Principal 추상 클래스에서 상속하고 비슷하게 작동합니다. 예를 들어 다음 코드를 사용하면 Active Directory 도메인 서비스, AD LDS 또는 SAM 계정 데이터베이스에 Group01이라는 그룹을 만들 수 있습니다.
GroupPrincipal group = new GroupPrincipal(principalContext,
    "Group01");
group.Save();
각 경우의 차이점은 서로 다른 저장소와의 컨텍스트를 설정하는 PrincipalContext 클래스에 포함됩니다. computer 개체를 생성하는 데 사용되는 코드에서도 컨텍스트를 사용하여 principal 개체를 생성한 다음 보안 주체 컨텍스트의 대상에 개체를 저장하는 비슷한 패턴을 따릅니다.
ComputerPrincipal computer = new ComputerPrincipal(domainContext);
computer.DisplayName = "Computer1";
computer.SamAccountName = "Computer1$";
computer.Enabled = true;
computer.SetPassword("p@ssw0rd01");
computer.Save();
이번에도 AccountManagement는 지원되는 모든 ID 저장소의 상호 작용 모델을 일관성 있게 유지합니다. 이 샘플은 도메인에 가입할 수 있는 computer 개체(sAMAccountName 특성 끝에 $ 필요)를 만드는 구문으로도 사용할 수 있지만 여기에서는 표시 이름 및 일반 이름을 설정하므로 $를 포함하지 않습니다. SAM 데이터베이스와 AD LDS에는 computer 클래스가 없기 때문에 AccountManagement는 도메인 기반 PrincipalContext 내에서만 이러한 유형의 개체를 만들도록 허용합니다. 또한 Active Directory 도메인 서비스에만 글로벌, 유니버설 및 도메인 로컬의 다양한 그룹 범위가 있으며 보안 및 메일 그룹이 모두 포함되어 있습니다. 따라서 GroupPrincipal 클래스는 필요에 따라 이러한 값을 설정할 수 있도록 null 허용 속성을 제공합니다.

그룹 멤버 자격 관리
AccountManagement 네임스페이스는 그룹 멤버 자격 관리도 간소화해 줍니다. AccountManagement가 개발되기 전에는 그룹 관리 방식에 있어서 저장소 유형별로 고유한 부분이 많았고 당연히 일관성도 없었습니다. 또한 구성원이 많은 그룹을 프로그래밍 방식으로 관리하기도 어려웠습니다. 게다가 SAM 그룹 멤버 자격을 관리하는 데는 COM interop을 사용하고 Active Directory 도메인 서비스 및 AD LDS 그룹을 관리하는 데는 LDAP 특성을 사용해야 했습니다. 그러나 이제 저장소에 관계없이 GroupPrincipal 클래스의 Members 속성을 사용하여 그룹의 멤버 자격을 열거하고 구성원을 관리할 수 있게 되었습니다.
사용자가 속한 모든 그룹을 가져오는 작업도 간단해 보이지만 실제로는 구현하기가 까다롭습니다. AccountManagement는 이를 위해 사용할 수 있는 몇 가지 메서드를 제공합니다. Principal 기본 클래스에는 GetGroups 메서드 및 IsMemberOf 메서드가 두 개씩 포함되어 있습니다. 이 메서드는 Principal 형식의 그룹 멤버 자격을 가져오고 해당 보안 주체가 그룹의 구성원인지 확인합니다. 또한 UserPrincipal은 모든 UserPrincipal 형식의 완전히 확장된 보안 그룹 멤버 자격을 반환하는 특수한 GetAuthorizationGroups 메서드도 제공합니다. 그림 7은 GetAuthorizationGroups 메서드 사용 방법을 보여 줍니다.
string userName = "user1Acct";

// ID 저장소에서 사용자 찾기
UserPrincipal user =
    UserPrincipal.FindByIdentity(
        adPrincipalContext, 
        userName);

// 사용자 보안 주체에 대한 그룹을 얻고
// 결과를 PrincipalSearchResult object에 저장
PrincipalSearchResult<Principal> results = 
    user.GetAuthorizationGroups();

// 사용자가 속할 그룹의 이름을 표시
Console.WriteLine("groups to which {0} belongs:", userName);
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

트러스트된 도메인 또는 외부 보안 주체 간에 그룹 멤버 자격을 확장하는 작업도 AccountManagement에 의해 간소화되었습니다. 이 작업은 Principal 클래스의 GetGroups(PrincipalContext) 메서드가 담당합니다.

자체 찾기
디렉터리에서 개체를 찾는 것도 프로그래머가 어려움을 겪는 작업 중 하나입니다. 개발자가 일상적으로 다루는 구문에 비해 LDAP가 특별히 복잡한 쿼리 언어라고 할 수는 없지만 생소한 것이 사실입니다. LDAP에 대해 기초 지식이 있더라도 이를 사용하여 일반적인 작업을 수행하는 방법을 알아내기는 쉽지 않습니다.
이번에도 AccountManagement는 개체를 찾는 작업을 간단하게 처리할 수 있도록 FindByIdentity 메서드를 제공합니다. 이 메서드는 UserPrincipal, GroupPrincipal 및 ComputerPrincipal 클래스가 상속하는 Principal 추상 클래스에 포함되어 있습니다. 따라서 이러한 Principal 형식 중 하나를 검색해야 할 때마다 FindByIdentity를 사용하면 됩니다.
FindByIdentity에는 PrincipalContext 및 찾을 값을 받는 두 개의 오버로드가 포함되어 있습니다. 값으로는 SamAccountName, Name, UserPrincipalName, DistinguishedName, Sid 또는 Guid의 지원되는 ID 유형을 사용할 수 있습니다. 두 번째 오버로드의 경우에도 값으로 지정할 ID 유형을 명시적으로 정의할 수 있습니다.
좀 더 간단한 오버로드를 먼저 살펴보면, 이전 예에서 만든 사용자 계정을 FindByIdentity를 사용하여 반환하는 코드는 다음과 같습니다.
UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "user1Acct");
컨텍스트가 설정되면(principalContext 개체에 저장) FindByIdentity 메서드를 사용하여 principal 개체(이 예의 경우 UserPrincipal)를 검색할 수 있습니다. 지원되는 ID 저장소에 대한 컨텍스트를 설정한 이후에는 ID를 검색하는 코드는 항상 동일합니다.
두 번째 FindByIdentity 생성자는 명시적으로 ID 형식을 지정할 수 있도록 해 줍니다. 이 생성자를 사용할 때는 값의 형식이 지정할 ID 유형과 일치하도록 해야 합니다. 예를 들어 다음 코드는 디렉터리의 지정한 위치에 해당 개체가 있는 경우 고유 이름을 사용하여 UserPrincipal을 반환합니다.
UserPrincipal user = UserPrincipal.FindByIdentity(
    adPrincipalContext, 
   IdentityType.DistinguishedName,
   "CN=User1Acct,OU=TechWriters,DC=FABRIKAM,DC=COM");
반면에 다음 코드는 IdentityType 열거형이 DistinguishedName 형식을 지정하지만 값은 이 형식이 아니므로 UserPrincipal을 반환하지 않습니다.
UserPrincipal user = UserPrincipal.FindByIdentity(
    adPrincipalContext,
   IdentityType.DistinguishedName,
   "user1Acct");
형식은 매우 중요합니다. 예를 들어 GUID 또는 SID IdentityType을 사용할 때는 각각 표준 COM GUID 형식 및 SDDL(Security Descriptor Description Language) 형식의 값을 사용해야 합니다. 이 기사의 코드 다운로드에는 올바른 형식을 보여 주는 두 가지 메서드(FindByIdentityGuid 및 FindByIdentitySid)가 포함되어 있습니다. 사용자의 디렉터리 저장소에서 일치하는 결과를 찾으려면 이러한 메서드의 GUID 또는 SID 값을 변경해야 합니다. 잠시 후 살펴보겠지만 PrincipalSearcher 클래스를 사용하면 이러한 형식을 쉽게 얻을 수 있습니다.
이제 Principal 개체를 찾아 바인딩했으므로 이에 대한 작업을 수행하기는 어렵지 않습니다. 예를 들어 다음과 같이 그룹에 사용자를 추가할 수 있습니다.
// 사용자 보안 주체 얻기
UserPrincipal user = 
    UserPrincipal.FindByIdentity(adPrincipalContext, "User1Acct");
// 그룹 보안 주체 얻기
GroupPrincipal group = 
    GroupPrincipal.FindByIdentity(adPrincipalContext, "Administrators");
// 사용자 추가
group.Members.Add(user);
// 변경 내용을 디렉터리에 저장
group.Save();
여기에서는 사용자를 찾은 다음 그룹을 찾는 데 FindByIdentity 메서드를 사용했습니다. 이러한 보안 주체 개체를 얻은 다음에는 그룹 Members 속성의 Add 메서드를 호출하여 그룹에 사용자 보안 주체를 추가합니다. 마지막으로 그룹의 Save 메서드를 호출하여 변경 내용을 디렉터리에 저장합니다.

일치하는 결과 찾기
강력한 예제에 의한 쿼리(QBE) 기능과 PrincipalSearcher 클래스를 사용하여 정의된 기준에 따라 개체를 찾을 수도 있습니다. QBE와 PrincipalSearcher 클래스에 대해서는 잠시 후에 자세히 설명하기로 하고, 간단한 검색 예를 먼저 살펴보겠습니다. 그림 8은 name/cn 접두사 "user"로 시작하는 비활성화된 사용자 계정을 모두 찾는 방법을 보여 줍니다.
// 검색될 대상을 설명하기 위한 보안 주체 개체 표현 만들기
UserPrincipal user = new UserPrincipal(adPrincipalContext);

// 검색의 속성 정의(와일드카드 사용 가능)
user.Enabled = false;
user.Name = "user*";

// 검색 작업을 실행하기 위한 보안 주체 검색기 만들기
PrincipalSearcher pS = new PrincipalSearcher();

// 만든 보안 주체 개체에 쿼리 필터 속성 할당
// PrincipalSearcher 생성자에서 사용자 보안 주체를 전달할 수도 있음
pS.QueryFilter = user;

// 쿼리 실행
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Disabled accounts starting with a name of 'user':");
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

PrincipalContext 변수인 adPrincipalContext는 Active Directory 도메인을 가리키지만 AD LDS 응용 프로그램 파티션도 마찬가지로 간단하게 가리킬 수 있습니다. 컨텍스트를 설정한 다음 코드에서는 새로운 UserPrincipal 개체를 만듭니다. 이는 검색 작업 대상이 되는 보안 주체의 메모리 내 표현입니다. 이 보안 주체를 만든 후에는 검색 결과를 제한하는 속성을 설정합니다. 다음 두 줄의 코드는 개발자가 설정할 수 있는 몇 가지 제한을 보여 줍니다. 이 코드는 사용자 이름이 특정 값으로 시작하는 비활성화된 모든 사용자를 지정합니다. Name 특성에 대한 속성 값에는 와일드카드를 사용할 수 있습니다.
LDAP 언어에서 검색 필터를 설정하는 데 익숙한 개발자라면 QBE가 왜 독창적이고 직관적인 대안인지 금방 알 수 있을 것입니다. QBE를 사용하여 쿼리 작업에 사용할 예 개체를 설정합니다. 그림 8에서 생성한 QBE 개체와 동등한 필터를 설정하는 다음 LDAP 언어와 비교해 보면 QBE가 일반적인 DirectoryServices 검색 언어에 비해 간단하다는 사실을 확인할 수 있습니다.
(&(objectCategory=person)(objectClass=user)(name=user*)(userAccount
Control:1.2.840.113556.1.4.803:=2))
이 코드에서 보듯이 LDAP 언어는 훨씬 복잡할 뿐만 아니라 Active Directory LDS 사용자 스키마에는 LDAP 언어에서 사용되는 userAccountControl 특성 대신 msDS-UserAccountDisabled 특성이 사용되므로 AD LDS에 대해서는 작동하지 않습니다. 이러한 차이도 역시 AccountManagement에 의해 내부적으로 처리됩니다.
그림 8에 나와 있는 QBE 개체를 설정하고 나면 PrincipalSearcher 개체를 만들고 코드 이전 부분에서 Principal 개체에 의해 생성된 QueryFilter 속성을 할당합니다. 이렇게 QueryFilter 속성을 설정하는 대신 PrincipalSearcher 생성자에 사용자 보안 주체를 전달할 수도 있습니다. 그런 다음 PrincipalSearcher의 FindAll 메서드를 호출하고 반환되는 결과를 PrincipalSearchResult 일반 목록에 할당하여 쿼리를 실행합니다. PrincipalSearchResult 목록은 반환되는 Principal 개체를 저장합니다. 마지막으로 보안 주체 목록을 열거하고 반환된 각 보안 주체의 Name 특성을 표시합니다.
참조 특성에는 QBE를 사용할 수 없습니다. 즉, QBE 개체에서 소유하지 않는 특성은 개체의 메모리 내 표현을 구성하는 데 사용할 수 없습니다.
foreach 루프를 사용하면 비활성화된 사용자 계정을 활성화하거나 삭제하는 등, 훨씬 다양한 작업이 가능합니다. 읽기 작업만 원하는 경우 다른 ID 저장소를 가리키려면 반환할 특성이 해당 저장소에 있어야 한다는 점에 주의해야 합니다. 예를 들어 AD LDS 사용자에는 sAMAccountName 특성이 없으므로 이 특성을 결과로 반환할 수 없습니다.

까다로운 검색 작업의 단순화
강력한 FindBy 메서드를 PrincipalSearchResult 클래스와 함께 사용하면 다른 방법으로는 검색이 어려운 사용자 및 컴퓨터 보안 주체에 대한 정보를 검색할 수 있습니다. 그림 9는 오늘 암호를 변경한 각 사용자의 이름을 검색하는 방법을 보여 줍니다. 이 예에서는 FindByPasswordSetTime 메서드와 PrincipalSearchResult 클래스를 사용합니다. 기본 pwdLastSet 특성은 디렉터리에 큰 정수로 저장되기 때문에 AccountManagement를 사용하지 않으면 작업이 복잡해집니다.
// 오늘 날짜 얻기
DateTime dt = DateTime.Today;

// 쿼리 실행
PrincipalSearchResult<Principal> results = 
    UserPrincipal.FindByPasswordSetTime(
        adPrincipalContext, 
        dt, 
        MatchType.GreaterThanOrEquals); 

Console.WriteLine("users whose password was set on {0}", 
    dt.ToShortDateString());
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

이 기사의 코드 다운로드에는 다른 FindBy 메서드 사용 예가 포함되어 있습니다. 이러한 다른 예도 모두 작동 방식은 그림 9와 비슷합니다.
FindBy 메서드는 검색하기 어려운 정보를 쉽게 찾는 유용한 도구지만 QBE 기능을 사용하여 결과를 추가로 필터링해야 하는 경우에는 적합하지 않습니다. 관련 특성이 읽기 전용이기 때문에 QBE 개체에서 특성을 설정할 수 없는 것은 물론 QBE가 참조하는 개체의 사용자도 설정할 수 없습니다. QBE를 사용하려면 예 principal 개체에 해당하는 읽기 전용 속성 및 AdvancedSearchFilter 속성을 함께 사용해야 합니다. 이에 대해서는 나중에 설명하도록 하겠습니다. 그림 10에서는 다른 FindBy 메서드를 나열하고 검색에 FindBy 메서드 대신 사용할 수 있는 해당하는 읽기 전용 속성을 보여 줍니다.

메서드 이름 읽기 전용 속성 설명
FindByLogonTime LastLogonTime 지정한 시간 내에 로그온한 계정
FindByExpirationTime AccountExpirationDate 지정한 시간 내에 만료된 계정
FindByPasswordSetTime LastPasswordSetTime 지정한 시간 내에 암호가 설정된 계정
FindByLockoutTime AccountLockoutTime 지정한 시간 내에 잠긴 계정
FindByBadPasswordAttempt LastBadPasswordAttempt 지정한 시간 내에 잘못된 암호가 입력된 계정
해당 메서드 없음 BadLogonCount 지정한 횟수만큼 로그온을 시도했지만 로그온에 실패한 계정
QBE를 구성할 때 읽기 전용 속성의 값은 설정할 수 없습니다. 그렇다면 검색 작업에서 속성은 어떻게 처리해야 할까요? 결과 집합을 열거하는 동안 결과 집합을 검색하고 읽기 전용 속성을 사용하여 조건 테스트를 수행할 수 있습니다. 그러나 크기가 커질 수 있는 결과 집합에는 이러한 방법이 적합하지 않습니다. 읽기 전용 속성으로 필터링되지 않은 결과를 먼저 검색하고 반환된 결과 집합을 읽기 전용 속성으로 필터링해야 하기 때문입니다. 코드 다운로드의 PrincipalSearchEx6v2 메서드는 이러한 비효율적인 방식을 보여 줍니다.
디렉터리 서비스 팀에서는 AuthenticablePrincipal 클래스에 AdvancedSearchFilter 속성을 추가함으로써 QBE의 이러한 한계를 해결했습니다. AdvancedSearchFilter를 사용하면 읽기 전용 속성을 기반으로 검색을 수행한 다음 QBE 메커니즘을 사용하여 설정 가능한 다른 속성과 결합할 수 있습니다. 그림 11은 UserPrincipal 클래스의 LastBadPasswordAttempt 읽기 전용 속성을 사용하여 오늘 잘못된 암호로 액세스를 시도한 사용자를 반환하는 방법을 보여 줍니다.
DateTime dt = DateTime.Today;

// 검색될 대상을 설명하기 위한 보안 주체 개체 표현 만들기
UserPrincipal user = new UserPrincipal(adPrincipalContext);

user.Enabled = true;

// 검색의 속성 정의(와일드카드 사용 가능)
user.Name = "*";

// 쿼리 필터에 LastBadPasswordAttempt >= Today 추가
user.AdvancedSearchFilter.LastBadPasswordAttempt
    (dt, MatchType.GreaterThanOrEquals);

// 검색 작업을 실행하기 위한 보안 주체 검색기를 만들고
// 쿼리 필터로 QBE 사용자 보안 주체를 할당
PrincipalSearcher pS = new PrincipalSearcher(user);

// 쿼리 실행
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Bad password attempts on {0}:", 
    dt.ToShortDateString());
foreach (UserPrincipal result in results)
{
    Console.WriteLine("name: {0}, {1}",
           result.Name,
           result.LastBadPasswordAttempt.Value);
}


사용자 인증
디렉터리 기반 응용 프로그램을 개발할 때, 특히 AD LDS를 사용하는 경우 디렉터리에 저장된 사용자의 자격 증명을 인증해야 할 때가 많습니다. .NET Framework 3.5가 발표되기 전에 프로그래머들은 System.DirectoryServices의 DirectoryEntry 클래스를 사용하여 내부적으로 강제로 LDAP 바인딩 작업을 수행하여 이를 구현했습니다. 그러나 이러한 코드는 안전성이나 성능이 떨어질 수 있으며 작성하기도 어렵습니다. 또한 ADSI 자체는 이러한 형식의 작업에 맞게 설계되지 않았으며 내부적으로 LDAP 연결을 캐시하는 방식 때문에 사용량이 많은 상황에서는 문제가 발생할 수 있습니다.
이미 설명했듯이 .NET Framework 2.0의 System.DirectoryServices.Protocols 어셈블리에는 연결 기반 프로그래밍 메타포를 사용하는 저수준 LDAP 클래스가 포함되어 있습니다. 이것은 ADSI 고유의 한계를 극복하기 위한 설계이지만 복잡한 코드를 작성해야 한다는 단점이 있습니다.
.NET Framework 3.5의 AccountManagement는 프로그래머가 모든 환경에서 작업할 수 있도록 ASP.NET의 ActiveDirectoryMembershipProvider 구현을 통한 강력한 기능과 사용 편의성을 제공합니다. 또한 AccountManagement 네임스페이스는 필요한 경우 로컬 SAM 데이터베이스에 대해 자격 증명을 인증할 수 있도록 합니다.
PrincipalContext 클래스의 두 ValidateCredentials 메서드를 사용하여 자격 증명 유효성 검사를 수행할 수 있습니다. 먼저 자격 증명의 유효성을 검사할 대상 디렉터리를 사용하여 PrincipalContext의 인스턴스를 만들고 적절한 옵션을 지정합니다. 컨텍스트를 설정한 후 제공한 사용자 이름 및 암호 값에 따라 ValidateCredentials에서 true 또는 false가 반환되는지 테스트합니다. 그림 12는 AD LDS에서 사용자를 인증하는 예를 보여 줍니다.
// AD LDS를 사용하여 컨텍스트 설정
PrincipalContext ldsContext = 
    new PrincipalContext(
        ContextType.ApplicationDirectory, 
        "sea-dc-02.fabrikam.com:50000", 
        "ou=ADAM Users,O=Microsoft,C=US");

// 디렉터리에 유효한 사용자인지 확인
Console.WriteLine(
    ldsContext.ValidateCredentials(
        "user1@adam", 
        "Password1", 
        ContextOptions.SimpleBind + 
        ContextOptions.SecureSocketLayer));

이 방법은 다양한 사용자 자격 증명 집합의 유효성을 빠르고 효율적으로 검사할 때 특히 유용합니다. 해당 디렉터리 저장소에 대한 단일 PrincipalContext 개체를 만들어 ValidateCredentials에 대한 각 호출에 반복적으로 사용할 수 있으며 PrincipalContext가 디렉터리에 대한 연결을 재사용할 수 있어 성능과 확장성 면에 유리합니다. 그리고 ValidateCredentials에 대한 호출은 스레드로부터 안전하므로 여러 스레드에 걸쳐 이 작업을 수행하는 데 인스턴스를 사용할 수 있습니다. PrincipalContext를 만드는 데 사용되는 자격 증명은 ValidateCredentials에 대한 호출로 변경되지 않으며 컨텍스트 및 메서드 호출은 별도의 연결을 유지한다는 점이 중요합니다.
기본적으로 AccountManagement는 보안 Windows 협상 인증을 사용하며 AD LDS에 대해 단순 바인딩을 수행할 때는 SSL 사용을 시도합니다. 따라서 수행할 인증 유형과 사용할 연결 보호 방식(해당하는 경우)을 항상 명시적으로 지정하는 것이 좋지만 기본 설정을 사용하더라도 보안에는 문제가 없습니다.
Windows Server® 2003 이상 버전의 Active Directory 도메인 서비스 및 AD LDS에는 고성능 인증 작업을 위한 빠른 동시 바인딩 기능이 포함되어 있습니다. 이 기능은 사용자에 대한 보안 토큰을 실제로 만들지 않고 사용자 암호의 유효성을 검사합니다. 일반적인 바인딩 작업과는 달리 빠른 동시 바인딩에서는 LDAP 연결 상태가 바인딩되지 않은 채로 유지됩니다. 동일한 연결에 대해 반복적으로 바인딩 작업을 수행하고 간단히 실패한 암호 시도를 확인하는 데 빠른 동시 바인딩을 사용할 수 있습니다. 이 기능은 ADSI 또는 System.DirectoryServices를 통해 사용할 수 있는 옵션이 아니라 Protocols 네임스페이스에서 제공되는 옵션입니다.
AccountManagement는 가능한 한 빠른 동시 바인딩을 사용하고 자동으로 이 옵션을 설정합니다. 그림 1의 Protocols 계층 위에 AccountManagement 계층이 있는 것도 이러한 이유에서입니다. 빠른 동시 바인딩은 네트워크에서 일반 텍스트 자격 증명을 전달하는 단순 바인딩 모드에서만 사용할 수 있습니다. 따라서 보안상의 이유로 빠른 동시 바인딩은 항상 SSL과 함께 사용해야 합니다.

확장성 모델
AccountManagement가 빛을 발하는 다른 분야로 확장성 모델을 들 수 있습니다. Active Directory 도메인 서비스와 AD LDS 모두에 사용할 수 있는 사용자 지정 프로비전 시스템을 구축하기 위해 다양한 Principal 파생 클래스를 사용하는 개발자가 증가할 것입니다. 조직에서 특히 AD LDS의 경우 사용자와 그룹에 대한 자체 메타데이터를 지원하기 위해 디렉터리에 사용자 지정 스키마 확장을 추가하는 경우도 많아질 것입니다.
AccountManagement는 .NET Framework 개체 지향 설계 및 특성 기반 확장 가능 메타데이터를 사용함으로써 사용자 지정 스키마를 지원하는 사용자 지정 보안 주체 클래스를 쉽게 만들 수 있게 해 줍니다. Principal 파생 클래스 중 하나에서 상속하고 클래스와 속성을 적절한 특성으로 표시하면 사용자 지정 principal 클래스가 이러한 디렉터리 특성은 물론 기본 제공 형식에서 이미 지원되는 특성을 읽고 쓸 수 있습니다.
AccountManagement가 제공하는 확장성 메커니즘은 Active Directory 도메인 서비스 또는 AD LDS에 저장된 보안 주체에 사용하도록 설계되었다는 사실을 알아야 합니다. 즉, 비 Microsoft LDAP 디렉터리는 크게 염두에 두지 않은 것입니다. 따라서 비 Microsoft LDAP 디렉터리에서 프로비전에 사용할 프레임워크를 구축하려면 Protocols 네임스페이스의 저수준 클래스를 사용해야 합니다. 또한 SAM 스키마는 확장할 수 없으므로 로컬 SAM 계정에는 확장성 모델이 적합하지 않습니다.
표준 LDAP 사용자 클래스를 사용하여 응용 프로그램의 보안 주체를 저장하는 AD LDS 디렉터리가 있다고 가정해 보겠습니다. 그리고 msdn-subscriberID라는 사용자 개체를 식별하기 위한 특별한 특성을 지원하도록 LDAP 디렉터리 스키마를 확장했다고 가정합니다. 그림 13에서는 사용자 개체를 프로비전하고 이 특성에 대해 만들기, 읽기 및 쓰기 작업을 제공하는 사용자 지정 클래스를 작성하는 방법을 보여 줍니다.
[DirectoryObjectClass("user")]
[DirectoryRdnPrefix("CN")]
class MsdnUser : UserPrincipal
{
    public MsdnUser(PrincipalContext context)
        : base(context) { }

    public MsdnUser(
        PrincipalContext context,
        string samAccountName,
        string password,
        bool enabled
        )
        : base(
           context,
           samAccountName,
           password,
           enabled
           )
    {
    }

    [DirectoryProperty("msdn-subscriberID")]
    public string MsdnSubscriberId
    {
        get
        {
            object[] result = this.ExtensionGet("msdn-subscriberID");
            if (result != null) {
                return (string)result[0];
            }
            else {
                return null;
            }
        }
        set { this.ExtensionSet("msdn-subscriberID", value); }
    }
}

이 코드는 UserPrincipal 클래스에서 상속하며 DirectoryObjectClass 및 DirectoryRdnPrefix라는 두 가지 특성이 지정됩니다. 보안 주체 확장 클래스에는 이 두 특성이 모두 필요합니다. DirectoryObjectClass 특성은 지원되는 저장소(Active Directory 도메인 서비스 또는 AD LDS)에서 디렉터리에 이 개체의 인스턴스를 만들 때 objectClass 디렉터리 특성에 사용하는 값을 결정합니다. 여기서는 기본 AD LDS 사용자 클래스이지만 실제로는 다른 어떤 것도 될 수 있습니다. DirectoryRdnPrefix 특성은 디렉터리에서 이 클래스의 개체를 명명하는 데 사용할 RDN(상대 고유 이름) 특성 이름을 결정합니다. Active Directory 도메인 서비스에서는 RDN 접두사를 변경할 수 없으며 보안 주체 클래스의 경우 항상 CN입니다. 그러나 보다 유연한 AD LDS에서는 원하는 경우 다른 RDN을 사용할 수 있습니다.
예의 클래스에는 문자열을 반환하는 MsdnSubscriberID라는 속성이 있습니다. 이 클래스는 속성 값을 저장하는 데 사용할 LDAP 스키마 특성을 지정하는 DirectoryProperty 특성으로 표시되어 있습니다. 기본 프레임워크는 이 보안 주체 유형에 대한 검색 작업을 최적화하는 데 이 값을 사용합니다.
예의 속성 get 및 set 구현에서는 Principal 기본 클래스의 protected ExtensionGet 및 ExtensionSet 메서드를 사용하여 기본 속성 캐시에 대해 값을 읽고 씁니다. 이러한 메서드는 아직 데이터베이스/ID 저장소에 저장되지 않은 개체의 값을 메모리 내에 저장하도록 지원하고 기존 개체에서 값을 읽고 쓰는 작업도 지원합니다. LDAP 디렉터리는 다양한 특성 형식을 지원할 뿐만 아니라 특성에 여러 값이 포함될 수 있으므로 이러한 메서드는 object[] 형식을 사용하여 값을 읽고 씁니다. 이러한 유연성은 유용하지만 개체 형식의 배열에 더해 강력한 형식의 스칼라 문자열 값을 제공하려면 예 구현에서 보듯이 추가적인 작업이 필요합니다. 결과적으로 사용자 지정 MsdnUser 클래스의 사용자에게는 프로그래밍이 용이한 인터페이스가 제공됩니다.
디렉터리 스키마에 더해 강력한 형식의 값까지 제공하는 기능은 이 확장성 모델의 가장 유용한 기능 중 하나입니다. .NET Framework에서 제공하는 다양한 형식 시스템을 사용하면 단순 문자열 형식 외에도 Active Directory 도메인 서비스 jpgPhoto 특성을 일반적으로 System.DirectoryServices에서 값을 읽을 때 얻는 기본 byte[]가 아니라 System.Drawing.Image 또는 System.IO.Stream으로 나타내는 등의 작업을 수행할 수 있습니다.
이 기사의 코드 다운로드에는 이러한 기능을 보여 주는 몇 가지 다른 예가 있습니다. 또한 MsdnUser 클래스로 테스트 디렉터리를 확장하는 데 사용할 수 있는 스키마 확장도 msdnschema.ldf라는 표준 LDIF 형식 파일로 제공됩니다. "디렉터리 서비스 리소스" 보충 기사의 유용한 링크도 참조하시기 바랍니다.

결론
AccountManagement는 Microsoft에서 제공하는 풍부한 디렉터리 서비스 프로그래밍 모델에 절실히 요구되던 관리 코드 추가 기능이라 할 수 있습니다. AccountManagement 네임스페이스가 추가됨에 따라 개발자들은 일반적인 CRUD 및 검색 작업에 강력한 형식의 여러 가지 보안 주체를 사용할 수 있게 되었습니다.
이 네임스페이스에는 안전하고 성능이 뛰어난 관리 코드를 쉽게 작성할 수 있도록 디렉터리 서비스 프로그래밍과 관련된 최선의 방법이 캡슐화되어 있습니다. 또한 AccountManagement는 확장이 가능하므로 Active Directory 도메인 서비스 및 AD LDS의 사용자 지정 디렉터리 개체와 완벽하게 상호 작용할 수 있습니다.

Joe Kaplan은 Accenture의 사내 IT 조직에서 .NET Framework를 사용한 엔터프라이즈 응용 프로그램 구축을 담당하고 있습니다. 또한 응용 프로그램 보안, 페더레이션 ID 관리 및 디렉터리 서비스 프로그래밍 전문가로서 이 분야에서 Microsoft MVP로 선정되기도 하였으며 .NET Developer's Guide to Directory Services Programming(Addison-Wesley, 2006)의 공동 저자이기도 합니다.

Ethan Wilansky Microsoft Directory Services MVP이자 EDS 엔터프라이즈 설계자입니다. Ethan은 EDS Innovation Engineering의 일환으로, 현재 사용자 지정 SharePoint 응용 프로그램 개발에 집중하고 있는 개발 팀을 이끌며 EDS에 디렉터리 서비스 프로그래밍 솔루션과 관련한 자문을 제공하고 있습니다.
Posted by tornado
|
원본 : http://angeleyes.tistory.com/73
지현~ 땡큐

윈도우 서비스 실행 시 에러가 나게 되면.
에러 메시지를 볼 수 있는 방법은 로그를 찍던지 해야 됩니다만..


그런 불편함을 항상 가지고 갈 수 없습니다..

코드를 아래와 같이 작성 하게 되면…

  1. protected override void OnStart(string[] args)  
  2. {  
  3.     // TODO: 여기에 서비스를 시작하는 코드를 추가합니다.  
  4.     System.Diagnostics.Debugger.Launch();  
  5.     FileWatcher fw = new FileWatcher(System.Windows.Forms.Application.StartupPath);  
  6.     fw.OnInit();  
  7. }  




위의 이미지를 띄워서 디버깅 걸 수 있습니다.

윈도우 서비스가 실행 될때가 아니라면 Process Attach 로 확인 할 수 있습니다만.
위 방법은 서비스 실행 시 방법입니다.
Posted by tornado
|

출처 : http://blog.studioego.info/417


혹시나 Windows 원격 접속을 해 보신 적이 있나요?

MicroSoft사의 WindowsXP에서는 Windows 원격 데스크탑 접속을 할 수 있습니다. 서버로 사용하는 컴퓨터에 원격 접속 설정을 하면 학교 연구실에 있는 컴퓨터를 계속 켜고 퇴근한 후에 집에서 노트북으로 학교 연구실 컴퓨터에 접속해서 원격으로 작업을 할 수 있는 장점이 있습니다.
원격 데스크톱 연결

Window에서는 원격으로 Windows를 연결하여 원격 작업을 할 수 있습니다.


그렇다만 Linux 컴퓨터는 Xwindow가 설치되어 있는데 Windows에서 Linux컴퓨터의 Xwindow를 원격으로 실행 시킬까요?

여러가지 방법 중에서 VNC를 사용하는 방법이 있지만, VNC demon을 실행시키지 않으면 Windows에서 Linux컴퓨터의 Xwindow 실행시킬수 없습니다.
여기서는 Windows에서 Xming을 설치한 다음 Xming을 이용하여 쉽게 Linux컴퓨터의 Xwindow를 원격으로 실행하는 방법을 알려드리겠습니다.

※ Xming이란?
Wikipedia : Xming
배포사이트 : http://www.straightrunning.com/XmingNotes/
Xming는 Windows용 X Server입니다.
Microsoft사의 Windows에서 Xwindow용 프로그램을 출력할 수 있도록 해주는 화면 출력 서버입니다.
Microsoft사의 Windows에서 Linux 컴퓨터안의 X Window 프로그램을 화면에 출력할 수 있는 서버인 Xming을 설치하면 리눅스 프로그램도 Microsoft사의 Windows에서 실행 할 수 있습니다.
Windows에서 리눅스 프로그램을 실행하는 것처럼 보이지만, 실제 실행은 리눅스 컴퓨터에서 하고, 다만 X Server는 프로그램에서 전송하는 화면을 출력하고, 사용자의 키보드나 마우스와 같은 입력을 프로그램 쪽으로 전송해 주는 역할을 합니다.

Xming 설치 및 실행

  1. http://sourceforge.net/projects/xming
  2. Xming 설치.
  3. Xming-fonts 설치.
여기서 Test한 환경은
Windows XP와 CentOS 5입니다.

1. 우선 XLaunch를 띄웁니다.
2. Display settings에서 One window without titlebar를 선택합니다.

3. 데스크탑에서 Xwindow를 실행시킬려면 Start a program을 선택합니다.

4. 여기서 Xwindow환경을 실행할때에 Gnome를 실행하려면 Start Program에서 gnome-session을 넣고 KDE를 실행하려면 startkde를 넣으시면 됩니다.
원격접속할때에는 Run Remote에서 Using Putty를 선택하여 IP와 ID, Password를 넣으시면 됩니다.

5. Clipboard를 원격접속하는 곳에서 쓸려면 선택합니다.

6. 이제 셋팅이 끝났습니다.

결과 화면


ps1. Xmanager같은 비싼 프로그램이 한글지원도 편하게 되어서 사용하기에 좋긴 하지만, 학생이고 오픈소스로 공개된 Xming을 오픈소스 프로그램인 iPutty에 연결해 씁니다. 있을 것은 다 있는 오픈소스 프로그램들이라서 굳이 불법으로 쓸 필요는 없으니깐요.
ps2. Xming때문에 학교 안나오고 집에서 작업 한 후에 교수님에게 작업한 결과물을 보여주면서 학교는 나왔습니다 하면서 땡땡이 칠 수 있는 장점도 있습니다!
Posted by tornado
|
출처 : http://support.microsoft.com/?kbid=313114


Create a new C# program

  1. In Visual C# 2005 or in Visual C# .NET, create a new C# console program that is named MBTest.
  2. In Solution Explorer, right-click References, and then click Add Reference.
  3. On the .NET tab, add a project reference to the System.DirectoryServices namespace.
  4. On the COM tab, add a reference to Microsoft CDO for Exchange Management.
  5. Replace the code in the Class1.cs file with the following code.

    Note In Visual C# 2005, replace the code in the Program.cs file instead.
    using System;
    using CDOEXM;
    using System.DirectoryServices;
    
    namespace MBTest
    {
         class Class1
         {
              [STAThread]
              static void Main(string[] args)
              {
                   //TODO: Change these items to values for your domain or organization.
                   string defaultNC = "DC=yourdomain,DC=com";
                   string alias = "jsmith"; 
                   string fullName = "Joseph Smith";
                   string password = "TestMb123.";
                   string domainName = "yourdomain.com";
                   string homeMDB = "CN=Mailbox Store (Your Server),CN=Your Storage Group," 
                             + "CN=InformationStore,CN=Your Server,CN=Servers,"
                             + "CN=Your Administrative Group,CN=Administrative Groups,"
                             + "CN=Your Org,CN=Microsoft Exchange,CN=Services,"
                             + "CN=Configuration,DC=Yourdomain,DC=Com";
    
                   DirectoryEntry container, user;
                   CDOEXM.IMailboxStore mailbox;
    
                   //This creates the new user in the "users" container.
                   //Set the sAMAccountName and the password
                   container = new DirectoryEntry("LDAP://cn=users," + defaultNC);
                   user = container.Children.Add("cn=" + fullName, "user");
                   user.Properties["sAMAccountName"].Add(alias);
                   user.CommitChanges();
                   user.Invoke("SetPassword", new object[]{password});
                   
                   //This enables the new user.
                   user.Properties["userAccountControl"].Value = 0x200; //ADS_UF_NORMAL_ACCOUNT
                   user.CommitChanges();
    
                   //Obtain the IMailboxStore interface, create the mailbox, and commit the changes.
                   mailbox = (IMailboxStore)user.NativeObject;
                   mailbox.CreateMailbox(homeMDB);
                   user.CommitChanges();
    
                   return;
              }
         }
    }
    					
  6. Change the variables in the TODO section in the Main function so that they contain the correct values for your domain.
  7. Compile the project, and then run the program.
  8. Confirm that the new account was created in the domain by starting the Active Directory Users and Computers snap-in in Microsoft Management Console (MMC). You see the new user in the Users container. To verify that this user is mailbox-enabled, view the user's properties and note that the Exchange tabs appear, and that a mailbox store is listed for the user on the Exchange General tab.

Code description

Create a new DirectoryEntry

This code demonstrates how to bind to a container (in this case, the Users container), and how to create a new user in the container. Do not forget the "cn=" entry for the new user's name.
container = new DirectoryEntry("LDAP://cn=users," + defaultNC);
user = container.Children.Add("cn=" + fullName, "user");
				

Set properties on the new user

  1. Assign a value for sAMAccountName. This is a mandatory attribute. The user account is not created if you do not specify a value.
  2. Because you have supplied the mandatory attributes, call CommitChanges to save the new user in the directory.
  3. Call IADs::SetPassword to set the password. You must do this after a call to CommitChanges.
  4. Enable the user by modifying the userAccountControl attribute.
    user.Properties["sAMAccountName"].Add(alias);
    user.CommitChanges();
    user.Invoke("SetPassword", new object[]{password});
                   
    //This enables the new user:
    user.Properties["userAccountControl"].Value = 0x200; //ADS_UF_NORMAL_ACCOUNT
    user.CommitChanges();
    				

Create a new mailbox

  1. To get the IMailboxStore interface, cast DirectoryEntry.NativeObject to this type. This cast does not succeed at run time if CDOEXM is not installed on a computer.
  2. Call the CreateMailbox method, and pass a valid distinguished name to a mailbox store in your Exchange organization.
  3. Call CommitChanges on DirectoryEntry to save the new mailbox.
    //Obtain the IMailboxStore interface, create the mailbox, and commit the changes.
    mailbox = (IMailboxStore)user.NativeObject;
    mailbox.CreateMailbox(homeMDB);
    user.CommitChanges();
    				

Troubleshooting

  • You must have appropriate permissions in the domain to create a user and a mailbox. Typically, to create mailbox-enabled users in a Windows 2000-based domain, you must be a member of the Windows 2000 Domain Administrators group for the domain.
  • If this code runs on a computer other than an Exchange 2000 Server-based computer, you must have Exchange 2000 System Management Tools installed on the computer. If you do not, CDOEXM is not available and the cast to the IMailboxStore interface throws an InvalidCastException response:
    An unhandled exception of type 'System.InvalidCastException' occurred in MBTest.exe
    Additional information: Specified cast is not valid.
  • If you receive an error message on the call to IMailboxStore.CreateMailbox, verify that the parameter that you passed to this method is a valid mailbox store in your organization. If it is not, you receive an error message that is similar to the following:
    An unhandled exception of type 'System.Runtime.InteropServices.COMException' occurred in MBTest.exe
    Additional information: There is no such object on the server.
Posted by tornado
|

출처 : http://www.bluestudios.co.uk/blog/?p=237


Recently I came across a small problem with setting up Oracle UCM 10gR3 on a MSSQL2005 instance.
Using the net.sourceforge.jtds.jdbc.Driver class.

The DBAs had setup a clustered SQL environment and had given me MSSQLDB0001\ARCHIVETR.
ARCHIVETR being the SQL instance which I needed to install the UCM to.

For future reference all you need to do is append ‘;instance=ARCHIVETR’ to the connection string like the following:

jdbc:jtds:sqlserver://<SQLServer>:<PORT>/<DBName>;instance=<SQLInstanceName>

Example:
jdbc:jtds:sqlserver://MSSQLDB0001:1433/UCM;instance=ARCHIVETR

Posted by tornado
|
출처 : http://www.artfulsoftware.com/mysqlbook/sampler/mysqled1ch20.html


Get It Done With MySQL 5&6, Chapter 20. Copyright © Peter Brawley and Arthur Fuller 2008. All rights reserved.

TOC    Previous    Next

Working with Graphs in MySQL

Graphs and SQL   Edge list   Edge-adjacency list model of a tree
   Nested set model of a tree   Edge-list model of a network   Parts explosions

Most non-trivial data is hierarchical. Customers have orders, which have line items, which refer to products, which have prices. Population samples have subjects, who take tests, which give results, which have sub-results and norms. Web sites have pages, which have links, which collect hits, which distribute across dates and times. With such data, we know the depth of the hierarchy before we sit down to write a query. The fixed depth of the hierarchy logically limits the number of JOINs needed in a query.

But if our data describes a family tree, or a browsing history, or a bill of materials, then hierarchical depth depends on the data. We no longer know how many JOINs our query will require. We need a different data model. 

That model is the graph (Fig 1), which is a set of nodes (vertices) and the edges (lines or arcs) that connect them. This chapter is about how to model and query graphs in a MySQL database.

Graph theory is a branch of topology. It is the study of geometric relations which aren't changed by stretching and compression—rubber sheet geometry, some call it. Graph theory is ideal for modelling hierarchies—like family trees, browsing histories, search trees and bills of materials—whose shape and size we can't know in advance.

Let the set of nodes in Fig 1 be N, the set of edges be L, and the graph be G. Then G is the tuple or ordered pair {N,L}:

    N = {A,B,C,D,E,F}
    L = {AC,CD,CF,BE}
    G = {N,L}

If the edges are directed, the graph is a digraph or directed graph. A mixed graph has both directed and undirected edges.

Examples of graphs are organisational charts; itineraries; route maps; parts explosions; massively multiplayer games; language rules; chat histories; network and link analysis in a wide variety of fields, for example search engines, forensics, epidemiology and telecommunications; data mining; models of chemical structure hierarchies; and biochemical processes.

Graph characteristics and models

Nodes and edges : Two nodes are adjacent if there is an edge between them. Two edges are adjacent if they connect to a common node. In a complete graph, all nodes are adjacent to all other nodes.

In a digraph or directed graph, the number of edges entering a node is its indegree; the number leaving is its outdegree. A node of indegree zero is a root node, a node of outdegree zero is a leaf node.

In a weighted graph, used for example to solve the travelling salesman problem, edges have a weight attribute. A digraph with weighted edges is a network.

Paths and cycles: A connected sequence of edges is a path, its length the number of edges traversed. Two nodes are connected if there is a path between them. If there is a path connecting every pair of nodes, the graph is a connected graph.

A path in which no node repeats is a simple path. A path which returns to its own origin without crossing itself is a cycle or circuit. A graph with multiple paths between at least one pair of nodes is reconvergent. A reconvergent graph may be cyclic or acyclic. A unit length cycle is a loop.

If a graph's edges intersect only at nodes, it is planar. Two paths having no node in common are independent.

Traversing graphs: There are two main approaches, breadth-first and depth-first. Breadth-first traversal visits all a node's siblings before moving on to the next level, and typically uses a queue. Depth-first traversal follows edges down to leaves and back before proceeding to siblings, and typically uses a stack.

Sparsity: A graph where the size of E approaches the maximum N2 is dense. When the multiple is much smaller than N, the graph is considered sparse.

Trees: A tree is a connected graph with no cycles. It is also a graph where the indegree of the root node is 0, and the indegree of every other node is 1. A tree where every node is of outdegree <=2 is a binary tree.  A forest is a graph in which every connected component is a tree.

Euler paths: A path which traverses every edge in a graph exactly once is an Euler path. An Euler path which is a circuit is an Euler circuit.

If and only if every node of a connected graph has even degree, it has an Euler circuit (which is why the good people of Königsberg cannot go for a walk crossing each of their seven bridges exactly once). If and only if a connected graph has exactly 2 nodes with odd degree, it has a non-circuit Euler path. The degree of an endpoint of a non-cycle Euler path is 1 + twice the number of times the path passes through that node, so it is always odd.

Models for computing graphs

Traditionally, computer science textbooks have offered edge lists, adjacency lists and adjacency matrices as data structures for graphs, with algorithms implemented in languages like C, C++ and Java. More recently other models and tools have been suggested, including query languages customised for graphs.

Edge list: The simplest way to represent a graph is to list its edges: for Fig 1, the edge list is {AC,CD,CF,BE}. It is easy to add an edge to the list; deletion is a little harder.

Table 1
Nodes Adjacent nodes
A C
B E
C F,D,A
D C
E B
F C
Adjacency list: An adjacency list is a ragged array: for each node it lists all adjacent nodes. Thus it represents a directed graph of n nodes as a list of n lists where list i contains node j if the graph has an edge from node i to node j.

An undirected graph may be represented by having node j in the list for node i, and node i in the list for node j. Table 1 shows the adjacency list of the graph in Fig 1 interpreted as undirected.

Adjacency matrix: An adjacency matrix represents a graph with n nodes as an n x n matrix, where the entry at (i,j) is 1 if there is an edge from node i to node j, or zero if there is not.

An adjacency matrix can represent a weighted graph using the weight as the entry, and can represent an undirected graph by using the same entry in both (i,j) and (j,i), or by using an upper triangular matrix.

There are useful glossaries here and here.

Graphs and SQL

Often standard SQL has been thought cumbersome for graph problems. Craig Mullins once wrote that "the set-based nature of SQL is not simple to master and is anathema to the OO techniques practiced by Java developers."

A few years after Mullins wrote that, SQL is everywhere, and it is increasingly applied to graph problems. DB2 has a WITH operator for processing recursive sets. Oracle has a CONNECT BY operator for graphs that are trees. SQL Server has recursive unions. MySQL has no such enhancements for graphs, but Joe Celko and Scott Stephens, among others, have published general SQL graph problem solutions that are simpler and smaller than equivalent C++, C# or Java code. Here we implement some of these ideas in MySQL.

Beware that in ports of edge list and adjacency list methods to SQL, there has been name slippage. What's often called the adjacency list model in the SQL world is actually an edge list model. If you follow the now-common practice in the SQL world of referring to edge lists as adjacency lists, don't be surprised to find that the model isn't quite like the adjacency list in Table 1. Here we waffle. We call them edge-adjacency lists.

There are also two newer kinds of models: what Joe Celko called the nested sets modealso known as the interval modelwhich uses greater-than/less-than arithmetic to encode tree relationships and modified preorder tree traversal (MPTT) to query them, and Tropashko's materialised path model, where each node is stored with its path to the root. So we have four main possibilities for modelling graphs in MySQL:

  • edge-adjacency lists: based on an adaptation by EF Codd of the logic of linked lists to table structures and queries,
  • adjacency matrices,
  • nested sets for trees simplify some queries, but insertion and deletion are cumbersome, and
  • materialised paths.

Here we work out how to implement edge-adjacency, nested set and materialised path models— or parts of them—in MySQL 5&6.

The edge list

The edge list is the simplest way to represent a graph: minimally, a one-column table of nodes and a two-column table of edges. The edges table can be thought of as a nodes-nodes bridge table, each row containing pointers to origin and destination nodes. Other details of nodes and edges can be encoded in extra nodes and edges columns, or in child tables.

In the real world, the nodes table might be a table of personnel, or assembly parts, or locations on a map. It might have many other columms of data. The edges table might also have additional columns for edge properties. The key integers of both tables might be BIGINTs.

To model Fig 1, though, we keep things as simple as possible:

Listing 1
CREATE TABLE nodes(
  nodeID CHAR(1) PRIMARY KEY
);
CREATE TABLE edges(
  childID CHAR(1) NOT NULL,
  parentID CHAR(1) NOT NULL,
  PRIMARY KEY(childID,parentID)
);
INSERT INTO nodes VALUES('A'), ('B'), ('C'), ('D'), ('E'), ('F');
INSERT INTO edges VALUES ('A','C'), ('C','D'), ('C','F'), ('B','E');
SELECT * FROM edges;
+---------+----------+
| childID | parentID |
+---------+----------+
| A       | C        |
| B       | E        |
| C       | D        |
| C       | F        |
+---------+----------+

Now, without any assumptions whatever about whether the graph is connected, whether it is directed, whether it is a tree, or whatever, how hard is it to write a reachability procedure, a procedure which tells us where we can get to from here, wherever 'here' is?

A simple approach is a breadth-first search:

  1. Seed the list with the starting node,
  2. Add, but do not duplicate, nodes which are children of nodes in the list,
  3. Add, but do not duplicate, nodes which are parents of nodes in the list,
  4. Repeat steps 2 and 3 until there are no more nodes to add.

Here it is as a MySQL stored procedure. It avoids duplicate nodes by defining reached.nodeID as a primary key and adding reachable nodes with INSERT IGNORE.

If you are running MySQL 5.0.6 through 5.0.15, you will have to make two changes to this and subsequent procedures: move CREATE TABLE statements outside the procedures, and set log_bin_trust_routine_creators=TRUE.

Listing 2
DROP PROCEDURE IF EXISTS ListReached;
DELIMITER |

CREATE PROCEDURE ListReached( IN root CHAR(1) )
BEGIN
  DECLARE rows SMALLINT DEFAULT 0;
  DROP TABLE IF EXISTS reached;
  CREATE TABLE reached (
    nodeID CHAR(1) PRIMARY KEY
  ) ENGINE=HEAP;
  INSERT INTO reached VALUES (root );
  SET rows = ROW_COUNT();
  WHILE rows > 0 DO
    INSERT IGNORE INTO reached
      SELECT DISTINCT childID
      FROM edges AS e
      INNER JOIN reached AS p ON e.parentID = p.nodeID;
    SET rows = ROW_COUNT();
    INSERT IGNORE INTO reached
      SELECT DISTINCT parentID
      FROM edges AS e
      INNER JOIN reached AS p ON e.childID = p.nodeID;
    SET rows = rows + ROW_COUNT();
  END WHILE;
  SELECT * FROM reached;
  DROP TABLE reached;
END;
|
DELIMITER ;
CALL ListReached('A');
+--------+
| nodeID |
+--------+
| A      |
| C      |
| D      |
| F      |
+--------+

To make the procedure more versatile, give it input parameters which tell it whether to list child, parent or all connections, and whether to recognise loops (for example C to C).

To give the model referential integrity, use InnoDB and make edges.childID and edges.parentID foreign keys. To add or delete a node, add or delete desired single rows in nodes and edges. To change an edge, edit it. The model does not require the graph to be connected or treelike, and does not presume direction.

The edge list is basic to what SQLers often call the adjacency list model.

Edge-adjacency list model of a tree

Writers in the SQL graph literature often give solutions using single denormalised tables. Denormalisation can cost, big time. The bigger the table, the bigger the cost. You cannot edit nodes and edges separately. Carrying extra node information during edge computation slows performance. With nodes and edges denormalised to one table, you have to represent the root node with a NULL.

Normalisation banishes these difficulties. For William Shakespeare's family tree (Fig 2) we again use two tables, a family table describing family members (nodes), and a familytree table with a row for each tie that binds (edges). Later, when we use a different tree model, we won't have to mess with the data being modelled.

Listing 3
-- Base data:
CREATE TABLE family (
  ID smallint(6) PRIMARY KEY AUTO_INCREMENT,
  name char(20) default '',
  siborder tinyint(4) default NULL,
  born smallint(4) unsigned default NULL,
  died smallint(4) unsigned default NULL
);
INSERT INTO family VALUES (1, 'Richard Shakespeare', NULL, NULL, 1561);
INSERT INTO family VALUES (2, 'Henry Shakespeare', 1, NULL, 1569);
INSERT INTO family VALUES (3, 'John Shakespeare', 2, 1530, 1601);
INSERT INTO family VALUES (4, 'Joan Shakespeare', 1, 1558, NULL);
INSERT INTO family VALUES (5, 'Margaret Shakespeare', 2, 1562, 1563);
INSERT INTO family VALUES (6, 'William Shakespeare', 3, 1564, 1616);
INSERT INTO family VALUES (7, 'Gilbert Shakespeare', 4, 1566, 1612);
INSERT INTO family VALUES (8, 'Joan Shakespeare', 5, 1568, 1646);
INSERT INTO family VALUES (9, 'Anne Shakespeare', 6, 1571, 1579);
INSERT INTO family VALUES (10, 'Richard Shakespeare', 7, 1574, 1613);
INSERT INTO family VALUES (11, 'Edmond Shakespeare', 8, 1580, 1607);
INSERT INTO family VALUES (12, 'Susana Shakespeare', 1, 1583, 1649);
INSERT INTO family VALUES (13, 'Hamnet Shakespeare', 1, 1585, 1596);
INSERT INTO family VALUES (14, 'Judith Shakespeare', 1, 1585, 1662);
INSERT INTO family VALUES (15, 'William Hart', 1, 1600, 1639);
INSERT INTO family VALUES (16, 'Mary Hart', 2, 1603, 1607);
INSERT INTO family VALUES (17, 'Thomas Hart', 3, 1605, 1670);
INSERT INTO family VALUES (18, 'Michael Hart', 1, 1608, 1618);
INSERT INTO family VALUES (19, 'Elizabeth Hall', 1, 1608, 1670);
INSERT INTO family VALUES (20, 'Shakespeare Quiney', 1, 1616, 1617);
INSERT INTO family VALUES (21, 'Richard Quiney', 2, 1618, 1639);
INSERT INTO family VALUES (22, 'Thomas Quiney', 3, 1620, 1639);
INSERT INTO family VALUES (23, 'John Bernard', 1, NULL, 1674);

-- Table which models the tree:
CREATE TABLE familytree (
  childID smallint NOT NULL,
  parentID smallint NOT NULL,
  PRIMARY KEY (childID, parentID);
);
INSERT INTO familytree VALUES
  (2, 1), (3, 1), (4, 2), (5, 2), (6, 2), (7, 2), (8, 2), (9, 2),
  (10, 2), (11, 2), (12, 6), (13, 6), (14, 6), (15, 8), (16, 8),
  (17, 8), (18, 8), (19, 12), (20, 14), (21, 14), (22, 14), (23, 19);

(The family PK is auto-increment, but the listing is more reader-friendly when the ID values are shown.)

It will be useful to have a function that returns the family.name for a pointer in familytree. Listing 4 shows that function, and simple queries which call it to retrieve child and parent names:

Listing 4
-- 5.0.16 OR LATER:
SET GLOBAL log_bin_trust_function_creators=TRUE;

DROP FUNCTION IF EXISTS PersonName;
DELIMITER |

CREATE FUNCTION PersonName( personID SMALLINT )
RETURNS CHAR(20)
BEGIN
  DECLARE pname CHAR(20) DEFAULT '';
  SELECT name INTO pname FROM family WHERE ID=personID;
  RETURN pname;
END;
|
DELIMITER ;

SELECT PersonName( parentID ) AS 'Parent of William'
FROM familytree
WHERE childID = 6;
+-------------------+
| Parent of William |
+-------------------+
| Henry Shakespeare |
+-------------------+
SELECT PersonName( childID ) AS 'Children of William'
FROM familytree
WHERE parentID = (
  SELECT ID FROM family
  WHERE name = 'William Shakespeare'
);
+---------------------+
| Children of William |
+---------------------+
| Susana Shakespeare  |
| Hamnet Shakespeare  |
| Judith Shakespeare  |
+---------------------+
SELECT
  PersonName(childID) AS child,
  PersonName(parentID) AS parent
FROM familytree;
+----------------------+---------------------+
| child                | parent              |
+----------------------+---------------------+
| Henry Shakespeare    | Richard Shakespeare |
| John Shakespeare     | Richard Shakespeare |
| Joan Shakespeare     | Henry Shakespeare   |
| Margaret Shakespeare | Henry Shakespeare   |
| William Shakespeare  | Henry Shakespeare   |
| Gilbert Shakespeare  | Henry Shakespeare   |
| Joan Shakespeare     | Henry Shakespeare   |
| Anne Shakespeare     | Henry Shakespeare   |
| Richard Shakespeare  | Henry Shakespeare   |
| Edmond Shakespeare   | Henry Shakespeare   |
| Susana Shakespeare   | William Shakespeare |
| Hamnet Shakespeare   | William Shakespeare |
| Judith Shakespeare   | William Shakespeare |
| William Hart         | Joan Shakespeare    |
| Mary Hart            | Joan Shakespeare    |
| Thomas Hart          | Joan Shakespeare    |
| Michael Hart         | Joan Shakespeare    |
| Elizabeth Hall       | Susana Shakespeare  |
| Shakespeare Quiney   | Judith Shakespeare  |
| Richard Quiney       | Judith Shakespeare  |
| Thomas Quiney        | Judith Shakespeare  |
| John Bernard         | Elizabeth Hall      |
+----------------------+---------------------+

GROUP_CONCAT() simplifies grouping parents with their children:

Listing 5
SELECT
  PersonName(parentID) AS Parent,
  GROUP_CONCAT( PersonName(childID) SEPARATOR ', ' ) AS Children
FROM familytree
GROUP BY parentID;
+---------------------+----------------------------------------------------------------------------------+
| Parent              | Children                                                                         |
+---------------------+----------------------------------------------------------------------------------+
| Richard Shakespeare | Henry Shakespeare, John Shakespeare                                              |
| Henry Shakespeare   | Edmond Shakespeare, Richard Shakespeare, Anne Shakespeare, Joan Shakespeare,     |
|                     | Gilbert Shakespeare, William Shakespeare, Margaret Shakespeare, Joan Shakespeare |
| William Shakespeare | Judith Shakespeare, Hamnet Shakespeare, Susana Shakespeare                       |
| Joan Shakespeare    | William Hart, Mary Hart, Thomas Hart, Michael Hart                               |
| Susana Shakespeare  | Elizabeth Hall                                                                   |
| Judith Shakespeare  | Shakespeare Quiney, Richard Quiney, Thomas Quiney                                |
| Elizabeth Hall      | John Bernard                                                                     |
+---------------------+----------------------------------------------------------------------------------+

The paterfamilias is the root node, individuals with no children are the leaf nodes, and queries to retrieve subtree statistics are straightforward:

Listing 6
SELECT
  PersonName(ID) AS Paterfamilias,
  IFNULL(born,'?') AS Born,
  IFNULL(died,'?') AS Died
FROM family AS f
LEFT JOIN familytree AS t ON f.ID=t.childID
WHERE t.childID IS NULL;
+---------------------+------+------+
| Paterfamilias       | Born | Died |
+---------------------+------+------+
| Richard Shakespeare | ?    | 1561 |
+---------------------+------+------+

SELECT
  PersonName(ID) AS Childless,
  IFNULL(born,'?') AS Born,
  IFNULL(died,'?') AS Died
FROM family AS f
LEFT JOIN familytree AS t ON f.ID=t.parentID
WHERE t.parentID IS NULL;
+----------------------+------+------+
| Childless            | Born | Died |
+----------------------+------+------+
| John Shakespeare     | 1530 | 1601 |
| Joan Shakespeare     | 1558 | ?    |
| Margaret Shakespeare | 1562 | 1563 |
| Gilbert Shakespeare  | 1566 | 1612 |
| Anne Shakespeare     | 1571 | 1579 |
| Richard Shakespeare  | 1574 | 1613 |
| Edmond Shakespeare   | 1580 | 1607 |
| Hamnet Shakespeare   | 1585 | 1596 |
| William Hart         | 1600 | 1639 |
| Mary Hart            | 1603 | 1607 |
| Thomas Hart          | 1605 | 1670 |
| Michael Hart         | 1608 | 1618 |
| Shakespeare Quiney   | 1616 | 1617 |
| Richard Quiney       | 1618 | 1639 |
| Thomas Quiney        | 1620 | 1639 |
| John Bernard         | ?    | 1674 |
+----------------------+------+------+

SELECT ROUND(AVG(died-born),2) AS 'Longevity of the childless'
FROM family AS f
LEFT JOIN familytree AS t ON f.ID=t.parentID
WHERE t.parentID IS NULL;
+----------------------------+
| Longevity of the childless |
+----------------------------+
|                      25.86 |
+----------------------------+

In marked contrast with Celko's nested sets model, inserting a new item in this model requires no revision of existing rows. We just add a new family row, then a new familytree row with IDs specifying who is the parent of whom. Deletion is also a two-step: delete the familytree row which documents the child-parent link, then delete the family row for that child.

Retrieving subtrees in the edge-adjacency list model

Retrieving subtrees is what gives the edge-adjacency list model its reputation for difficulty. We can't know in advance, except in the simplest of trees, how many levels of parent and child have to be queried, so we need recursion or a logically equivalent loop.

It's a natural problem for a stored procedure. Here is a breadth-first algorithm that is straightforward, though not optimised:

  1. Create a table for the results, descendants,
  2. Create a HEAP table, nextparents, for the next parents whose children are to be found , and another HEAP table for use as a temp copy,
  3. Seed nextparents with the name of the ancestor whose descendants we wish to list,
  4. Add to descendants all children of rows in nextparents,
  5. Seed nextparents with those children's IDs, and if there are any, loop back to 4.

As a convenience, the procedure accepts either a name or numeric ID argument. (With MySQL 5.0.6 or 5.0.7, you will have to move the CREATE TABLE calls outside the procedure, to step around a MySQL bug that was fixed in 5.0.9.)

Listing 7
DROP PROCEDURE IF EXISTS ListDescendants;
DELIMITER |

CREATE PROCEDURE ListDescendants( ancestor CHAR(20) )
BEGIN
  DECLARE rows, iLevel, iMode INT DEFAULT 0;
  -- create temp tables
  DROP TEMPORARY TABLE IF EXISTS descendants,nextparents,prevparents;
  CREATE TEMPORARY TABLE descendants( childID INT, parentID INT, level INT );
  CREATE TEMPORARY TABLE IF NOT EXISTS nextparents ( parentID SMALLINT) ENGINE=MEMORY;
  CREATE TEMPORARY TABLE prevparents LIKE nextparents;
  -- seed nextparents
  IF ancestor RLIKE '[:alpha:]+' THEN      -- ancestor passed as a string
    INSERT INTO nextparents SELECT id FROM family WHERE name=ancestor;
  ELSE
    SET iMode = 1;                         -- ancestor passed as a numeric
    INSERT INTO nextparents VALUES( CAST( ancestor AS UNSIGNED ));
  END IF;
  SET rows = ROW_COUNT();
  WHILE rows > 0 DO
    -- add children of nextparents
    SET iLevel = iLevel + 1;
    INSERT INTO descendants
      SELECT t.childID, t.parentID, iLevel
      FROM familytree AS t
      INNER JOIN nextparents USING(parentID);
    SET rows = ROW_COUNT();
    -- save nextparents to prevparents
    TRUNCATE prevparents;
    INSERT INTO prevparents
      SELECT * FROM nextparents;
    -- next parents are children of these parents:
    TRUNCATE nextparents;
    INSERT INTO nextparents
      SELECT childID FROM familytree
      INNER JOIN prevparents USING (parentID);
    SET rows = rows + ROW_COUNT();
  END WHILE;  
  -- result
  IF iMode = 1 THEN
    SELECT CONCAT(REPEAT( ' ', level), parentID ) As Parent, GROUP_CONCAT(childID) AS Child
    FROM descendants GROUP BY parentID ORDER BY level;
  ELSE
    SELECT CONCAT(REPEAT( ' ', level), PersonName(parentID) ) As Parent, PersonName(childID) AS Child
    FROM descendants;
  END IF;
  DROP TEMPORARY TABLE descendants,nextparents,prevparents;
END;
|
DELIMITER ;
CALL ListDescendants( 1 );
+---------+-------------------+
| Parent  | Child             |
+---------+-------------------+
|  1      | 2,3               |
|   2     | 11,10,9,8,7,6,5,4 |
|    6    | 14,13,12          |
|    8    | 15,16,17,18       |
|     12  | 19                |
|     14  | 20,21,22          |
|      19 | 23                |
+---------+-------------------+
CALL ListDescendants( 'Richard Shakespeare' );
+------------------------+----------------------+
| Parent                 | Child                |
+------------------------+----------------------+
|  Richard Shakespeare   | Henry Shakespeare    |
|  Richard Shakespeare   | John Shakespeare     |
|   Henry Shakespeare    | Joan Shakespeare     |
|   Henry Shakespeare    | Margaret Shakespeare |
|   Henry Shakespeare    | William Shakespeare  |
|   Henry Shakespeare    | Gilbert Shakespeare  |
|   Henry Shakespeare    | Joan Shakespeare     |
|   Henry Shakespeare    | Anne Shakespeare     |
|   Henry Shakespeare    | Richard Shakespeare  |
|   Henry Shakespeare    | Edmond Shakespeare   |
|    William Shakespeare | Susana Shakespeare   |
|    William Shakespeare | Hamnet Shakespeare   |
|    William Shakespeare | Judith Shakespeare   |
|    Joan Shakespeare    | William Hart         |
|    Joan Shakespeare    | Mary Hart            |
|    Joan Shakespeare    | Thomas Hart          |
|    Joan Shakespeare    | Michael Hart         |
|     Susana Shakespeare | Elizabeth Hall       |
|     Judith Shakespeare | Shakespeare Quiney   |
|     Judith Shakespeare | Richard Quiney       |
|     Judith Shakespeare | Thomas Quiney        |
|      Elizabeth Hall    | John Bernard         |
+------------------------+----------------------+

Not too bad. Notice how little of the code is specific to the Shakespeare example: the logic will port easily to any other edge list. In fact let's prove that right now by rewriting ListDescendants() for the general case. We need six parameters to drive three PREPAREd queries:

  • the name and key column of the data table (family, ID in the Shakespeare example),
  • the name of the edge table, its ID column and its parentID column (familytree, childID, parentID),
  • a parameter for the ancestorID whose subtree is to be listed:
Listing 7a: General-purpose edge list subtree walker
DROP PROCEDURE IF EXISTS GenericSubtree;
DELIMITER |
CREATE PROCEDURE GenericSubtree( 
  dataTable CHAR(64),
  dataIDcol CHAR(64),
  edgeTable CHAR(64),
  edgeIDcol CHAR(64),
  edgeParentIDcol CHAR(64),
  ancestorID INT
)
BEGIN
  DECLARE rows INT DEFAULT 0;
  -- create temp tables
  DROP TEMPORARY TABLE IF EXISTS descendants,nextparents,prevparents;
  CREATE TEMPORARY TABLE descendants( id INT, parentID INT );
  CREATE TEMPORARY TABLE IF NOT EXISTS nextparents ( parentID INT ) ENGINE=MEMORY;
  CREATE TEMPORARY TABLE prevparents LIKE nextparents;
  -- seed nextparents table with ancestorID
  INSERT INTO nextparents VALUES (ancestorID);
  SET rows = ROW_COUNT();
  WHILE rows > 0 DO
    -- insert children of nextparents into descendants
    SET @sql = CONCAT( 'INSERT INTO descendants SELECT t.', edgeIDcol, ',t.', 
                       edgeParentIDcol, ' FROM ', edgeTable, ' AS t JOIN nextparents n ON t.', 
                       edgeParentIDcol, ' = n.parentID'
                     );
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    SET rows = ROW_COUNT();
    DROP PREPARE stmt;
    -- save copy of nextparents to prevparents
    TRUNCATE prevparents;
    INSERT INTO prevparents SELECT * FROM nextparents;
    -- insert the children of these parents into nextparents:
    TRUNCATE nextparents;
    SET @sql = CONCAT( 'INSERT INTO nextparents SELECT ', edgeIDcol, ' FROM ', edgeTable,
                       ' t JOIN prevparents p ON t.', edgeParentIDcol, ' = p.parentID'
                     );
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    SET rows = rows + ROW_COUNT();
    DROP PREPARE stmt;
  END WHILE;
  -- show the result
  SET @sql = CONCAT( 'SELECT t.* FROM descendants d',
                     ' JOIN ', edgeTable, ' e ON d.ID = e.', edgeIDCol,
                     ' JOIN ', dataTable, ' t ON e.', edgeIDcol, '=t.', dataIDcol
                   );
  PREPARE stmt FROM @sql;
  EXECUTE stmt;
  DROP PREPARE stmt;
  -- clean up
  DROP TEMPORARY TABLE descendants,nextparents,prevparents;
END;
|
DELIMITER ;

Does it work? Let's have William's descendants again:

CALL GenericSubtree( 'family', 'ID', 'familytree', 'childID', 'parentID', 
                     (SELECT ID from family where name='William Shakespeare') );
+----+--------------------+----------+------+------+
| ID | name               | siborder | born | died |
+----+--------------------+----------+------+------+
| 12 | Susana Shakespeare |        1 | 1583 | 1649 |
| 13 | Hamnet Shakespeare |        1 | 1585 | 1596 |
| 14 | Judith Shakespeare |        1 | 1585 | 1662 |
| 19 | Elizabeth Hall     |        1 | 1608 | 1670 |
| 20 | Shakespeare Quiney |        1 | 1616 | 1617 |
| 21 | Richard Quiney     |        2 | 1618 | 1639 |
| 22 | Thomas Quiney      |        3 | 1620 | 1639 |
| 23 | John Bernard       |        1 | NULL | 1674 |
+----+--------------------+----------+------+------+

Is it fast? No. But once we have this algorithm, pruning subtrees is no harder than calling GenericSubtree() then deleting the listed rows. Better still, write a generic tree pruner from Listing 7a replacing the final SELECT with a DELETE command. To insert a subtree, prepare a table of new rows, point its top edge at an existing node as parent, and INSERT it. 

For speed, try recursion! Here is a recursive depth-first PHP treewalk for the  familytree and family tables:

Listing 7b: Recursive edge list subtree in PHP
$info = recursivesubtree( 1, $a = array(), 0 );
foreach( $info as $row ) 
  echo str_repeat( "&nbsp;", 2*$row[4] ), ( $row[3] > 0 ) ? "<b>{$row[1]}</b>" : $row[1], "<br/>";

function recursivesubtree( $rootID, $a, $level ) {
  $childcountqry = "(SELECT COUNT(*) FROM familytree WHERE parentID=t.childID) AS childcount";
  $qry = "SELECT t.childid,f.name,t.parentid,$childcountqry,$level " .
         "FROM familytree t JOIN family f ON t.childID=f.ID " .
         "WHERE parentid=$rootID ORDER BY childcount<>0,t.childID";
  $res = mysql_qry( $qry );
  while( $row = mysql_fetch_row( $res )) {
    $a[] = $row;
    if( $row[3] > 0 ) $a = recursivesubtree( $row[0], $a, $level+1 );    // down before right
  }
  return $a;
}

A query with a subquery, a fetch loop, and a recursive call--that's all there is to it. To port this to MySQL, you must have set maximum recursion depth in my.cnf/ini or in your client:

Listing 7c: Recursive edge list subtree in MySQL
SET @@SESSION.max_sp_recursion_depth=25;
DROP PROCEDURE IF EXISTS recursivesubtree;
DELIMITER |
CREATE PROCEDURE recursivesubtree( iroot INT, ilevel INT )
BEGIN
  DECLARE ichildid, iparentid,ichildcount,done INT DEFAULT 0;
  DECLARE cname VARCHAR(64);
  DECLARE cur CURSOR FOR
  SELECT 
    t.childid,t.parentid,f.name,
    (SELECT COUNT(*) FROM familytree WHERE parentID=t.childID) AS childcount
  FROM familytree t JOIN family f ON t.childID=f.ID
  WHERE parentid=iroot ORDER BY childcount<>0,t.childID;
  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;
  IF ilevel = 0 THEN
    DROP TEMPORARY TABLE IF EXISTS descendants;
    CREATE TEMPORARY TABLE descendants(
      childID INT,parentID INT,name VARCHAR(64),childcount INT,level INT
    );
  END IF;
  OPEN cur;
  WHILE NOT done DO
    FETCH cur INTO ichildid,iparentid,cname,ichildcount;
    IF NOT done THEN
      INSERT INTO descendants VALUES(ichildid,iparentid,cname,ichildcount,ilevel );
      IF ichildcount > 0 THEN
        CALL recursivesubtree( ichildid, ilevel + 1 );
      END IF;
    END IF;
  END WHILE;
  CLOSE cur;
END;
|
DELIMITER ;
CALL recursivesubtree(1,0);
SELECT CONCAT(REPEAT(' ',2*level),IF(childcount,UPPER(name),name)) AS 'Richard\'s Descendants'
FROM descendants;
+--------------------------+
| Richard's Descendants    |
+--------------------------+
| John Shakespeare         |
| HENRY SHAKESPEARE        |
|   Joan Shakespeare       |
|   Margaret Shakespeare   |
|   Gilbert Shakespeare    |
|   Anne Shakespeare       |
|   Richard Shakespeare    |
|   Edmond Shakespeare     |
|   WILLIAM SHAKESPEARE    |
|     Hamnet Shakespeare   |
|     SUSANA SHAKESPEARE   |
|       ELIZABETH HALL     |
|         John Bernard     |
|     JUDITH SHAKESPEARE   |
|       Shakespeare Quiney |
|       Richard Quiney     |
|       Thomas Quiney      |
|   JOAN SHAKESPEARE       |
|     William Hart         |
|     Mary Hart            |
|     Thomas Hart          |
|     Michael Hart         |
+--------------------------+

The recursive depth-first treewalk is faster than the breadth-first procedures. It is also faster than a MySQL version of Kendall Willet's depth-first edge list subtree algorithm:

Listing 7d: Depth-first edge list subtree 
CREATE PROCEDURE depthfirstsubtree( iroot INT )
BEGIN
  DECLARE ilastvisited, inxt, ilastord INT;
  SET ilastvisited = iroot;
  SET ilastord = 1;
  DROP TABLE IF EXISTS descendants;
  CREATE TABLE descendants SELECT childID,parentID,-1 AS ord FROM familytree;
  UPDATE descendants SET ord=1 WHERE childID=iroot;
  this: LOOP
    SET inxt = NULL;
    SELECT MIN(childID) INTO inxt FROM descendants   -- go down
    WHERE parentID = ilastvisited AND ord = -1 ;
    IF inxt IS NULL THEN                             -- nothing down, so go right   
      SELECT MIN(d2.childID) INTO inxt 
      FROM descendants d1
      JOIN descendants d2 ON d1.parentID = d2.parentID AND d1.childID < d2.childID
      WHERE d1.childID = ilastvisited;
    END IF;
    IF inxt IS NULL THEN                             -- nothing right. so go up        
      SELECT parentID INTO inxt FROM descendants
      WHERE childID = ilastvisited AND parentID IS NOT NULL;
    END IF;
    UPDATE descendants SET ord = ilastord + 1
    WHERE childID = inxt AND ord = -1;
    IF ROW_COUNT() > 0 THEN
      SET ilastord = ilastord + 1;
    END IF;
    IF inxt IS NULL THEN
      LEAVE this;
    END IF;
    SET ilastvisited = inxt;
  END LOOP;
END;

One reason Willet's is slower is that MySQL does not permit use of temporary tables with its queries; when all algorithms are denied temp tables, though, this algorithm is still slower than recursion.

Edge list subtrees are easier to query than their reputation suggests. And edge tables are flexible. For a tree describing a parts explosion rather than a family, just add columns for weight, quantity, assembly time, cost, price and so on. Reports need only aggregate column values and sums. We'll revisit this near the end of the chapter.

Enumerating paths in an edge-adjacency list

Path enumeration in an edge list tree is almost as easy as depth-first subtree traversal:

  • create a table for paths,
  • seed it with paths of unit length from the tree table,
  • iteratively add paths till there are no more to add.

MySQL's INSERT IGNORE command simplifies the code by removing the need for a NOT EXISTS(...) clause in the INSERT ... SELECT statement. Since adjacencies are logically symmetrical, we make path direction the caller's choice, UP or DOWN:

Listing 8
DROP PROCEDURE IF EXISTS ListAdjacencyPaths;
DELIMITER |
CREATE PROCEDURE ListAdjacencyPaths( IN direction CHAR(5) )
BEGIN
  DROP TABLE IF EXISTS paths;
  CREATE TABLE paths(
    start SMALLINT,
    stop SMALLINT,
    len SMALLINT,
    PRIMARY KEY(start,stop)
  ) ENGINE=HEAP;
  IF direction = 'UP' THEN
    INSERT INTO paths
      SELECT childID,parentID,1
      FROM familytree;
  ELSE
    INSERT INTO paths
      SELECT parentID,childID,1
      FROM familytree;
  END IF;
  WHILE ROW_COUNT() > 0 DO
    INSERT IGNORE INTO paths
      SELECT DISTINCT
        p1.start,
        p2.stop,
        p1.len + p2.len
      FROM paths AS p1 INNER JOIN paths AS p2 ON p1.stop = p2.start;
  END WHILE;
  SELECT start, stop, len
  FROM paths
  ORDER BY start, stop;
  DROP TABLE paths;
END;
|
DELIMITER ;

To find the paths from just one node, seed the paths table with paths from the starting node, then iteratively search a JOIN of familytree and paths for edges which will extend existing paths in the user-specified direction:

Listing 9
DROP PROCEDURE IF EXISTS ListAdjacencyPathsOfNode;
DELIMITER |
CREATE PROCEDURE ListAdjacencyPathsOfNode( IN node SMALLINT, IN direction CHAR(5) )
BEGIN
  TRUNCATE paths;
  IF direction = 'UP' THEN
    INSERT INTO paths
      SELECT childID,parentID,1
      FROM familytree
      WHERE childID = node;
  ELSE
    INSERT INTO paths
      SELECT parentID,childID,1
      FROM familytree
      WHERE parentID = node;
  END IF;
  WHILE ROW_COUNT() > 0 DO
    IF direction = 'UP' THEN
      INSERT IGNORE INTO paths
        SELECT DISTINCT
          paths.start,
          familytree.parentID,
          paths.len + 1
        FROM paths
          INNER JOIN familytree ON paths.stop = familytree.childID;
    ELSE
      INSERT IGNORE INTO paths
        SELECT DISTINCT
          paths.start,
          familytree.childID,
          paths.len + 1
        FROM paths
          INNER JOIN familytree ON paths.stop = familytree.parentID;

    END IF;
  END WHILE;
    SELECT start, stop, len
    FROM paths
    ORDER BY start, stop;
END;
|
DELIMITER ;

CALL ListAdjacencyPathsOfNode(1,'DOWN');
+-------+------+------+
| start | stop | len  |
+-------+------+------+
|     1 |    2 |    1 |
|     1 |    3 |    1 |
|     1 |    4 |    2 |
|     1 |    5 |    2 |
|     1 |    6 |    2 |
|     1 |    7 |    2 |
|     1 |    8 |    2 |
|     1 |    9 |    2 |
|     1 |   10 |    2 |
|     1 |   11 |    2 |
|     1 |   12 |    3 |
|     1 |   13 |    3 |
|     1 |   14 |    3 |
|     1 |   15 |    3 |
|     1 |   16 |    3 |
|     1 |   17 |    3 |
|     1 |   18 |    3 |
|     1 |   19 |    4 |
|     1 |   20 |    4 |
|     1 |   21 |    4 |
|     1 |   22 |    4 |
|     1 |   23 |    5 |
+-------+------+------+

These algorithms don't bend the brain. They perform acceptably with large trees. Querying edge-adjacency lists for subtrees and paths is less daunting than their reputation suggests.

Nested sets model of a tree

Imagine an oval drawn round every leaf and every subtree in Fig 2, and a final oval round the entire tree. The tree is a set. Each subtree is a subset. That's the basic idea of the nested sets model.

The advantage of the nested sets model is that root, leaves, subtrees, levels, tree height, ancestors, descendants and paths can be retrieved without recursion or application language code. The disadvantages are:

  • initial setup of the tree table can be difficult,
  • queries for parents (immediate superiors) and children (immediate subordinates) are more complicated than with an edge list model,
  • insertion, updates and deletion are extremely cumbersome since they may require updates to much of the tree.

The nested sets model depends on using a modified preorder tree traversal (MPTT) depth-first algorithm to assign each node left and right integers which define the node's tree position. All nodes of a subtree have

  • left values greater than the subtree parent's left value, and
  • right values smaller than that of the subtree parent's right value.

so queries for subtrees are dead simple. If the numbering scheme is integer-sequential as in Fig 3, the root node receives a left value of 1 and a right value equal to twice the item count.

To see how to code nested sets using MPTT, trace the ascending integers in Fig 3, starting with 1 on the left side of the root node (Richard Shakespeare). Following edges downward and leftward, the left side of each box gets the next integer. When you reach a leaf (Joan, left=3), the right side of that box gets the next integer (4). If there is another node to the right on the same level, continue in that direction; otherwise continue up the right side of the subtree you just descended. When you arrive back at the root on the right side, you're done. Down, right and up.

A serious problem with this scheme jumps out right away: after you've written the Fig 3 tree to a table, what if historians discover an older brother or sister of Henry and John? Every row in the tree table must be updated!

Celko and others have proposed alternative numbering schemes to get round this problem, but the logical difficulty remains: inserts and updates can invalidate many or all rows, and no SQL CHECK or CONSTRAINT can prevent it. The nested sets model is not good for trees which require frequent updates, and is pretty much unsupportable for large updatable trees that will be accessed by many concurrent users. But as we'll see in a moment, it can be very useful indeed for reporting a tree.

How to build a nested set representation from an edge list

Obviously, numbering a tree by hand will be error-prone, and seriously impractical for large trees. It's usually best, therefore, to code the tree initially as an edge-adjacency list, then use a stored procedure to translate the edge list to a nested sets model. We can write a stored procedure that uses Celko's pushdown stack method to translate any edge list into a nested sets tree:

  1. create a table for the tree: node, leftedge, rightedge, and a stack pointer (top),
  2. seed that table, nestedsettree, with the root node of the adjacency tree,
  3. set a counter to 1 plus the left value of the root node, i.e. 2,
  4. while that counter is less than the right value of the root node ...
    • insert rows for children of parent rows into nestedsettree, or
    • update existing right integers in nestedsettree.
Listing 10
DROP PROCEDURE IF EXISTS EdgeListToNestedSet;
DELIMITER |
CREATE PROCEDURE EdgeListToNestedSet( edgeTable CHAR(64), idCol CHAR(64), parentCol CHAR(64) )
BEGIN
  DECLARE maxrightedge, rows SMALLINT DEFAULT 0;
  DECLARE current SMALLINT DEFAULT 1;
  DECLARE nextedge SMALLINT DEFAULT 2;
  -- create working tree table as a copy of edgeTable
  DROP TEMPORARY TABLE IF EXISTS tree;
  CREATE TEMPORARY TABLE tree( childID INT, parentID INT );
  SET @sql = CONCAT( 'INSERT INTO tree SELECT ', idCol, ',', parentCol, ' FROM ', edgeTable );
  PREPARE stmt FROM @sql;
  EXECUTE stmt;
  DROP PREPARE stmt;
  -- initialise result table
  DROP TABLE IF EXISTS nestedsettree;
  CREATE TABLE nestedsettree (
    top SMALLINT, nodeID SMALLINT, leftedge SMALLINT, rightedge SMALLINT
  ) ENGINE=HEAP;
  -- find root of tree (or: add an sproc param for the desired root value)
  SET @sql = CONCAT( 'SELECT DISTINCT f.', parentCol, ' INTO @root FROM ',
                     edgeTable, ' AS f LEFT JOIN ', edgeTable, ' AS t ON f.',
                     parentCol, '=', 't.', idCol, ' WHERE t.', idCol, ' IS NULL'
                   );
  -- For the familytree table, the above query parses to:
  -- SELECT DISTINCT f.parentid INTO @root 
  -- FROM familytree AS f LEFT JOIN familytree AS t ON f.parentid=t.childid 
  -- WHERE t.childid IS NULL |
  PREPARE stmt FROM @sql;
  EXECUTE stmt;
  DROP PREPARE stmt;
  -- build nested sets tree
  SET maxrightedge = 2 * (1 + (SELECT + COUNT(*) FROM tree));
  INSERT INTO nestedsettree VALUES( 1, @root, 1, maxrightedge );
  WHILE nextedge < maxrightedge DO
    SELECT *
    FROM nestedsettree AS s
    JOIN tree AS t ON s.nodeID=t.parentID AND s.top=current;
    SET rows = FOUND_ROWS();
    IF rows > 0 THEN
      BEGIN
        INSERT INTO nestedsettree
          SELECT current+1, MIN(t.childID), nextedge, NULL
          FROM nestedsettree AS s
          JOIN tree AS t ON s.nodeID = t.parentID AND s.top = current;
        DELETE FROM tree
        WHERE childID = (SELECT nodeID FROM nestedsettree WHERE top=(current+1));
        SET nextedge = nextedge + 1;
        SET current = current + 1;
       END;
     ELSE
       BEGIN
         UPDATE nestedsettree
         SET rightedge=nextedge, top = -top
         WHERE top=current;
         SET nextedge=nextedge+1;
         SET current=current-1;
       END;
    END IF;
  END WHILE;
  SET rows := ( SELECT COUNT(*) FROM tree );
  DROP TEMPORARY TABLE tree;
  -- show result
  IF rows > 0 THEN
    SELECT 'Orphaned rows remain';
  ELSE
    SELECT nodeID, leftedge AS 'Left', rightedge AS 'Right'
    FROM nestedsettree ORDER BY nodeID;
  END IF;
END |
DELIMITER ;
CALL EdgeListToNestedSet( 'familytree', 'childID', 'parentID' );
+--------+------+-------+
| nodeID | Left | Right |
+--------+------+-------+
|      1 |    1 |    46 |
|      2 |    2 |    43 |
|      3 |   44 |    45 |
|      4 |    3 |     4 |
|      5 |    5 |     6 |
|      6 |    7 |    24 |
|      7 |   25 |    26 |
|      8 |   27 |    36 |
|      9 |   37 |    38 |
|     10 |   39 |    40 |
|     11 |   41 |    42 |
|     12 |    8 |    13 |
|     13 |   14 |    15 |
|     14 |   16 |    23 |
|     15 |   28 |    29 |
|     16 |   30 |    31 |
|     17 |   32 |    33 |
|     18 |   34 |    35 |
|     19 |    9 |    12 |
|     20 |   17 |    18 |
|     21 |   19 |    20 |
|     22 |   21 |    22 |
|     23 |   10 |    11 |
+--------+------+-------+
Finding a node's parent and children

A nested sets tree can be queried without recursion, but using the model's arithmetic in a query requires a bit of thought. For example, if we INNER JOIN the tree table AS t1 to itself AS t2 ON t2.leftedge BETWEEN t1.leftedge AND t1.rightedge, and if we scope the query to the descendants of a particular node, we get a list of t2.nodeID values in which the children (one level down) occur once, the grandkids (two levels down) occur twice, and so on:

Listing 11a
SELECT GROUP_CONCAT(t2.nodeID ORDER BY t2.nodeID) AS 'Descendants of William'
FROM nestedsettree AS t1
INNER JOIN nestedsettree AS t2
  ON t2.leftedge BETWEEN t1.leftedge AND t1.rightedge
WHERE t1.leftedge > 7 AND t1.rightedge < 24;
+-------------------------------------------+
| Descendants of William                    |
+-------------------------------------------+
| 12,13,14,19,19,20,20,21,21,22,22,23,23,23 |
+-------------------------------------------+

Therefore HAVING COUNT(t2.nodeID)=1 will scope listed descendants to those who are one level down:

Listing 11b
DROP PROCEDURE IF EXISTS ListNestedSetChildNodes;
DELIMITER |
CREATE PROCEDURE ListNestedSetChildNodes( node SMALLINT )
BEGIN
  DECLARE thisleft, thisright SMALLINT DEFAULT 0;
  SELECT leftedge, rightedge
    INTO thisleft, thisright
  FROM nestedsettree
  WHERE nodeID = node;
  SELECT
    PersonName(t2.nodeid) AS Children
  FROM nestedsettree AS t1
    INNER JOIN nestedsettree AS t2
    ON t2.leftedge BETWEEN t1.leftedge AND t1.rightedge
  WHERE t1.leftedge > thisleft AND t1.rightedge < thisright
  GROUP BY t2.nodeid
  HAVING COUNT(t2.nodeid) = 1
  ORDER BY t2.leftedge;
END;
|
DELIMITER ;

CALL ListNestedSetChildNodes(6);
+--------------------+
| Children           |
+--------------------+
| Susana Shakespeare |
| Hamnet Shakespeare |
| Judith Shakespeare |
+--------------------+

Similar logic gets us the parent of a node:

  1. retrieve its leftedge and rightedge values,
  2. compute its level,
  3. find the node which is one level up and which has edge values outside the node's leftedge and rightedge values.
Listing 12
DROP PROCEDURE IF EXISTS ShowNestedSetParent;
DELIMITER |
CREATE PROCEDURE ShowNestedSetParent( node SMALLINT )
BEGIN
  DECLARE level, thisleft, thisright SMALLINT DEFAULT 0;
  -- find node edges
  SELECT leftedge, rightedge
    INTO thisleft, thisright
  FROM nestedsettree
  WHERE nodeID = node;
  -- find node level
  SELECT COUNT(n1.nodeid)
    INTO level
  FROM nestedsettree AS n1
    INNER JOIN nestedsettree AS n2
    ON n2.leftedge BETWEEN n1.leftedge AND n1.rightedge
  WHERE n2.nodeid = node
  GROUP BY n2.nodeid;
  -- find parent
  SELECT
    PersonName(n2.nodeid) AS Parent
  FROM nestedsettree AS n1
    INNER JOIN nestedsettree AS n2
    ON n2.leftedge BETWEEN n1.leftedge AND n1.rightedge
  WHERE n2.leftedge < thisleft AND n2.rightedge > thisright
  GROUP BY n2.nodeid
  HAVING COUNT(n1.nodeid)=level-1;
END;
|
DELIMITER ;
CALL ShowNestedSetParent(6);
+-------------------+
| Child             |
+-------------------+
| Henry Shakespeare |
+-------------------+
Other queries

For some queries, adjacency list and nested sets queries are equivalently simple. For example to find the tree root and leaves, compare Listing 6 with:

Listing 13
SELECT
  name AS Paterfamilias,
  IFNULL(born,'?') AS Born,
  IFNULL(died,'?') AS Died
FROM nestedsettree AS t
INNER JOIN family AS f ON t.nodeID=f.ID
WHERE leftedge = 1;
+---------------------+------+------+
| Paterfamilias       | Born | Died |
+---------------------+------+------+
| Richard Shakespeare | ?    | 1561 |
+---------------------+------+------+

SELECT
  name AS 'Childless Shakespeares',
  IFNULL(born,'?') AS Born,
  IFNULL(died,'?') AS Died
FROM nestedsettree AS t
INNER JOIN family AS f ON t.nodeID=f.ID
WHERE rightedge = leftedge + 1;
+------------------------+------+------+
| Childless Shakespeares | Born | Died |
+------------------------+------+------+
| Joan Shakespeare       | 1558 | ?    |
| Margaret Shakespeare   | 1562 | 1563 |
| John Bernard           | ?    | 1674 |
| Hamnet Shakespeare     | 1585 | 1596 |
| Shakespeare Quiney     | 1616 | 1617 |
| Richard Quiney         | 1618 | 1639 |
| Thomas Quiney          | 1620 | 1639 |
| Gilbert Shakespeare    | 1566 | 1612 |
| William Hart           | 1600 | 1639 |
| Mary Hart              | 1603 | 1607 |
| Thomas Hart            | 1605 | 1670 |
| Michael Hart           | 1608 | 1618 |
| Anne Shakespeare       | 1571 | 1579 |
| Richard Shakespeare    | 1574 | 1613 |
| Edmond Shakespeare     | 1580 | 1607 |
| John Shakespeare       | 1530 | 1601 |
+------------------------+------+------+

But in contrast to edge list models, finding subtrees in a nested sets model requires no twisted code, no stored procedure. To retrieve the nestedsettree nodes in William's subtree, simply ask for the nodes whose leftedge values are greater, and whose rightedge values are smaller than William's:

Listing 14
SELECT
  PersonName(t.nodeID) AS Descendant
FROM nestedsettree AS s
  INNER JOIN nestedsettree AS t
  ON s.leftedge < t.leftedge AND s.rightedge > t.rightedge
WHERE s.nodeID = (
  SELECT ID FROM family
  WHERE name='William Shakespeare'
);

Finding a single path in the nested sets model is about as complicated as path enumeration (Listings 8, 9) with edge-adjacency lists:

Listing 15
SELECT
  t2.nodeID AS Node,
  PersonName(t2.nodeID) AS Person,
  (SELECT COUNT(*)
   FROM nestedsettree AS t4
   WHERE t4.leftedge BETWEEN t1.leftedge AND t1.rightedge
     AND t2.leftedge BETWEEN t4.leftedge AND t4.rightedge
   ) AS Path
FROM nestedsettree AS t1
  INNER JOIN nestedsettree AS t2 ON t2.leftedge BETWEEN t1.leftedge AND t1.rightedge
  INNER JOIN nestedsettree AS t3 ON t3.leftedge BETWEEN t2.leftedge AND t2.rightedge
WHERE t1.nodeID=(SELECT ID FROM family WHERE name='William Shakespeare')
  AND t3.nodeID=(SELECT ID FROM family WHERE name='John Bernard');
+------+---------------------+------+
| Node | Person              | Path |
+------+---------------------+------+
|    6 | William Shakespeare |    1 |
|   12 | Susana Shakespeare  |    2 |
|   19 | Elizabeth Hall      |    3 |
|   23 | John Bernard        |    4 |
+------+---------------------+------+
Displaying the tree

Here the nested sets model shines. The arithmetic that was used to build the tree makes short work of summary queries. For example to retrieve a node list which preserves all parent-child relations, we need just two facts:

  • listing order is the order taken in the node walk that created the tree, i.e. leftedge,
  • a node's indentation depth is simply the JOIN (edge) count from root to node:
Listing 16
SELECT
  CONCAT( SPACE(2*COUNT(n1.nodeid)-2), PersonName(n2.nodeid) )
  AS 'The Shakespeare Family Tree'
FROM nestedsettree AS n1
  INNER JOIN nestedsettree n2
  ON n2.leftedge BETWEEN n1.leftedge AND n1.rightedge
GROUP BY n2.nodeid
ORDER BY n2.leftedge;
+-----------------------------+
| The Shakespeare Family Tree |
+-----------------------------+
| Richard Shakespeare         |
|   Henry Shakespeare         |
|     Joan Shakespeare        |
|     Margaret Shakespeare    |
|     William Shakespeare     |
|       Susana Shakespeare    |
|         Elizabeth Hall      |
|           John Bernard      |
|       Hamnet Shakespeare    |
|       Judith Shakespeare    |
|         Shakespeare Quiney  |
|         Richard Quiney      |
|         Thomas Quiney       |
|     Gilbert Shakespeare     |
|     Joan Shakespeare        |
|       William Hart          |
|       Mary Hart             |
|       Thomas Hart           |
|       Michael Hart          |
|     Anne Shakespeare        |
|     Richard Shakespeare     |
|     Edmond Shakespeare      |
|   John Shakespeare          |
+-----------------------------+

To retrieve only a subtree, add a query clause which restricts nodes to those whose edges are within the range of the parent node's left and right edge values, for example for William and his descendants...

WHERE n1.leftedge >= 7 AND n1.rightedge <=24

Node insertions, updates and deletions

Nested set arithmetic also helps with insertions, updates and deletions, but the problem remains that changing just one node can require changing much of the tree.

Inserting a node in the tree requires at least two pieces of information: the nodeID to insert, and the nodeID of its parent. The model is normalised so the nodeID first must have been added to the parent (family) table. The algorithm for adding the node to the tree is:

  1. increment leftedge by 2 in nodes where the rightedge value is greater than the parent's rightedge,
  2. increment rightedge by 2 in nodes where the leftedge value is greater than the parent's leftedge,
  3. insert a row with the given nodeID, leftedge = 1 + parent's leftedge, rightedge = 2 + parent's leftedge.

That's not difficult, but all rows will have to be updated!

Listing 17
DROP PROCEDURE IF EXISTS InsertNestedSetNode;
DELIMITER |
CREATE PROCEDURE InsertNestedSetNode( IN node SMALLINT, IN parent SMALLINT )
BEGIN
  DECLARE parentleft, parentright SMALLINT DEFAULT 0;
  SELECT leftedge, rightedge
    INTO parentleft, parentright
  FROM nestedsettree
  WHERE nodeID = parent;
  IF FOUND_ROWS() = 1 THEN
    BEGIN
      UPDATE nestedsettree
        SET rightedge = rightedge + 2
      WHERE rightedge > parentleft;
      UPDATE nestedsettree
        SET leftedge = leftedge + 2
      WHERE leftedge > parentleft;
      INSERT INTO nestedsettree
        VALUES ( 0, node, parentleft + 1, parentleft + 2 );
    END;
  END IF;
END;
|
DELIMITER ;

"Sibline" or horizontal order is obviously significant in family trees, but may not be significant in other trees. Listing 17 adds the new node at the left edge of the sibline. To specify another position, modify the procedure to accept a third parameter for the nodeID which is to be to the left or right of the insertion point.

Updating a node in place requires nothing more than editing nodeID to point at a different parent row.

Deleting a node raises the problem of how to repair links severed by the deletion. In tree models of parts explosions, the item to be deleted is often replaced by a new item, so it can be treated like a simple nodeID update. In organisational boss-employee charts, though, does a colleague move over, does a subordinate get promoted, does everybody in the subtree move up a level, or does something else happen? No formula can catch all the possibilities. Listing 18 illustrates how to handle two common scenarios, move everyone up, and move someone over. All possibilities except simple replacement of the nodeID involve changes elsewhere in the tree.

Listing 18
DROP PROCEDURE IF EXISTS DeleteNestedSetNode;
DELIMITER |
CREATE PROCEDURE DeleteNestedSetNode( IN mode CHAR(7), IN node SMALLINT )
BEGIN
  DECLARE thisleft, thisright SMALLINT DEFAULT 0;
  SELECT leftedge, rightedge
    INTO thisleft, thisright
  FROM nestedsettree
  WHERE nodeID = node;
  IF mode = 'PROMOTE' THEN
    BEGIN                                                         -- Ian Holsman found these two bugs
      DELETE FROM nestedsettree
      WHERE nodeID = node;
      UPDATE nestedsettree
        SET leftedge = leftedge - 1, rightedge = rightedge - 1    -- rather than = thisleft
      WHERE leftedge BETWEEN thisleft AND thisright;
      UPDATE nestedsettree
        SET rightedge = rightedge - 2
      WHERE rightedge > thisright;
      UPDATE nestedsettree
        SET leftedge = leftedge - 2
      WHERE leftedge > thisright;                                 -- rather than > thisleft
    END;
  ELSEIF mode = 'REPLACE' THEN
    BEGIN
      UPDATE nestedsettree
        SET leftedge = thisleft - 1, rightedge = thisright
      WHERE leftedge = thisleft + 1;
      UPDATE nestedsettree
        SET rightedge = rightedge - 2
      WHERE rightedge > thisleft;
      UPDATE nestedsettree
        SET leftedge = leftedge - 2
      WHERE leftedge > thisleft;
      DELETE FROM nestedsettree
      WHERE nodeID = node;
    END;
  END IF;
END;
|
DELIMITER ;
Nested set model summary

Given the concurrency nightmare which nested sets impose for inserts and deletions, it is reasonable to reserve the nested set model for fairly static trees whose users are mostly interested in querying subtrees. You could think of the nested set model as an OLAP tool: maintain an OLTP tree in an edge-adjacency list representation, and build a nested sets OLAP table when the report is needed.

If you will be using the nested sets model, you may be converting back and forth with edge list models, so here is a simple query which generates an edge list from a nested set tree:

Listing 19
SELECT
  p.nodeID AS parentID,
  c.nodeID AS childID
FROM nestedsettree AS p
  INNER JOIN nestedsettree AS c
  ON p.leftedge = (SELECT MAX(s.leftedge)
                   FROM nestedsettree AS s
                   WHERE c.leftedge > s.leftedge
                     AND c.leftedge < s.rightedge)
ORDER BY p.nodeID;

Edge list model of a non-tree graph

Many graphs are not trees. Imagine a small airline which has just acquired licences for flights no longer than 6,000 km between Los Angeles (LAX), New York (JFK), Heathrow in London, Charles de Gaulle in Paris, Amsterdam-Schiphol, Arlanda in Sweden, and Helsinki-Vantaa. You have been asked to compute the shortest possible one-way routes that do not deviate more than 90° from the direction of the first hop—roughly, one-way routes and no circuits.

Airports are nodes, flights are edges, routes are paths. We will need three tables.

Airports (nodes)

To identify an airport we need its code, location name, latitude and longitude. Latitude and longitude are usually given as degrees, minutes and seconds, north or south of the equator, east or west of Greenwich. To hide details that aren't directly relevant to nodes and edges, code latitude and longitude as simple reals where longitudes west of Greenwich and latitudes south of the equator are negative, whilst longitudes east of Greenwich and latitudes north of the equator are positive:

Listing 20
CREATE TABLE airports (
  code char(3) NOT NULL,
  city char(100) default NULL,
  latitude float NOT NULL,
  longitude float NOT NULL,
  PRIMARY KEY (code)
) ENGINE=MyISAM;

INSERT INTO airports VALUES ('JFK', 'New York, NY', 40.75, -73.97);
INSERT INTO airports VALUES ('LAX', 'Los Angeles, CA', 34.05, -118.22);
INSERT INTO airports VALUES ('LHR', 'London, England', 51.5, -0.45);
INSERT INTO airports VALUES ('HEL', 'Helsinki, Finland', 60.17, 24.97);
INSERT INTO airports VALUES ('CDG', 'Paris, France', 48.86, 2.33);
INSERT INTO airports VALUES ('STL', 'St Louis, MO', 38.63, -90.2);
INSERT INTO airports VALUES ('ARN', 'Stockholm, Sweden', 59.33, 18.05);
Flights (edges)

The model attaches two weights to flights: distance and direction.

We need a method of calculating the Great Circle Distance—the geographical distance between any two cities - another natural job for a stored function. The distance calculation

  • converts to radians the degree coordinates of any two points on the earth's surface,
  • calculates the angle of the arc subtended by the two points, and
  • converts the result, also in radians, to surface (circumferential) kilometres (1 radian=6,378.388 km).
Listing 21
SET GLOBAL log_bin_trust_function_creators=TRUE;   -- since 5.0.16
DROP FUNCTION IF EXISTS GeoDistKM;
DELIMITER |
CREATE FUNCTION GeoDistKM( lat1 FLOAT, lon1 FLOAT, lat2 FLOAT, lon2 FLOAT ) RETURNS float
BEGIN
  DECLARE pi, q1, q2, q3 FLOAT;
  SET pi = PI();
  SET lat1 = lat1 * pi / 180;
  SET lon1 = lon1 * pi / 180;
  SET lat2 = lat2 * pi / 180;
  SET lon2 = lon2 * pi / 180;
  SET q1 = COS(lon1-lon2);
  SET q2 = COS(lat1-lat2);
  SET q3 = COS(lat1+lat2);
  SET rads = ACOS( 0.5*((1.0+q1)*q2 - (1.0-q1)*q3) );
  RETURN 6378.388 * rads;
END;
|
DELIMITER ;

That takes care of flight distances. Flight direction is, approximately, the arctangent (ATAN) of the difference between flights.depart and flights.arrive latitudes and longitudes. Now we can seed the airline's flights table with one-hop flights up to 6,000 km long:

Listing 22
CREATE TABLE flights (
  id INT PRIMARY KEY AUTO_INCREMENT,
  depart CHAR(3),
  arrive CHAR(3),
  distance DECIMAL(10,2),
  direction DECIMAL(10,2)
) ENGINE=MYISAM;

INSERT INTO flights
  SELECT
  NULL,
  depart.code,
  arrive.code,
  ROUND(GeoDistKM(depart.latitude,depart.longitude,arrive.latitude,arrive.longitude),2),
  ROUND(DEGREES(ATAN(arrive.latitude-depart.latitude,arrive.longitude-depart.longitude)),2)
  FROM airports AS depart
  INNER JOIN airports AS arrive ON depart.code <> arrive.code
  HAVING Km <= 6000;

SELECT * FROM flights;
+----+--------+--------+----------+-----------+
| id | depart | arrive | distance | direction |
+----+--------+--------+----------+-----------+
|  1 | LAX    | JFK    | 3941.18  | 8.61      |
|  2 | LHR    | JFK    | 5550.77  | -171.68   |
|  3 | CDG    | JFK    | 5837.46  | -173.93   |
|  4 | STL    | JFK    | 1408.11  | 7.44      |
|  5 | JFK    | LAX    | 3941.18  | -171.39   |
|  6 | STL    | LAX    | 2553.37  | -170.72   |
|  7 | JFK    | LHR    | 5550.77  | 8.32      |
|  8 | HEL    | LHR    | 1841.91  | -161.17   |
|  9 | CDG    | LHR    | 354.41   | 136.48    |
| 10 | ARN    | LHR    | 1450.12  | -157.06   |
| 11 | LHR    | HEL    | 1841.91  | 18.83     |
| 12 | CDG    | HEL    | 1912.96  | 26.54     |
| 13 | ARN    | HEL    | 398.99   | 6.92      |
| 14 | JFK    | CDG    | 5837.46  | 6.07      |
| 15 | LHR    | CDG    | 354.41   | -43.52    |
| 16 | HEL    | CDG    | 1912.96  | -153.46   |
| 17 | ARN    | CDG    | 1545.23  | -146.34   |
| 18 | JFK    | STL    | 1408.11  | -172.56   |
| 19 | LAX    | STL    | 2553.37  | 9.28      |
| 20 | LHR    | ARN    | 1450.12  | 22.94     |
| 21 | HEL    | ARN    | 398.99   | -173.08   |
| 22 | CDG    | ARN    | 1545.23  | 33.66     |
+----+--------+--------+----------+-----------+

The distances agree approximately with public information sources for flight lengths. For a pair of airports A and B not very near the poles, the error in calculating direction using ATAN(), is small. To remove that error, instead of ATAN() use a formula from spherical trigonometry (for example one of the formulas at http://www.dynagen.co.za/eugene/where/formula.html).

Routes (paths)

A route is a path along one or more of these edges, so flights:routes is a 1:many relationship. For simplicity, though, we denormalise our representation of routes with a variation of the materialised path model to store all the hops of one route as a list of flights in one routes column. The column routes.route is the sequence of airports, from first departure to final arrival, the column routes.hops is the number of hops in that route, and the column routes.direction is the direction:

Listing 23
CREATE TABLE routes (
  id INT PRIMARY KEY AUTO_INCREMENT,
  depart CHAR(3),
  arrive CHAR(3),
  hops SMALLINT,
  route CHAR(50),
  distance DECIMAL(10,2),
  direction DECIMAL(10,2)
) ENGINE=MYISAM;

Starting with an empty routes table, how do we populate it with the shortest routes between all ordered pairs of airports?

  1. Insert all 1-hop flights from the flights table.
  2. Add in the set of shortest multi-hop routes for all pairs of airports which don't have rows in the flights table.

For 1-hop flights we just write

Listing 24
INSERT INTO routes
  SELECT
    NULL,
    depart,
    arrive,
    1,
    CONCAT(depart,',',arrive),
    distance,
    direction
  FROM flights;

NULL being the placeholder for the auto-incrementing id column.

For multi-hop routes, we iteratively add in sets of all allowed 2-hop, 3-hop, ... n-hop routes, replacing longer routes by shorter routes as we find them, until there is nothing more to add or replace. That also breaks down to two logical steps: add hops to build the set of next allowed routes, and update longer routes with shorter ones.

Next allowed routes

The set of next allowed routes is the set of shortest routes that can be built by adding, to existing routes, flights which leave from the last arrival airport of an existing route, which arrive at an airport which is not yet in the given route, and which stay within ± 90° of the route's initial compass direction. That is, every new route is a JOIN between routes and flights in which

  • depart = routes.depart,
  • arrive = flights.arrive,
  • flights.depart = routes.arrive,
  • distance = MIN(routes.distance + flights.distance),
  • LOCATE( flights.arrive,routes.route) = 0,
  • flights.direction+360 > routes.direction+270 AND flights.direction+360 < routes.direction+450

This is a natural logical unit of work for a VIEW:

Listing 25
CREATE OR REPLACE VIEW nextroutes AS
  SELECT
    routes.depart,
    flights.arrive,
    routes.hops+1 AS hops,
    CONCAT(routes.route, ',', flights.arrive) AS route,
    MIN(routes.distance + flights.distance) AS distance,
    routes.direction
  FROM routes INNER JOIN flights
    ON routes.arrive = flights.depart
    AND LOCATE(flights.arrive,routes.route) = 0
  WHERE flights.direction+360>routes.direction+270 
    AND flights.direction+360<routes.direction+450
  GROUP BY depart,arrive;

How to add these new hops to routes? In standard SQL, this variant on a query by Scott Stephens should do it...

Listing 26
INSERT INTO routes
  SELECT NULL,depart,arrive,hops,route,distance,direction FROM nextroutes
  WHERE (nextroutes.depart,nextroutes.arrive) NOT IN (
    SELECT depart,arrive FROM routes
  );

but MySQL does not yet support subqueries on the table being updated. We have to use a subquery-less (and faster) version of that logic:

Listing 27
INSERT INTO routes
  SELECT
    NULL,
    nextroutes.depart,
    nextroutes.arrive,
    nextroutes.hops,
    nextroutes.route,
    nextroutes.distance,
    nextroutes.direction
  FROM nextroutes
  LEFT JOIN routes ON nextroutes.depart = routes.depart
        AND nextroutes.arrive = routes.arrive
  WHERE routes.depart IS NULL AND routes.arrive IS NULL;

Running that code right after the initial seeding from flights gives ...

SELECT * FROM routes;
+----+--------+--------+------+-------------+----------+-----------+
| id | depart | arrive | hops | route       | distance | direction |
+----+--------+--------+------+-------------+----------+-----------+
|  1 | LAX    | JFK    |    1 | LAX,JFK     | 3941.18  | 8.61      |
|  2 | LHR    | JFK    |    1 | LHR,JFK     | 5550.77  | -171.68   |
|  3 | CDG    | JFK    |    1 | CDG,JFK     | 5837.46  | -173.93   |
|  4 | STL    | JFK    |    1 | STL,JFK     | 1408.11  | 7.44      |
|  5 | JFK    | LAX    |    1 | JFK,LAX     | 3941.18  | -171.39   |
|  6 | STL    | LAX    |    1 | STL,LAX     | 2553.37  | -170.72   |
|  7 | JFK    | LHR    |    1 | JFK,LHR     | 5550.77  | 8.32      |
|  8 | HEL    | LHR    |    1 | HEL,LHR     | 1841.91  | -161.17   |
|  9 | CDG    | LHR    |    1 | CDG,LHR     | 354.41   | 136.48    |
| 10 | ARN    | LHR    |    1 | ARN,LHR     | 1450.12  | -157.06   |
| 11 | LHR    | HEL    |    1 | LHR,HEL     | 1841.91  | 18.83     |
| 12 | CDG    | HEL    |    1 | CDG,HEL     | 1912.96  | 26.54     |
| 13 | ARN    | HEL    |    1 | ARN,HEL     | 398.99   | 6.92      |
| 14 | JFK    | CDG    |    1 | JFK,CDG     | 5837.46  | 6.07      |
| 15 | LHR    | CDG    |    1 | LHR,CDG     | 354.41   | -43.52    |
| 16 | HEL    | CDG    |    1 | HEL,CDG     | 1912.96  | -153.46   |
| 17 | ARN    | CDG    |    1 | ARN,CDG     | 1545.23  | -146.34   |
| 18 | JFK    | STL    |    1 | JFK,STL     | 1408.11  | -172.56   |
| 19 | LAX    | STL    |    1 | LAX,STL     | 2553.37  | 9.28      |
| 20 | LHR    | ARN    |    1 | LHR,ARN     | 1450.12  | 22.94     |
| 21 | HEL    | ARN    |    1 | HEL,ARN     | 398.99   | -173.08   |
| 22 | CDG    | ARN    |    1 | CDG,ARN     | 1545.23  | 33.66     |
| 23 | ARN    | JFK    |    2 | ARN,LHR,JFK | 7000.89  | -157.06   |
| 24 | CDG    | LAX    |    2 | CDG,JFK,LAX | 9778.64  | -173.93   |
| 25 | CDG    | STL    |    2 | CDG,JFK,STL | 7245.57  | -173.93   |
| 26 | HEL    | JFK    |    2 | HEL,LHR,JFK | 7392.68  | -161.17   |
| 27 | JFK    | ARN    |    2 | JFK,LHR,ARN | 7000.89  | 8.32      |
| 28 | JFK    | HEL    |    2 | JFK,LHR,HEL | 7392.68  | 8.32      |
| 29 | LAX    | CDG    |    2 | LAX,JFK,CDG | 9778.64  | 8.61      |
| 30 | LAX    | LHR    |    2 | LAX,JFK,LHR | 9491.95  | 8.61      |
| 31 | LHR    | LAX    |    2 | LHR,JFK,LAX | 9491.95  | -171.68   |
| 32 | LHR    | STL    |    2 | LHR,JFK,STL | 6958.88  | -171.68   |
| 33 | STL    | CDG    |    2 | STL,JFK,CDG | 7245.57  | 7.44      |
| 34 | STL    | LHR    |    2 | STL,JFK,LHR | 6958.88  | 7.44      |
+----+--------+--------+------+-------------+----------+-----------+

... adding 12 two-hop rows.

Replace longer routes with shorter ones

As we build routes with more hops, it is logically possible that the nextroutes view will find shorter routes for an existing routes pair of depart and arrive. Standard SQL for replacing existing routes rows with nextroutes rows which match (depart, arrive) and have shorter distance values would be:

Listing 28
UPDATE routes SET (hops,route,distance,direction) = (
  SELECT hops, route, distance, direction
  FROM nextroutes
  WHERE nextroutes.depart = routes.depart AND nextroutes.arrive = routes.arrive
)
WHERE (depart,arrive) IN (
  SELECT depart,arrive FROM nextroutes
  WHERE nextroutes.distance < routes.distance
);

but MySQL does not support SET(col1,...) syntax, and as with Listing 7, MySQL does not yet accept subqueries referencing the table being updated, so we have to write more literal SQL:

Listing 29
UPDATE routes, nextroutes
SET
  routes.hops=nextroutes.hops,
  routes.route=nextroutes.route,
  routes.distance=nextroutes.distance,
  routes.direction=nextroutes.direction
WHERE routes.arrive=nextroutes.arrive
  AND routes.depart=nextroutes.depart
  AND nextroutes.distance < routes.distance;

Running this code right after the first run of Listing 27 updates no rows. To test the logic of iteration, continue running Listings 27 and 29 until no rows are being added or changed. The final result is:

SELECT * FROM ROUTES;
+----+--------+--------+------+-----------------+----------+-----------+
| id | depart | arrive | hops | route           | distance | direction |
+----+--------+--------+------+-----------------+----------+-----------+
|  1 | LAX    | JFK    |    1 | LAX,JFK         | 3941.18  | 8.61      |
|  2 | LHR    | JFK    |    1 | LHR,JFK         | 5550.77  | -171.68   |
|  3 | CDG    | JFK    |    1 | CDG,JFK         | 5837.46  | -173.93   |
|  4 | STL    | JFK    |    1 | STL,JFK         | 1408.11  | 7.44      |
|  5 | JFK    | LAX    |    1 | JFK,LAX         | 3941.18  | -171.39   |
|  6 | STL    | LAX    |    1 | STL,LAX         | 2553.37  | -170.72   |
|  7 | JFK    | LHR    |    1 | JFK,LHR         | 5550.77  | 8.32      |
|  8 | HEL    | LHR    |    1 | HEL,LHR         | 1841.91  | -161.17   |
|  9 | CDG    | LHR    |    1 | CDG,LHR         | 354.41   | 136.48    |
| 10 | ARN    | LHR    |    1 | ARN,LHR         | 1450.12  | -157.06   |
| 11 | LHR    | HEL    |    1 | LHR,HEL         | 1841.91  | 18.83     |
| 12 | CDG    | HEL    |    1 | CDG,HEL         | 1912.96  | 26.54     |
| 13 | ARN    | HEL    |    1 | ARN,HEL         | 398.99   | 6.92      |
| 14 | JFK    | CDG    |    1 | JFK,CDG         | 5837.46  | 6.07      |
| 15 | LHR    | CDG    |    1 | LHR,CDG         | 354.41   | -43.52    |
| 16 | HEL    | CDG    |    1 | HEL,CDG         | 1912.96  | -153.46   |
| 17 | ARN    | CDG    |    1 | ARN,CDG         | 1545.23  | -146.34   |
| 18 | JFK    | STL    |    1 | JFK,STL         | 1408.11  | -172.56   |
| 19 | LAX    | STL    |    1 | LAX,STL         | 2553.37  | 9.28      |
| 20 | LHR    | ARN    |    1 | LHR,ARN         | 1450.12  | 22.94     |
| 21 | HEL    | ARN    |    1 | HEL,ARN         | 398.99   | -173.08   |
| 22 | CDG    | ARN    |    1 | CDG,ARN         | 1545.23  | 33.66     |
| 23 | ARN    | JFK    |    2 | ARN,LHR,JFK     | 7000.89  | -157.06   |
| 24 | CDG    | LAX    |    2 | CDG,JFK,LAX     | 9778.64  | -173.93   |
| 25 | CDG    | STL    |    2 | CDG,JFK,STL     | 7245.57  | -173.93   |
| 26 | HEL    | JFK    |    2 | HEL,LHR,JFK     | 7392.68  | -161.17   |
| 27 | JFK    | ARN    |    2 | JFK,LHR,ARN     | 7000.89  | 8.32      |
| 28 | JFK    | HEL    |    2 | JFK,LHR,HEL     | 7392.68  | 8.32      |
| 29 | LAX    | CDG    |    2 | LAX,JFK,CDG     | 9778.64  | 8.61      |
| 30 | LAX    | LHR    |    2 | LAX,JFK,LHR     | 9491.95  | 8.61      |
| 31 | LHR    | LAX    |    2 | LHR,JFK,LAX     | 9491.95  | -171.68   |
| 32 | LHR    | STL    |    2 | LHR,JFK,STL     | 6958.88  | -171.68   |
| 33 | STL    | CDG    |    2 | STL,JFK,CDG     | 7245.57  | 7.44      |
| 34 | STL    | LHR    |    2 | STL,JFK,LHR     | 6958.88  | 7.44      |
| 35 | ARN    | LAX    |    3 | ARN,LHR,JFK,LAX | 10942.07 | -157.06   |
| 36 | ARN    | STL    |    3 | ARN,LHR,JFK,STL | 8409.00  | -157.06   |
| 37 | HEL    | LAX    |    3 | HEL,LHR,JFK,LAX | 11333.86 | -161.17   |
| 38 | HEL    | STL    |    3 | HEL,LHR,JFK,STL | 8800.79  | -161.17   |
| 39 | LAX    | ARN    |    3 | LAX,JFK,CDG,ARN | 10942.07 | 8.61      |
| 40 | LAX    | HEL    |    3 | LAX,JFK,CDG,HEL | 11333.86 | 8.61      |
| 41 | STL    | ARN    |    3 | STL,JFK,CDG,ARN | 8409.00  | 7.44      |
| 42 | STL    | HEL    |    3 | STL,JFK,CDG,HEL | 8800.79  | 7.44      |
+----+--------+--------+------+-----------------+----------+-----------+

All that's left to do is to assemble the code in a stored procedure:

Listing 30
DROP PROCEDURE IF EXISTS BuildRoutes;
DELIMITER |
CREATE PROCEDURE BuildRoutes()
BEGIN
  DECLARE rows INT DEFAULT 0;
  TRUNCATE routes;

  -- STEP 1, LISTING 24: SEED ROUTES WITH 1-HOP FLIGHTS
  INSERT INTO routes
    SELECT
      NULL,
      depart,
      arrive,
      1,
      CONCAT(depart,',',arrive),
      distance,
      direction
  FROM flights;
  SET rows = ROW_COUNT();

  WHILE (rows > 0) DO

    -- STEP 2, LISTINGS 25, 27: ADD NEXT SET OF ROUTES
    INSERT INTO routes
      SELECT
        NULL,
        nextroutes.depart,
        nextroutes.arrive,
        nextroutes.hops,
        nextroutes.route,
        nextroutes.distance,
        nextroutes.direction
      FROM nextroutes
      LEFT JOIN routes ON nextroutes.depart = routes.depart
            AND nextroutes.arrive = routes.arrive
      WHERE routes.depart IS NULL AND routes.arrive IS NULL;
    SET rows = ROW_COUNT();

    -- STEP 3, LISTING 29: UPDATE WITH SHORTER nextroutes ROUTES IF ANY
    UPDATE routes,nextroutes SET
      routes.hops=nextroutes.hops,
      routes.route=nextroutes.route,
      routes.distance=nextroutes.distance,
      routes.direction=nextroutes.direction
    WHERE routes.arrive=nextroutes.arrive
      AND routes.depart=nextroutes.depart
      AND nextroutes.distance < routes.distance;
    SET rows = rows + ROW_COUNT();

  END WHILE;

END;
|
DELIMITER ;

If you are running MySQL 5.0.6 or 5.0.7, BuildRoutes() fails to insert four rows:

+--------+--------+-----------------+------+----------+-----------+
| depart | arrive | route           | hops | distance | direction |
+--------+--------+-----------------+------+----------+-----------+
| ARN    | LAX    | ARN,LHR,JFK,LAX |    3 | 10942.07 | -157.06   |
| ARN    | STL    | ARN,LHR,JFK,STL |    3 | 8409.00  | -157.06   |
| HEL    | LAX    | HEL,LHR,JFK,LAX |    3 | 11333.86 | -161.17   |
| HEL    | STL    | HEL,LHR,JFK,STL |    3 | 8800.79  | -161.17   |
+--------+--------+-----------------+------+----------+-----------+

That MySQL bug (#11302) was corrected in 5.0.9.

Route queries

Route queries are straightforward. How do we check that the algorithm produced no duplicate depart-arrive pairs? The following query should yield zero rows,

Listing 31
SELECT depart, arrive, COUNT(*)
FROM routes
GROUP BY depart,arrive
HAVING COUNT(*) > 1;

and does. Reachability queries are just as simple, for example where can we fly to from Helsinki?

Listing 32
SELECT *
FROM routes
WHERE depart='HEL'
ORDER BY distance;
+----+--------+--------+------+-----------------+----------+-----------+
| id | depart | arrive | hops | route           | distance | direction |
+----+--------+--------+------+-----------------+----------+-----------+
| 21 | HEL    | ARN    |    1 | HEL,ARN         | 398.99   | -173.08   |
|  8 | HEL    | LHR    |    1 | HEL,LHR         | 1841.91  | -161.17   |
| 16 | HEL    | CDG    |    1 | HEL,CDG         | 1912.96  | -153.46   |
| 26 | HEL    | JFK    |    2 | HEL,LHR,JFK     | 7392.68  | -161.17   |
| 38 | HEL    | STL    |    3 | HEL,LHR,JFK,STL | 8800.79  | -161.17   |
| 37 | HEL    | LAX    |    3 | HEL,LHR,JFK,LAX | 11333.86 | -161.17   |
+----+--------+--------+------+-----------------+----------+-----------+

An extended edge list model is simple to implement, gracefully accepts extended attributes for nodes, edge and paths, does not unduly penalise updates, and responds to queries with reasonable speed.

Parts explosions

A bill of materials for a house would include the cement block, lumber, shingles, doors, wallboard, windows, plumbing, electrical system, heating system, and so on. Each subassembly also has a bill of materials; the heating system has a furnace, ducts, and so on. A bill of materials implosion links component pieces to a major assembly. A bill of materials explosion breaks apart assemblies and subassemblies into their component parts.

Which graph model best handles a parts explosion? Combining edge list and "nested sets" algorithms provides a clean solution.

Imagine a new company that plans to make variously sized bookcases, either packaged as do-it-yourself kits of, or assembled from sides, shelves, shelf brackets, backboards, feet and screws. Shelves and sides are cut from planks. Backboards are trimmed from laminated sheeting. Feet are machine-carved from readycut blocks. Screws and shelf brackets are purchased in bulk. Here are the elements of one bookcase:

  1 backboard, 2 x 1 m
    1 laminate
    8 screws
  2 sides 2m x 30 cm
    1 plank length 4m
    12 screws
  8 shelves 1 m x 30 cm (incl. top and bottom)
    2 planks
    24 shelf brackets
  4 feet 4cm x 4cm
    4 cubes
    16 screws

which may be packaged in a box for sale at one price, or assembled as a finished product at a different price. At any time we need to be able to answer questions like

  • Do we have enough parts to make the bookcases on order?
  • What assemblies or packages would be most profitable to make given the current inventory?

There is no reason to break the normalising rule that item detail belongs in a nodes table, and graph logic belongs in an edges table. Edges also require a quantity attribute, for example a shelf includes four shelf brackets. Nodes and edges may also have costs and prices:

  • item purchase cost,
  • item assembly cost,
  • assembly cost,
  • assembly selling price.

In many parts problems like this one, items occur in multiple assemblies and subassemblies. The graph is not a tree. Also, it is often desirable to model multiple graphs without the table glut that would arise from giving each graph its own edges table. A simple way to solve this problem is to represent multiple graphs (assemblies) in the edges table by giving every row not only childID and parentID pointers, but a pointer which identifies the root itemID of the graph to which the row belongs.

So the data model is just two tables, for items (nodes) and for product graphs or assemblies (edges). Assume that the company begins with a plan to sell the 2m x 1m bookcase in two forms, assembled and kit, and that the purchasing department has bought quantities of raw materials (laminate, planks, shelf supports, screws, wood cubes, boxes). Here are the nodes (items) and edges (assemblies):

Listing 33
CREATE TABLE items (
  itemID INT PRIMARY KEY AUTO_INCREMENT,
  name CHAR(20) NOT NULL,
  onhand INT NOT NULL DEFAULT 0,
  reserved INT NOT NULL DEFAULT 0,
  purchasecost DECIMAL(10,2) NOT NULL DEFAULT 0,
  assemblycost DECIMAL(10,2) NOT NULL DEFAULT 0,
  price DECIMAL(10,2) NOT NULL DEFAULT 0
);
CREATE TABLE assemblies (
  assemblyID INT NOT NULL,
  assemblyroot INT NOT NULL,
  childID INT NOT NULL,
  parentID INT NOT NULL,
  quantity DECIMAL(10,2) NOT NULL,
  assemblycost DECIMAL(10,2) NOT NULL,
  PRIMARY KEY(assemblyID,childID,parentID)
);
INSERT INTO items VALUES    -- inventory
  (1,'laminate',40,0,4,0,8),
  (2,'screw',1000,0,0.1,0,.2),
  (3,'plank',200,0,10,0,20),
  (4,'shelf bracket',400,0,0.20,0,.4),
  (5,'wood cube',100,0,0.5,0,1),
  (6,'box',40,0,1,0,2),
  (7,'backboard',0,0,0,3,0),
  (8,'side',0,0,0,8,0),
  (9,'shelf',0,0,0,4,0),
  (10,'foot',0,0,0,1,0),
  (11,'bookcase2x30',0,0,0,10,0),
  (12,'bookcase2x30 kit',0,0,0,2,0);
INSERT INTO assemblies VALUES
  (1,11,1,7,1,0),      -- laminate to backboard
  (2,11,2,7,8,0),      -- screws to backboard
  (3,11,3,8,.5,0),     -- planks to side
  (4,11,2,8,6,0),      -- screws to side
  (5,11,3,9,0.25,0),   -- planks to shelf
  (6,11,4,9,4,0),      -- shelf brackets to shelf
  (7,11,5,10,1,0),     -- wood cubes to foot
  (8,11,2,10,1,0),     -- screws to foot
  (9,11,7,11,1,0),     -- backboard to bookcase
  (10,11,8,11,2,0),    -- sides to bookcase
  (11,11,9,11,8,0),    -- shelves to bookcase
  (12,11,10,11,4,0),   -- feet to bookcase
  (13,12,1,7,1,0),     -- laminate to backboard
  (14,12,2,7,8,0),     -- screws to backboard
  (15,12,3,8,0.5,0),   -- planks to side
  (16,12,2,8,6,0),     -- screws to sides
  (17,12,3,9,0.25,0),  -- planks to shelf
  (18,12,4,9,4,0),     -- shelf brackets to shelves
  (19,12,5,10,1,0),    -- wood cubes to foot
  (20,12,2,10,1,0),    -- screws to foot
  (21,12,7,12,1,0),    -- backboard to bookcase kit
  (22,12,8,12,2,0),    -- sides to bookcase kit
  (23,12,9,12,8,0),    -- shelves to bookcase kit
  (24,12,10,12,4,0),   -- feet to bookcase kit
  (25,12,6,12,1,0);    -- container box to bookcase kit

Now, we want a parts list, a bill of materials, which will list show parent-child relationships and quantities, and sum the costs. Could we adapt the depth-first "nested sets" treewalk algorithm (Listing 10) to this problem even though our graph is not a tree and our sets are not properly nested? Yes indeed. We just have to modify the treewalk to handle multiple parent nodes for any child node, and add code to percolate costs and quantities up the graph. Navigation remains simple using leftedge and rightedge values. This is just the sort of problem the Celko algorithm is good for: reporting!

Listing 34
DROP PROCEDURE IF EXISTS ShowBOM;
DELIMITER |
CREATE PROCEDURE ShowBOM( IN root INT )
BEGIN
  DECLARE thischild, thisparent, rows, maxrightedge INT DEFAULT 0;
  DECLARE thislevel, nextedgenum INT DEFAULT 1;
  DECLARE thisqty, thiscost DECIMAL(10,2);

  -- Create and seed intermediate table:
  DROP TABLE IF EXISTS edges;
  CREATE TABLE edges (
    childID smallint NOT NULL,
    parentID smallint NOT NULL,
    PRIMARY KEY (childID, parentID)
  ) ENGINE=HEAP;
  INSERT INTO edges
    SELECT childID,parentID
    FROM assemblies
    WHERE assemblyRoot = root;
  SET maxrightedge = 2 * (1 + (SELECT COUNT(*) FROM edges));
  -- Create and seed result table:
  DROP TABLE IF EXISTS bom;
  CREATE TABLE bom (
    level SMALLINT,
    nodeID SMALLINT,
    parentID SMALLINT,
    qty DECIMAL(10,2),
    cost DECIMAL(10,2),
    leftedge SMALLINT,
    rightedge SMALLINT
  ) ENGINE=HEAP;
  INSERT INTO bom
    VALUES( thislevel, root, 0, 0, 0, nextedgenum, maxrightedge );
  SET nextedgenum = nextedgenum + 1;
  WHILE nextedgenum < maxrightedge DO
    -- How many children of this node remain in the edges table?
    SET rows = (
      SELECT COUNT(*)
      FROM bom AS s
      INNER JOIN edges AS t ON s.nodeID=t.parentID AND s.level=thislevel
    );
    IF rows > 0 THEN
      -- There is at least one child edge.
      -- Compute qty and cost, insert into bom, delete from edges.
      BEGIN
        -- Alas MySQL nulls MIN(t.childid) when we combine the next two queries
        SET thischild = (
          SELECT MIN(t.childID)
          FROM bom AS s
          INNER JOIN edges AS t ON s.nodeID=t.parentID AND s.level=thislevel
        );
        SET thisparent = (
          SELECT DISTINCT t.parentID
          FROM bom AS s
          INNER JOIN edges AS t ON s.nodeID=t.parentID AND s.level=thislevel
        );
        SET thisqty = (
          SELECT quantity FROM assemblies
          WHERE assemblyroot = root
            AND childID = thischild
            AND parentID = thisparent
        );
        SET thiscost = (
          SELECT a.assemblycost + (thisqty * (i.purchasecost + i.assemblycost ))
          FROM assemblies AS a
          INNER JOIN items AS i ON a.childID = i.itemID
          WHERE assemblyroot = root
            AND a.parentID = thisparent
            AND a.childID = thischild
        );
        INSERT INTO bom
          VALUES(thislevel+1, thischild, thisparent, thisqty, thiscost, nextedgenum, NULL);
        DELETE FROM edges
        WHERE childID = thischild AND parentID=thisparent;
        SET thislevel = thislevel + 1;
        SET nextedgenum = nextedgenum + 1;
      END;
    ELSE
      BEGIN
        -- Set rightedge, remove item from edges
        UPDATE bom
        SET rightedge=nextedgenum, level = -level
        WHERE level = thislevel;
        SET thislevel = thislevel - 1;
        SET nextedgenum = nextedgenum + 1;
      END;
    END IF;
  END WHILE;
  SET rows := ( SELECT COUNT(*) FROM edges );
  IF rows > 0 THEN
    SELECT 'Orphaned rows remain';
  ELSE
    -- Total
    SET thiscost = (SELECT SUM(qty*cost) FROM bom);
    UPDATE bom
    SET qty = 1, cost = thiscost
    WHERE nodeID = root;
    -- Show the result
    SELECT
      CONCAT(Space(Abs(level)*2), ItemName(nodeid,root)) AS Item,
      ROUND(qty,2) AS Qty,
      ROUND(cost, 2) AS Cost
    FROM bom
    ORDER BY leftedge;
  END IF;
END;
|
DELIMITER ;

-- Function used by ShowBOM() to retrieve bom item names:
DROP FUNCTION IF EXISTS ItemName;
SET GLOBAL log_bin_trust_function_creators=TRUE;
DELIMITER |
CREATE FUNCTION ItemName( id INT, root INT ) RETURNS CHAR(20)
BEGIN
  DECLARE s CHAR(20) DEFAULT '';
  SELECT name INTO s FROM items WHERE itemid=id;
  RETURN IF( id = root, UCASE(s), s );
END;
|
DELIMITER ;
CALL ShowBOM(11);
+---------------------+------+--------+

| Item                | Qty  | Cost   |

+---------------------+------+--------+

|   BOOKCASE2X30      |  1.0 | 327.93 |

|     backboard       |  1.0 |   3.00 |

|       laminate      |  1.0 |   4.00 |

|       screw         |  8.0 |   0.80 |

|     side            |  2.0 |  16.00 |

|       screw         |  6.0 |   0.60 |

|       plank         |  0.5 |   5.00 |

|     shelf           |  8.0 |  32.00 |

|       plank         |  0.3 |   2.50 |

|       shelf bracket |  4.0 |   0.80 |

|     foot            |  4.0 |   4.00 |

|       screw         |  1.0 |   0.10 |

|       wood cube     |  1.0 |   0.50 |

+---------------------+------+--------+


With ShowBOM() in hand, it's easy to compare costs of assemblies and subassemblies. By adding price columns, we can do the same for prices and profit margins. And now that MySQL has re-enabled prepared statements in stored procedures, it will be relatively easy to write a more general version of ShowBOM(). We leave that to you.

Shorter and sweeter

But ShowBOM() is not the small, efficient bit of nested sets reporting code we were hoping for. There is a simpler solution: hide graph cycles from the edges table by making them references to rows in a nodes table, so we can treat the edges table like a tree; then apply a breadth-first edge-list subtree algorithm to generate the Bill of Materials. Again assume a cabinetmaking company making bookcases (with a different costing model). For clarity, skip inventory tracking for now. An items table ww_nodes tracks purchased and assembled bookcase elements with their individual costs, and an assemblies/edges ww_edges table tracks sets of edges that combine to make products.

Listing 35: DDL for a simpler parts explosion
DROP TABLE IF EXISTS ww_nodes;
CREATE TABLE ww_nodes (
  nodeID int,
  description CHAR(50),
  cost decimal(10,2)
);
INSERT INTO ww_nodes VALUES (1,'finished bookcase',10);
INSERT INTO ww_nodes VALUES (2,'backboard2x1',1);
INSERT INTO ww_nodes VALUES (3,'laminate2x1',8);
INSERT INTO ww_nodes VALUES (4,'screw',.10);
INSERT INTO ww_nodes VALUES (5,'side',4);
INSERT INTO ww_nodes VALUES (6,'plank',20);
INSERT INTO ww_nodes VALUES (7,'shelf',4);
INSERT INTO ww_nodes VALUES (8,'shelf bracket',.5);
INSERT INTO ww_nodes VALUES (9,'feet',1);
INSERT INTO ww_nodes VALUES (10,'cube4cmx4cm',1);
INSERT INTO ww_nodes VALUES (11,'bookcase kit',2);
INSERT INTO ww_nodes VALUES (12,'carton',1);
 
DROP TABLE IF EXISTS ww_edges;
CREATE TABLE ww_edges (
  rootID INT,
  nodeID int,
  parentnodeID int,
  qty decimal(10,2)
);
INSERT INTO ww_edges VALUES (1,1,null,1);
INSERT INTO ww_edges VALUES (1,2,1,1);
INSERT INTO ww_edges VALUES (1,3,2,1);
INSERT INTO ww_edges VALUES (1,4,2,8);
INSERT INTO ww_edges VALUES (1,5,1,2);
INSERT INTO ww_edges VALUES (1,6,5,1);
INSERT INTO ww_edges VALUES (1,4,5,12);
INSERT INTO ww_edges VALUES (1,7,1,8);
INSERT INTO ww_edges VALUES (1,6,7,.5);
INSERT INTO ww_edges VALUES (1,8,7,4);
INSERT INTO ww_edges VALUES (1,9,1,4);
INSERT INTO ww_edges VALUES (1,10,9,1);
INSERT INTO ww_edges VALUES (1,4,9,1);
 
INSERT INTO ww_edges VALUES (11,11,null,1);
INSERT INTO ww_edges VALUES (11,2,11,1);
INSERT INTO ww_edges VALUES (11,3,2,1);
INSERT INTO ww_edges VALUES (11,4,2,8);
INSERT INTO ww_edges VALUES (11,5,11,2);
INSERT INTO ww_edges VALUES (11,6,5,1);
INSERT INTO ww_edges VALUES (11,4,5,12);
INSERT INTO ww_edges VALUES (11,7,11,8);
INSERT INTO ww_edges VALUES (11,6,7,.5);
INSERT INTO ww_edges VALUES (11,8,7,4);
INSERT INTO ww_edges VALUES (11,9,11,4);
INSERT INTO ww_edges VALUES (11,10,9,1);
INSERT INTO ww_edges VALUES (11,4,9,11);
INSERT INTO ww_edges VALUES (11,12,11,1);

Here is an adaptation of the breadth-first edge list algorithm to retrieve a Bill of Materials for a product identified by a rootID:

·  Initialise a level-tracking variable to zero.

·  Seed a temp reporting table with the rootID of the desired product.

·  While rows are being retrieved, increment the level tracking variable and add rows to the temp table whose parentnodeIDs are nodes at the current level.

·   Print the BOM ordered by path with indentation proportional to tree level.

Listing 36: A simpler parts explosion
DROP PROCEDURE IF EXISTS ww_bom;
DELIMITER |
CREATE PROCEDURE ww_bom( root INT )
BEGIN
  DECLARE lev INT DEFAULT 0;
  DECLARE totalcost DECIMAL( 10,2);
  DROP TABLE IF EXISTS temp;
  CREATE TABLE temp                                 -- initialise temp table with root node
  SELECT
    e.nodeID AS nodeID,
    n.description AS Item,
    e.parentnodeID,
    e.qty,
    n.cost AS nodecost,
    e.qty * n.cost AS cost,
    0 as level,                                     -- tree level
    CONCAT(e.nodeID,'') AS path                     -- path to this node as a string
  FROM ww_nodes n
  JOIN ww_edges e USING(nodeID)                     -- root node
  WHERE e.nodeID = root AND e.parentnodeID IS NULL;
  WHILE FOUND_ROWS() > 0 DO 
    BEGIN
      SET lev = lev+1;                              -- increment level
      INSERT INTO temp                              -- add children of this level
      SELECT 
        e.nodeID,
        n.description AS Item,
        e.parentnodeID,
        e.qty,
        n.cost AS nodecost,
        e.qty * n.cost AS cost,
        lev,                                
        CONCAT(t.path,',',e.nodeID)
      FROM ww_nodes n
      JOIN ww_edges e USING(nodeID)
      JOIN temp t ON e.parentnodeID = t.nodeID
      WHERE e.rootID = root AND t.level = lev-1;
    END;
  END WHILE;
  WHILE lev > 0 DO                                  -- percolate costs up the graph
    BEGIN
      SET lev = lev - 1;
      DROP TABLE IF EXISTS tempcost;
      CREATE TABLE tempcost                         -- compute child cost
      SELECT p.nodeID, SUM(c.nodecost*c.qty) AS childcost
      FROM temp p 
      JOIN temp c ON p.nodeid=c.parentnodeid
      WHERE c.level=lev
      GROUP by p.nodeid;
      UPDATE temp JOIN tempcost USING(nodeID)       -- update parent item cost
      SET nodecost = nodecost + tempcost.childcost;
      UPDATE temp SET cost = qty * nodecost         -- update parent cost
      WHERE level=lev-1;
    END;
  END WHILE;
  SELECT                                            -- list BoM
    CONCAT(SPACE(level*2),Item) AS Item,
    ROUND(nodecost,2) AS 'Unit Cost',
    ROUND(Qty,0) AS Qty,ROUND(cost,2) AS Cost FROM temp
  ORDER by path;  
END |
DELIMITER ;
CALL ww_bom( 1 );
+-------------------+-----------+------+--------+
| Item              | Unit Cost | Qty  | Cost   |
+-------------------+-----------+------+--------+
| finished bookcase |    206.60 |  1.0 | 206.60 |
|   backboard2x1    |      9.80 |  1.0 |   9.80 |
|     laminate2x1   |      8.00 |  1.0 |   8.00 |
|     screw         |      0.10 |  8.0 |   0.80 |
|   side            |     25.20 |  2.0 |  50.40 |
|     screw         |      0.10 | 12.0 |   1.20 |
|     plank         |     20.00 |  1.0 |  20.00 |
|   shelf           |     16.00 |  8.0 | 128.00 |
|     plank         |     20.00 |  0.5 |  10.00 |
|     shelf bracket |      0.50 |  4.0 |   2.00 |
|   foot            |      2.10 |  4.0 |   8.40 |
|     cube4cmx4cm   |      1.00 |  1.0 |   1.00 |
|     screw         |      0.10 |  1.0 |   0.10 |
+-------------------+-----------+------+--------+

Summary

Stored procedures, stored functions and Views make it possible to implement edge list graph models, nested sets graph models, and breadth-first and depth-first graph search algorithms in MySQL 5&6.

Further Reading

Celko J, "Trees and Hierarchies in SQL For Smarties", Morgan Kaufman, San Francisco, 2004.

Codersource.net, "Branch and Bound Algorithm in C#", http://www.codersource.net/csharp_branch_and_bound_algorithm_implementation.aspx.

Math Forum, "Euler's Solution: The Degree of a Vertex", http://mathforum.org/isaac/problems/bridges2.html

Muhammad RB, "Trees", http://www.personal.kent.edu/~rmuhamma/GraphTheory/MyGraphTheory/trees.htm.

Mullins C, "The Future of SQL", http://www.craigsmullins.com/idug_sql.htm.

Rodrigue J-P, "Graph Theory: Definition and Properties", http://people.hofstra.edu/geotrans/eng/ch2en/meth2en/ch2m1en.html.

Santry P, "Recursive SQL User Defined Functions", http://www.wwwcoder.com/main/parentid/191/site/1857/68/default.aspx.

Shasha D, Wang JTL, and Giugno R, "Algorithmics and applications of tree and graph searching", In Symposium on Principles of Database Systems, 2002, p 39--52.

Stephens S, "Solving directed graph problems with SQL, Part I", http://builder.com.com/5100-6388_14-5245017.html.

Stephens, S, "Solving directed graph problems with SQL, Part II", http://builder.com.com/5100-6388_14-5253701.html.

Steinbach T, "Migrating Recursive SQL from Oracle to DB2 UDB", http://www-106.ibm.com/developerworks/db2/library/techarticle/0307steinbach/0307steinbach.html.

Tropashko V, "Nested Intervals Tree Encoding in SQL, http://www.sigmod.org/sigmod/record/issues/0506/p47-article-tropashko.pdf

Van Tulder G, "Storing Hierarchical Data in a Database", http://www.sitepoint.com/print/hierarchical-data-database.

Venagalla S, "Expanding Recursive Opportunities with SQL UDFs in DB2 v7.2", http://www-106.ibm.com/developerworks/db2/library/techarticle/0203venigalla/0203venigalla.html.

Wikipedia, "Graph Theory", http://en.wikipedia.org/wiki/Graph_theory.

Willets K, "SQL Graph Algorithms", http://willets.org/sqlgraphs.html.



Last updated 20 Aug 2007

Posted by tornado
|

업데이트가 안되서리...

/etc/yum.conf 를 아래와 같이 수정하고 되었다.


[core]
name=Fedora Core 4
baseurl=http://mirror.secuidc.com/centos/4/os/i386/

[updates]
name=Fedora Core 4 updates
baseurl=http://mirror.secuidc.com/centos/4/updates/i386/

Posted by tornado
|

출처 - http://iccc.skku.ac.kr/zbxe/tip_tech_service/71356


-----------------------------------------------------------------------------

sudo apt-get install mysql-server mysql-client

$sudo cp /etc/mysql/my.cnf /etc/mysql/my.cnf.orig


my.cnf 파일 편집

    [client]
    default-character-set=utf8

    [mysqld]
    character-set-client-handshake=FALSE
    init_connect="SET collation_connection = utf8_general_ci"
    init_connect="SET NAMES utf8"
    default-character-set=utf8
    character-set-server=utf8
    collation-server=utf8_general_ci

    [mysqldump]
    default-character-set=utf8

    [mysql]
    default-character-set=utf8


mysql을 재시작

    $sudo /etc/init.d/mysql restart


변경 여부 확인

    $mysql -u id -p
    mysql> status



Posted by tornado
|
출처 : http://blog.naver.com/mdw9754/10001069368




오라클 소프트웨어 다운로드


오라클 10G R2를 다운로드 한다.

http://www.oracle.com/technology/software/products/database/oracle10g/index.html

Oracle Database 10g Release 2 (10.2.0.1.0) for Linux x86 을 선택
(32bit용 Linux가 설치된 것으로 가정하였다. 만일 64bit용 Linux가 설치되어 있다면 64bit용 오라클을 다운로드 하면 되겠다.)

다운로드한 파일의 압축을 해제한다. (root 계정으로..)
$ unzip 10201_database_linux32.zip
database/stage/Components/oracle.server/10.2.0.3.0/1
database/stage/Components/oracle.server/10.2.0.3.0
database/stage/Components/oracle.server
database/stage/Components/oracle.tg/10.2.0.3.0/1/DataFiles
database/stage/Components/oracle.tg/10.2.0.3.0/1
database/stage/Components/oracle.tg/10.2.0.3.0
database/stage/Components/oracle.tg
database/stage/Components/oracle.assistants.dbca/10.2.0.3.0/1/DataFiles/doc.3.1.jar
database/stage/Components/oracle.assistants.dbca/10.2.0.3.0/1/DataFiles/class.jar
...

커널 변수(Kernel parameter) 확인

Oracle 10G R2를 설치하기 위한 커널 변수는 다음을 만족해야 한다.

shmmax  = 2147483648
shmmni  = 4096
shmall  = 2097152
shmmin  = 1
shmseg  = 10
semmsl  = 250
semmns  = 32000
semopm  = 100
semmni  = 128
file-max = 65536
ip_local_port_range = 1024 65000

다음과 같이 현재 오라클을 설치하려는 Linux 시스템의 커널 변수를 확인할 수 있다.


su - root
sysctl -a


너무 많은 내용이 스크롤 되어 확인하기 힘들다면 grep 명령어로 각각 확인해볼 수 있다.


sysctl -a | grep shmmax


아마도 대부분 Linux를 설치한 후 초기 상태라면 오라클 10G R2 설치 조건을 만족하지 못할 것이다.

다음과 같이 커널 변수를 세팅할 수 있다.

/etc/sysctl.conf 파일을 열어 다음 내용을 추가한다.

kernel.shmmax=2147483648
kernel.sem=250 32000 100 128
fs.file-max=65536
net.ipv4.ip_local_port_range=1024 65000

여기서 수정한 커널 변수를 Linux를 리부팅하면 자동적으로 적용되겠지만, 다음과 같이 리부팅하지 않고 즉시 적용이 가능하다.

su - root
sysctl -p



소프트웨어 패키지 확인(RPM)

오라클 10G R2를 설치하기 위해 다음과 같은 패키지가 사전에 설치되어 있어야 한다.

  make-3.79.1
  gcc-3.2.3-34
  glibc-2.3.2-95.20
  compat-db-4.0.14-5
  compat-gcc-7.3-2.96.128
  compat-gcc-c++-7.3-2.96.128
  compat-libstdc++-7.3-2.96.128
  compat-libstdc++-devel-7.3-2.96.128
  openmotif21-2.1.30-8
  setarch-1.3-1
  libaio-0.3.96-5

다음 명령어로 확인할 수 있다.

rpm -q make gcc glibc compat-db compat-gcc compat-gcc-c++ compat-libstdc++ compat-libstdc++-devel openmotif21 setarch libaio


설치되지 않았거나 구버전이 설치되어 있다면, 알아서(ㅡ.ㅡ) 구해 설치하도록 한다. 물론 꼭 없어도 오라클의 기본적인 설치와 구동은 가능하지만, 오라클 10G R2의 모든 기능을 정상적으로 사용하기를 원한다면 설치하도록 권장한다.


오라클 10G R2는 Linux에 설치할 때 Linux 소프트웨어 패키지를 확인하여 지원 가능한 Linux에만 설치를 진행한다.

필자는 Haansoft Linux에 설치를 진행하려 하는데 오라클 10G R2는 Haansoft Linux를 알지 못하므로 편법으로 약간 수정이 필요하다.

오라클 10G R2는 Redhat Linux를 지원하므로 Radhat Linux 인것처럼 속이도록 한다.

즉, /etc/redhat-release 파일을 생성하여 Haansoft Linux를 Redhat Linux인것처럼 하면 된다.


su - root
cat > /etc/redhat-release << EOF
Red Hat Enterprise Linux AS release 3 (Taroon)
EOF


오라클 설치가 끝난후 다시 삭제하면 된다. (삭제하지 않아도 별 문제 없음)

이렇게 하면 오라클 OUI (Oracle Universal Installer)가 Redhat Linux 로 인식하여 정상적으로 설치를 진행할 수 있다.

참고로, "runInstaller -ignoreSysPrereqs" 명령으로 무시하고 진행해도 되지만, 권장사항은 아니다.

oracle 계정 생성 및 .profile 편집

오라클 10G R2를 설치하고 운영하기 위해서는 적절한 Linux 사용자 계정이 필요하다. 물론, 아무 계정이나 만들어서 설치해도 되고, root에 설치해도 상관 없지만, 원할한 설치와 운영을 위해서는 꼭 별도의 오라클 계정을 생성하여 설치하도록 권장한다.

oracle 계정을 생성하여 dba 그룹에 소속하도록 한다.

dba 그룹 생성(gid를 700으로 설정하였다.)

groupadd -g 700 dba


oracle 계정 생성(uid를 700으로 설정하고 소속 그룹을 dba로 하였다.)

useradd -u 700 -c "Oracle Administrator" -s  /bin/ksh -g dba oracle
passwd oracle
******


위와 같이 command를 사용해도 되지만, GUI 환경이 편하다면 GUI환경에서 그룹과 계정을 각각 생성하여도 상관 없다. oracle이 사용하는 shell은 ksh(Kohn shell)을 사용하도록 하였다.

oracle 계정의 .profile 파일을 수정하자. (없으면 생성한다.) 다음 항목을 추가한다.


export ORACLE_BASE=/u/app/oracle
export ORACLE_HOME=$ORACLE_BASE/product/10.2.0/db_1
export PATH=$PATH:$ORACLE_HOME/bin
export LD_LIBRARY_PATH=$ORACLE_HOME/lib


만일 oracle 계정의 shell을 ksh 로 하지 않고, bash로 했다면, .profile 파일을 수정하지 않고, .bash_profile 파일을 수정하도록 한다.


여유 디스크 공간 확인 및 디렉토리 생성

오라클 10G R2를 설치하기 위해서는 최소한 1GB 이상의 디스크 공간이 필요하다.

또한, 오라클을 설치하기 전에 필요한 디렉토리는 미리 생성해두는 것이 좋다.

일반적으로 오라클 S/W와 Datafile(실제 데이터가 저장되는 공간)의 물리적인 위치를 별도로 지정하는 것이 좋다.

oracle 계정의 home 디렉토리와 오라클 S/W가 설치되는 곳은 달라도 상관 없다.

우선, 오라클 S/W는 /u/app/oracle/product/10.2.0/db_1 에 설치하도록 하자. (Datafile 및 기타 파일의 위치는 오라클 설치 후 DB 생성시 고려하면 되겠다.)

su - root
mkdir -p /u/app/oracle/product/10.2.0/db_1
chown -R oracle:dba /u/app/oracle




오라클 10G R2 설치

oracle 계정으로 switch user (su) 한 다음, 다운받은 오라클 설치 파일을 압축해제한 디렉토리로 이동하여 runInstaller를 실행한다.


# xhost +
# su - oracle
$ cd database
$ ./runInstaller


xhost + 명령을 실행한 이유는 간혹 oracle 계정에서 Display 세팅때문에 OUI를 실행하지 못하는 경우가 있는데, Display의 제어권을 root에서 oracle로 넘겨주기 위함이다. xhost + 를 실행하지 않아도 잘 되는 경우에는 그냥 oracle계정으로 su 하여 runInstaller를 실행하면 된다.


runInstaller를 실행시키면 몇 초 후 OUI (Oracle Universal Installer)가 실행된다.

- 설치방식선택 : 고급설치 (기본설치를 선택하면 쉽게 할 수 있지만,
                                      고급설치를 통해 좀 더 배워보자.)
                        "다음" 클릭

- 인벤토리 디렉토리 및 인증서 지정
                     : 인벤토리 디렉토리에 대한 전체 경로 입력 (제품에 대한 설치정보를 저장하는 곳이며 약 150KB정도가 사용된다.)
                        -> /u/app/oracle/oraInventory
                     : 운영체제 그룹 이름 지정 (oracle을 설치하는 Linux 계정이 속한 그룹명을 입력하면 된다.)
                        -> dba
                        "다음" 클릭

-  설치유형 선택 : 사용자정의 (보통 Enterprise Edition을 선택하면 되지만, 여기서는 필요한 부분만 최소용량으로 설치하기 위해 사용자 정의를 선택하였다.)
                        "다음" 클릭

- 홈 세부 정보 지정
                       : 이름 (oraInventory 내에 저장하기 위한 이름으로 특별한 의미는 없으며 추후 설치된 오라클을 제거할 때의 이름으로 사용되므로 적절히 입력하거나 입력되어 있는 내용을 그대로 사용하면 된다.)
                          -> OraDB10g_home1
                       : 경로 (.profile에서 ORACLE_HOME 으로 지정한 디렉토리가 입력되어 있을 것이다. 확인하고 틀렸으면 수정하자.)
                          -> /u/app/oracle/product/10.2.0/db_1
                          "다음" 클릭

- 사용가능한 제품 구성 요소 (설치 유형에서 "사용자정의"를 선택하였기때문에 제품 구성요소를 선택하여 설치를 할 수 있다. Eterprise Edition을 선택했다면 이 화면은 나타나지 않는다.)
                        : 선택되어 있는대로 진행해도 상관 없지만, 필자의 경우 개발 목적으로 설치하는 것이 아니므로
                           -> Oracle Programmer 10.2.0.1.0 체크 해제
                           -> Oracle XML Developer Kit 10.2.0.1.0 체크 해제
                                (나머지 항목은 그대로)
                            "다음" 클릭

- 제품별 필요 조건 검사 : 사전에 커널변수와 필요한 패키지들을 미리 세팅해두었다면 큰 무리 없이 통과가 가능하다. 만일, 상태 항목에 "경고" 라는 문구가 있는 항목이 있으면 아래 내용을 읽어보고 조치한 후 "재시도" 버튼을 클릭하면 되고, 귀찮으면(ㅡ.ㅡ) "경고" 바로 좌측에 있는 체크박스를 모두 클릭한 후 "다음" 클릭.

- 권한 부여된 운영체제 그룹
                        : 데이터베이스 관리자(OSDBA) 그룹
                            -> dba
                        : 데이터베이스 운영자(OSOPER) 그룹
                            -> dba
                         (국내의 경우 관리자와 운영자가 동일한 경우가 대부분이지만, 외국의 경우 실제로 관리자와 운영자가 분리되어 있으므로 이러한 선택사항이 있는것으로 보인다. 모두 dba로 입력하자.)
                         "다음" 클릭.

- 데이터베이스 생성 : 데이터베이스 소프트웨어만 설치
                         (여기서 데이터베이스를 생성해도 되지만, 이것저것 준비할 것도 많고, 시간이 오래걸리므로 소프트웨어만 설치하고 데이터베이스는 나중에 생성하도록 하자.)
                         "다음" 클릭.

- 요약 : 지금까지 세팅한 설치 옵션들을 한 눈에 요약하여 보여준다.
           만일 틀린부분이 있다면 뒤로 가서 다시 세팅하도록 하자.
           "설치" 클릭.

- 설치 (설치는 상당한 시간이 소요된다. 시스템의 사양에 따라서 많은 차이가 있지만, 대략 1시간 정도 생각하면 되겠다. 설치하는데 심심하다면 설치화면 하단에 있는 로그파일 위치에 가서 생성되는 로그를 확인해보는 것도 좋겠다.)

- 구성 스크립트 실행 (새로운 터미널 창을 열어 root로 switch user(su - root)한 후 스크립트를 실행한다.)
    mkdir /opt  (Haansoft Linux에는 /opt 디렉토리가 없으므로 생성하도록 한다. root.sh 실행시 필요하다.)
    /u/app/oracle/oraInventory/orainstRoot.sh
    /u/app/oracle/product/10.2.0/db_1/root.sh
  (현재 오라클을 설치하는 oracle 계정은 타 디렉토리에 대한 생성 권한이 없기때문에 root로 위 스크립트를 실행하여 필요한 환경을 세팅하는 것이다.)
    "종료" 클릭.

오라클 10G R2 가 성공적으로 설치되었다.
Posted by tornado
|
xxxObj.onclick = function (e) {
 if (window.event) e = window.event; 
var srcEl = e.srcElement? e.srcElement : e.target;
// srcEl now can be used x-browser.
// (rest of the script here)
}

-----------------------------------------------------------------------
ex) 쇼핑몰 : 판매업체 선택 --> 상품 목록 출력 --> 가격출력 순으로 작업되면
아래와 비슷한 함수들을 만들어 놓고 사용함

function printGoods(e){
if (window.event) e = window.event;
var srcEl = e.srcElement? e.srcElement : e.target;

if(!srcEl) return;

var vendorCode = srcEl.abbr;

if(!vendorCode) return;

var hidVendorCode = document.getElementById("hidVendorCode");
hidVendorCode.value = vendorCode;

var tmpArr = new Array();

for(var i = 0; i < goodsArr.length; i++){
if(goodsArr[i].VENDOR_CODE == vendorCode){
tmpArr.push(goodsArr[i]);
}
}

for(var i = 0; i < tmpArr.length; i++){
var id = "goodsTD_"+ (i + 1);
var goodsTD = document.getElementById(id);

if(goodsTD){
goodsTD.innerHTML = tmpArr[i].GOODS_NAME;
goodsTD.abbr = tmpArr[i].GOODS_CODE;
addEvent(id, "click", printPrice );
}
}
}



'DHTML > Javascript' 카테고리의 다른 글

[그래프 툴] jquery flot  (0) 2009.06.18
[펌] 부모창과 닫히는 팝업창  (0) 2009.04.28
[링크] 스크립트 주석 표준  (0) 2008.07.29
마우스 우클릭 금지  (0) 2008.03.08
TextArea 에 숫자만 입력하기  (0) 2007.11.14
Posted by tornado
|
status.index가 핵심이군....
홀짝 구별하려면 status.odd 로 하면 될것 같음.




<s:iterator value="model.targetBranchList" status="status">
    <s:property value="branchName" />
    <s:if test="#status.index <= (model.targetBranchList.size() - 2)">,</s:if>
</s:iterator>
Posted by tornado
|
출처 : http://www.ociweb.com/mark/programming/WAX.html

삽질 끝????

Introduction

What's the best way to read a large XML document? Of course you'd use a SAX parser or a pull parser. What's the best way to write a large XML document? Building a DOM structure to describe a large XML document won't work because it won't fit in memory. Even if it did, it's not a simple API to use. There hasn't been a solution that is simple and memory efficient until now.

Writing API for XML (WAX) is a free, open-source, library for writing XML documents. I created it because I got an OutOfMemoryError while trying to output a large XML document from an application I wrote using JDOM, another Java-based XML library. I searched for other libraries that could write large XML documents but couldn't find any that were as simple to use as I thought they should be.

WAX is released under the LGPL with the intention of making its use unencumbered. It is well-tested and ready for production use. The WAX home page is at http://www.ociweb.com/wax/. Java and Ruby versions are available now. The Java version of WAX can be downloaded from Google Code at http://code.google.com/p/waxy/. For information about the Ruby version, click here. Ports for other programming languages will follow.

WAX has the following characteristics:

  • focuses on writing XML, not reading it
  • requires less code than other approaches
  • uses less memory than other approaches
    (because it outputs XML as each method is called rather than
    storing it in a DOM-like structure and outputting it later)
  • doesn't depend on any Java classes other than standard JDK classes
  • is a small library (around 16K)
  • writes all XML node types
  • always outputs well-formed XML or throws an exception unless running in "trust me" mode
  • provides extensive error checking
  • automatically escapes special characters in text and attribute values (unless "unescaped" methods are used)
  • allows most error checking to be turned off for performance
  • knows how to associate DTDs, XML Schemas and XSLT stylesheets with the XML it outputs
  • is well-suited for writing XML request and response messages for REST-based and SOAP-based services

WAX Tutorial

This section provides many examples of using WAX. Each code snippet is followed by the output it produces.

When the no-arg WAX constructor is used, XML is written to standard output. There are also WAX constructors that take a java.io.OutputStream or a java.io.Writer object.

Here's a simple example where only a root element is written:

WAX wax = new WAX();
wax.start("car").close();
<car/>

After a WAX object is closed, a new one must be created in order to write more XML. In the examples that follow, assume that has been done.

Let's write a root element with some text inside:

wax.start("car").text("Prius").end().close();
<car>Prius</car>

The default indentation used is two spaces. The end method terminates the element that is started by the start method. In this case it's not necessary to call end because the close method terminates all unterminated elements.

Let's put the text inside a child element:

wax.start("car").start("model").text("Prius").close();
<car>
<model>Prius</model>
</car>

Let's do the same with the child convenience method: which is equivalent to calling start, text and end.

wax.start("car").child("model", "Prius").close();
<car>
<model>Prius</model>
</car>

Let's put text containing all the special XML characters in a CDATA section:

wax.start("car").start("model").cdata("1<2>3&4'5\";6").close();
<car>
<model>
<![CDATA[1<2>3&4'5"6]]>
</model>
</car>

Let's output the XML without indentation, on a single line:

wax.noIndentsOrLineSeparators();
wax.start("car").child("model", "Prius").close();
<car><model>Prius</model></car>

Let's indent the XML with four spaces instead of the default of two:

wax.setIndent("    "); // can also call setIndent(4)
wax.start("car").child("model", "Prius").close();
<car>
<model>Prius</model>
</car>

Let's add an attribute:

wax.start("car").attr("year", 2008).child("model", "Prius").close();
<car year="2008">
<model>Prius</model>
</car>

Attributes must be specified before any content for their element is specified. For example, calling start, attr and text is valid, but calling start, text and attr is not. If this rule is violated then an IllegalStateException is thrown.

Let's add an XML declaration:

WAX wax = new WAX(Version.V1_0); // Version is an enum
wax.start("car").attr("year", 2008)
.child("model", "Prius").close();
<?xml version="1.0" encoding="UTF-8"?>
<car year="2008">
<model>Prius</model>
</car>

Let's add a comment:

wax.comment("This is a hybrid car.")
.start("car").child("model", "Prius").close();
<!-- This is a hybrid car. -->
<car>
<model>Prius</model>
</car>

Let's add a processing instruction:

wax.processingInstruction("target", "data")
.start("car").attr("year", 2008)
.child("model", "Prius").close();
<?target data?>
<car year="2008">
<model>Prius</model>
</car>

Let's associate an XSLT stylesheet with the XML: The xslt method is a convenience method for adding this commonly used processing instruction.

wax.xslt("car.xslt")
.start("car").attr("year", 2008)
.child("model", "Prius").close();
<?xml-stylesheet type="text/xsl" href="car.xslt"?>
<car year="2008">
<model>Prius</model>
</car>

Let's associate a default namespace with the XML:

wax.start("car").attr("year", 2008)
.defaultNamespace("http://www.ociweb.com/cars")
.child("model", "Prius").close();
<car year="2008"
xmlns="http://www.ociweb.com/cars">
<model>Prius</model>
</car>

Let's associate a non-default namespace with the XML:

String prefix = "c";
wax.start(prefix, "car").attr("year", 2008)
.namespace(prefix, "http://www.ociweb.com/cars")
.child(prefix, "model", "Prius").close();
<c:car year="2008"
xmlns:c="http://www.ociweb.com/cars">
<c:model>Prius</c:model>
</c:car>

Like attributes, namespaces must be specified before any content for their element is specified. If this rule is violated then an IllegalStateException is thrown.

Let's associate an XML Schema with the XML:

wax.start("car").attr("year", 2008)
.defaultNamespace("http://www.ociweb.com/cars", "car.xsd")
.child("model", "Prius").close();
<car year="2008"
xmlns="http://www.ociweb.com/cars"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xsi:schemaLocation="http://www.ociweb.com/cars car.xsd">
<model>Prius</model>
</car>

Let's associate multiple XML Schemas with the XML:

wax.start("car").attr("year", 2008)
.defaultNamespace("http://www.ociweb.com/cars", "car.xsd")
.namespace("m", "http://www.ociweb.com/model", "model.xsd")
.child("m", "model", "Prius").close();
<car year="2008"
xmlns="http://www.ociweb.com/cars"
xmlns:m="http://www.ociweb.com/model"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xsi:schemaLocation="http://www.ociweb.com/cars car.xsd
http://www.ociweb.com/model model.xsd">
<m:model>Prius</m:model>
</car>

Let's associate a DTD with the XML:

wax.dtd("car.dtd")
.start("car").attr("year", 2008)
.child("model", "Prius").close();
<!DOCTYPE car SYSTEM "car.dtd">
<car year="2008">
<model>Prius</model>
</car>

Let's add and use entity definitions:

String url = "http://www.ociweb.com/xml/";
wax.entityDef("oci", "Object Computing, Inc.")
.externalEntityDef("moreData", url + "moreData.xml")
.start("root")
.unescapedText("The author works at &oci; in St. Louis, Missouri.",
true) // avoiding escaping for entity reference
.unescapedText("&moreData;", true)
.close();
<!DOCTYPE root [
<!ENTITY oci "Object Computing, Inc.">
<!ENTITY moreData SYSTEM "http://www.ociweb.com/xml/moreData.xml">
]>
<root>
The author works at &oci; in St. Louis, Missouri.
&moreData;
</root>

A common usage pattern is to pass a WAX object to a method of model objects that use it to write their XML representation. For example, a Car class could have the following method.

public void toXML(WAX wax) {
wax.start("car")
.attr("year", year)
.child("make", make)
.child("model", model)
.end();
}

An example of the XML this would produce follows:

<car year="2008">
<make>Toyota</make>
<model>Prius</model>
</car>

A Person class whose objects hold a reference to an Address object could have the following method.

public void toXML(WAX wax) {
wax.start("person")
.attr("birthdate", birthdate)
.child("name", name);
address.toXML(wax);
wax.end();
}

The Address class could have the following method.

public void toXML(WAX wax) {
wax.start("address")
.child("street", street);
.child("city", city);
.child("state", state);
.child("zip", zip);
.end();
}

An example of the XML this would produce follows:

<person birthdate="4/16/1961">
<name>R. Mark Volkmann</name>
<address>
<street>123 Some Street</street>
<city>Some City</city>
<state>MO</state>
<zip>12345</zip>
</address>
</person>

Posted by tornado
|

출처 : http://wheelersoftware.com/articles/spring-cxf-web-services.html



1 | 2 | 3 | 4 | Next »
Software Development

Web Services with Spring 2.5 and Apache CXF 2.0

Quickly create web services in Spring 2.5 using Apache CXF 2.0, a.k.a. XFire 2.0.

In this tutorial I explain how to get a web service up and running using Spring 2.5 and Apache CXF 2.0, which is the combination of Celtix and XFire and is considered XFire 2.0. (I don't know what the Celtix team would say about that, but that's what the XFire site says.) Here I just treat the web service itself; to learn about consuming the web service using Spring and CXF, please see the article Make Web Services Transparent with Spring 2.5 and Apache CXF 2.0.

CXF supports both WSDL-first and Java-first web service development. This article takes the Java-first approach.

Project Setup

The first thing you'll need to do is download CXF from the Apache CXF site. At the time of this writing the project is in incubator status and the latest version is 2.0.4. That's the version I'm using.

You'll also find it useful to know about the section of the CXF user documentation that deals with writing a service with Spring, but currently the docs describe how to integrate with Spring 2.0, and since I want to integrate with Spring 2.5, there are some differences worth highlighting along the way.

Also, the docs describe a "Hello, World" web service that just returns a string, and in this tutorial we want to go a little further than that and actually do a class databinding.

Here are the service-side dependencies you'll need. Inside the distribution there is a file called WHICH_JARS that describes in a little more detail what the JARs are for and which ones you'll really need, if you're interested in that. But the following is essentially the list given in the user docs.

CXF itself

  • cxf-2.0.4-incubator.jar

CXF dependencies

Note that for CXF 2.0.4 the documented dependencies are almost but not quite the same as the JARs that are actually included in the distribution, once again, at the time of this writing (February 2008). Specifically the stax, neethi and XMLSchema JARs are not the ones listed. Here's the corrected list for CXF 2.0.4:

  • commons-logging-1.1.jar
  • geronimo-activation_1.1_spec-1.0-M1.jar (or Sun's Activation jar)
  • geronimo-annotation_1.0_spec-1.1.jar (JSR 250)
  • geronimo-javamail_1.4_spec-1.0-M1.jar (or Sun's JavaMail jar)
  • geronimo-servlet_2.5_spec-1.1-M1.jar (or Sun's Servlet jar)
  • geronimo-stax-api_1.0_spec-1.0.jar
  • geronimo-ws-metadata_2.0_spec-1.1.1.jar (JSR 181)
  • jaxb-api-2.0.jar
  • jaxb-impl-2.0.5.jar
  • jaxws-api-2.0.jar
  • neethi-2.0.2.jar
  • saaj-api-1.3.jar
  • saaj-impl-1.3.jar
  • wsdl4j-1.6.1.jar
  • wstx-asl-3.2.1.jar
  • XmlSchema-1.3.2.jar
  • xml-resolver-1.2.jar

Aegis dependencies

In addition to the above, you will need to add jdom-1.0.jar since Aegis databinding uses it.

Spring dependencies

In this case ignore what's in the CXF documentation, because we're integrating with Spring 2.5 instead of Spring 2.0. I'm going to assume that you have Spring 2.5 already set up on your web services project, including Hibernate or anything else that your web services will need on the implementation side.

Just in case you were wondering, you don't need the Spring MVC module's DispatcherServlet as CXF comes with its own servlet, org.apache.cxf.transport.servlet.CXFServlet.

OK, that should be good for basic project setup. Let's write a simple service.


Posted by tornado
|
출처 : http://www.vitarara.org/cms/struts_2_cookbook/post_and_redirect


<action name="createSalesOrderConfirmation" class="sales.CreateSalesOrderAction">
<result name="redirect" type="redirect-action">
<param name="actionName">displaySalesOrder</param>
<param name="namespace">/order/sales</param>
<param name="parse">true</param>
<param name="id">${order.id}</param>
</result>
</action>
Posted by tornado
|

Struts2 에서는 아래와 같은 방법으로 사용 가능합니다.

 

src/defaultConfigure.properties format 을 등록.

 

# number format

format.qty={0,number, ,###,###,###}

 

JSP 페이지에서는 다음과 같이 호출합니다.

 

<s:text name="format.qty"><s:param name="value" value="%{10000}"/></s:text>

 

결과는 10,000 과 같이 출력됩니다.

Posted by tornado
|
출처 : http://saloon.javaranch.com/cgi-bin/ubb/ultimatebb.cgi?ubb=get_topic&f=56&t=006418

자바를 너무 오랫동안 안했나보다... 완전 허무 ...


You should be able to delete ${catalina.home}/work/Catalina/localhost/appName/SESSION.ser where appName is your application (or just delete the whole work directory).

This should enable you to restart.



Posted by tornado
|
출처 : http://struts.apache.org/2.0.12/docs/how-do-i-set-a-global-resource-bundle.html



In Struts 2, resource bundles can be associated with classes. The framework will automatically discover and load class-orientated resource bundles. You can also specify one or more global resource bundles, which would be available to all classes in the application, using either the standard properties file, or a custom listener.

Properties file

Global resource bundles can be specified in the struts.properties configuration file.

struts.properties
struts.custom.i18n.resources=global-messages
The framework searches the class heirarchy first, then, as a last resource, checks the global resources.

Multiple resource bundles can be specified by providing a comma-separated list.

struts.properties
struts.custom.i18n.resources=global-messages, image-messages

Listener

Aside from the properties file, a Listener could also be used to load global resource bundles.

ActionGlobalMessagesListener.java
public class ActionGlobalMessagesListener implements ServletContextListener {
private static Logger log = Logger.getLogger(ActionGlobalMessagesListener .class);
private static final String DEFAULT_RESOURCE = "global-messages";

/**
* Uses the LocalizedTextUtil to load messages from the global message bundle.
* @see
javax.servlet.ServletContextListener#contextInitialized(javax.servlet.Servle
tContextEvent)
*/
public void contextInitialized(ServletContextEvent arg0) {
log.info("Loading global messages from " + DEFAULT_RESOURCE);
LocalizedTextUtil.addDefaultResourceBundle(DEFAULT_RESOURCE);
log.info("Global messages loaded.");
}

/**
* @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent)
*/
public void contextDestroyed(ServletContextEvent arg0) {

// do nothing

}
}

web.xml:
(under listeners section)

web.xml
<listener>
<listener-class>mypackagename.ActionGlobalMessagesListener</listener-class>
</listener>

Posted by tornado
|
간단하게 되는군요

<page:applyDecorator name="theDecorator">
    <s:action name="footer" executeResult="true" />
</page:applyDecorator>
Posted by tornado
|

출처 : http://mwultong.blogspot.com/2006/10/ubuntu-root-root.html


Q: root 아이디로 로그인이 안됩니다



원래 우분투 리눅스는 root (관리자 계정)로 로그인할 수 없습니다. 설치할 때 사용자 ID를 root 로 정하면 로그인이 아예 불가능하게 됩니다.


다음은 우분투에 root 계정과 암호를 만들어 주는 방법입니다.

프롬프트에서

sudo passwd root

라고 합니다. 주의! 위에서 "passwd"라는 문자열은 진짜 암호가 아니라 문자 그대로 입력해야 합니다.

만약 패스워드가 foo 라고 해서

sudo foo root

이렇게 하면 안됩니다. 정확히 sudo passwd root 이렇게 적어 주어야 합니다.

그러면 현재 암호를 먼저 묻습니다. 현재 로그인한 ID의 암호를 한번 입력해 주면 이제

Enter new UNIX password:

라고 나오며 root 의 암호를 2번 묻습니다. 새 암호를 만들어 적어 주면 됩니다.

그러면 이제 root 로 로그인할 수 있습니다. root 계정이 생기는 것입니다.

Ctrl+D키를 눌러, 로그아웃한 후 root 로 로그인해 봅니다.


그런데 root 의 패스워드가 짧고 간단하다면 해커들의 표적이 됩니다. 되도록 길고 복잡해야 합니다.
Posted by tornado
|

http://izpack.org/

이런 툴도 있었네요.

인스톨 쉴드만 써봤는데... 리눅스에서는 콘솔로 할까~ 생각중이였는데 이런 툴이 있었군요.

조만간 테스트 해봐야겠습니다.

 

사용자 삽입 이미지


Posted by tornado
|