출처 : http://tigerwoods.tistory.com/31


예제 프로젝트 다운로드



 

  

1. 환경설정 개요 (Preferences)

 

안드로이드 플랫폼은 Data를 저장하는 방법으로 환경설정(이하 Preferences), 파일, Local DB, 네트워크를 제공한다.

 

그 중 Preferences는 가장 간단하게 정보를 저장하는 방법(mechanism)을 제공하며, App이나 그 컴포넌트 (Activity, Service 등)의 환경 설정 정보를 저장/복원하는 용도로 사용된다.

 

 

▌Preferences의 형태▐

안드로이드에서 Preferences는 ListView의 형태로 표현되며 쉬운 Preferences의 구현을 위해 PreferenceActivity 클래스를 제공한다. PreferenceActivity 클래스는 XML 기반의 Preference 정의 문서를 통해 App 파일 저장소에 Preferences 파일을 생성하고 사용하는 방식으로 작동한다. (참고: 일반 Activity를 사용해 ListView형태의 Preference Activity가 아닌 커스텀 Preference Activity를 구현 할 수도 있지만 (Container + 각종 UI 위젯 사용) 이 글에서는 그 방법에 대해 다루지 않으려고 합니다)

 

 

▌Preferences 데이터 저장▐

Preferences를 위한 데이터 저장 시 사용되는 자료구조는 Map 방식이다. 즉 키(key)-값(value) 한 쌍으로 이루어진 1~n개의 항목으로 구성된다. 키(key) 는 String 타입으로, 말 그대로 각 데이터에 접근할 수 있게 하는 유일한 구분자며, 값(Value)은 기본 자료형 (int, boolean, string 등) 기반의 데이터로 구성된다. 다음은 Map 데이터 구조의 한가지 예이다.

 키(Key) - String 형
 값(Value) - 기본자료형
 Font
 "Noraml"
 FontSize 20
 FontColor FFFFFFFF
 ... ...
 ... ...


 

위와 같은 Preferences데이터는 안드로이드 디바이스의

data/data/App 패키지 이름/shared_prefs/

경로에 XML형태의 파일로 생성된다. xml 파일 이름은, 파일 생성/접근을 위해 어떤 메서드를 사용하느냐에 따라 시스템이 지정 할 수도 있고, 개발자가 임의의 파일 이름을 지정할 수도 있다.

 

 

참고로, 안드로이드에서 Preferences 데이터 파일을 다수의 App간에 공유하는 것은 기본적으로 불가능 하게 디자인 되어 있다. (참고: 여기서 불가능하다는 뜻은 Preferences Framework에서는 타 App의 환경 설정 파일에 접근할 수 있는 메서드를 제공하지 않는다는 뜻이다. 그럴 필요가 있을지 모르겠지만, 굳이 접근해야 한다면 Preferences 파일 생성 모드를 MODE_WORLD_READABLE로 설정해 guest가 파일을 쓰고 읽을 수 있도록 하고(위에서 MyCustomePref.xml 처럼) Preferences 데이터 파일을 파일 스트림을 통해 읽어와 직접 파싱하고 사용할 수 있는 편법이 있긴 하다)

 

XML파일을 디바이스로부터 추출해 열어보면 Preference 데이터가 다음과 같은 형식으로 저장되어 있음을 볼 수 있다.

1<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
2<map>
3    <boolean name="GreetingMsg" value="true" />
4    <boolean name="sub_checkbox" value="true" />
5    <string name="AdditionalMsg">tigerwoods.tistory.com</string>
6    <string name="TextColor">FF00FF00</string>
7    <string name="Ringtone">content://settings/system/ringtone</string>
8</map>

 

 

Preferences 구현 시 위와 같은 XML 기반 Preferences 데이터 파일을 File I/O API를 사용해 직접 생성/사용 하는 것은 아니다. 안드로이드는 Preferences구현/운용을 위해 android.preference 패키지 및 몇 가지 유용한 클래스/인터페이스(PreferenceActivity, SharedPreferences 등)를 제공하는데, 그것들의 도움을 받으면 손쉽게 Preferences를 구현/운용 할 수 있다.

 

 

▌Preferences 구현▐

안드로이드에서는 Preference 구현을 위해 많은 관련 클래스를 제공하기 때문에 그 클래스들을 적절히 이용하여 다음과 같이 몇 가지 단계만 추가하기만 하면 쉽게 프로젝트에 Preferences 기능을 추가 할 수 있다.

  • 프로젝트 내 어떤 환경정보를 Preferences로 관리 할지 설계
  • 위 설계에 따라 Preferences XML 문서를 작성하고 프로젝트 리소스로 공급 (\res\xml\ 밑)
  • 작성된 Preferences XML문서를 사용하는 PreferenceActivity 구현
  • Preferences가 필요한 Activity로부터 PreferenceActivity를 호출 (Intent 이용)
  • App이 다시 실행될 때 이미 저장된 Preference데이터 파일이 있다면 onResume()과 같은 callback내부에서 환경 설정 값 설정을 불러와 사용

 

위에 나열된 사항 이외에 Preference 데이터 파일의 생성, 데이터 변경 이벤트 listener, 데이터의 저장과 같은 부분은 PreferenceActivity 클래스가 자동으로 관리해 줌으로 개발자는 별로 신경 쓸 필요가 없다. (참고: PreferenceActivity를 사용하지 않고 일반 Activity로 구현한다면 안드로이드가 제공하는 여러 Preferences 관련 클래스 등을 이용해 파일 생성, 이벤트 listener, 저장 등을 직접 구현 해야 한다.)

 

그럼 위 Preference 구현 5단계 중 두 번째인 Preference XML 문서 작성부터 한 번 살펴보자.

 

 

 

 

2. Preference XML 문서 작성하기

 

Preference XML 문서를 작성하기 전에 문서를 구성할 Preferences 관련 클래스들에 관해 약간의 지식이 필요함. 이 글 끝 부분에 "참고. Preference XML 문서 작성시 사용되는 클래스" 단락에 간단한 클래스 설명과 중요 XML 속성, 메서드 등이 설명 되어 있음으로 참고.

 

Preferences XML 문서는 프로젝트 폴더\res\xml\파일이름.xml의 위치에 생성 한다. xml 파일의 이름은 개발자 임의로 지정할 수 있으며 추 후 code 상에서 R.xml.파일이름으로 접근 할 수 있다.

 

이렇게 생성된 XML 문서 내부를 여러 Preference 관련 클래스를 사용해 정의 해야 하는데, 이 글에 포함된 예제 프로젝트에서 사용된 Preference XML 문서를 살펴보면 다음과 같다.

소스 펼치기

 

 

그럼 settings.xml에서 사용된 요소들을 하나씩 분석해 보자

 

 

<PreferenceScreen>

Preferences XML 문서의 root 요소는 항상 <PreferenceScreen>이며 root 요소 내부에는 반드시 xmlns 속성 (xmlns:android=http://......)을 정의해 주어야 한다.

 

root로 사용된 <PreferenceScreen>는 child를 포함하는 container 역할을 하며, PreferenceActivity에게 Preferences 계층 구조를 전달하는 역할을 한다. root로 사용된 <PreferenceScreen>은 container역할 만 하고 자기 자신은 화면에 직접 표현이 안됨으로 title, summary 속성을 지정할 필요가 없다.

 

key 속성에 지정된 문자열은 추 후 code에서 이 preference 계층 구조 내부의 특정 요소를 찾을 수 있는 키 값이 된다.


Preferencescreen root = (PreferenceScreen)getPreference("preference_root");

CheckBoxPreference cb = (CheckBoxPreference)getPreference("checkbox");

 

 

<CheckBoxPreference> & <EditTextPreference>

Root <PreferenceScreen> 밑에 처음 나오는 두 개의 child이며, XML에 추가된 순서대로 Preference view에 표시된다.

각 아이템은 key, title, summary 속성이 지정되어 있으며, checkbox의 경우 초기 설정이 check 상태로 지정되어 있다.

<EditTextPreference> 같은 경우 DialogPreference로부터 상속하며, 클릭 시 별도의 다이얼로그 창을 popup 한다.

 

 

<PreferenceCategory>

예제는 총 2개의 <PreferenceCategory>가 있으며 첫 번째는 <ListPreference>와 <RingtonePreference>를 하나의 Category로 묶는다. 예제 XML의 34번째 라인에 보면 두 번째 Category도 지정되어 있으며 <PreferenceScreen>을 Catefory 멤버로 가지고 있다.

 

<PreferenceCategory>에서 title속성은 다른 Preference 객체와 같이 제목을 표시하지만, summary는 지정되어도 화면에 표시 되지 않는다.

 

또, 첫 번째 category의 속성 중 android:enabled 속성이 false로 지정되어 있음으로 Preference초기 실행 시 child인 <ListPreference>와 <RingtonePreference>는 비활성화 상태로 표현된다. 이와 같이 여러 Preference 아이템을 하나의 category로 묶으면 그룹 속성을 지정하는 것이 가능하다. (Preference의 마지막 항목, checkbox를 check하면 첫 번째 category를 활성화 함)

 

 

<ListPreference>

다른 항목들과 마찬가지로 key, title, summary가 설정되어 있으며, entries 속성에는 사용자에게 보일 ListView 아이템을 지정하며, entryValues속성에는 컴퓨터가 처리할 처리 할 정보를 제공한다. 예를 들면 entries – "Red", "Green", "Blue" 등 사람이 읽을 수 있는 값을 제공하고, entryValues에는 "FFFF0000", "FF00FF00", "FF0000FF" 등 컴퓨터가 사용할 정보를 제공한다.

 

또, android:defaultValue 속성이 "FFFFFFFF"으로 지정되어 있어 사용자가 설정 값을 바꾸기 전까지는 "FFFFFFFF" (흰색)이 기본 값으로 사용된다.

 

 

<RingtonePreference>

key, title, summary가 지정되어 있으며, showDefault와 showSilent가 true로 설정되어 Default 항목과 Slient 항목이 리스트에 표시된다. (<RingtonePreference>는 사용자가 지정한 정보를 바탕으로 Ringtone 설정 기능을 제공하는 Activity들을 찾는 Intent를 발생 시킨다. 디바이스에 여러 종류의 ringtone과 ringtone 관련 activity를 제공하는 app이 설치되어 있다면 더 많은 ringtone 옵션이 화면에 표시 될 수도 있다)

 

 

<PreferenceCategory>

두 번째 그룹으로 CheckBox를 별도의 화면에 표현하는 <PreferenceScreen> child를 포함.

 

 

<PreferenceScreen>

<PreferenceScreen>가 child 요소로 사용된 경우. Preferences에서 하나의 아이템으로 표현되기 때문에 title, summary가 지정되어 있다. 이 아이템을 선택하면 별도의 Preference 창 (dialog 기반)이 Popup한다.

 

 

<CheckBoxPreference>

key, title이 지정되어 있으며, summayOn과 summaryOff에는 각각 check상태의 도움말과 uncheck상태의 도움말이 설정 되어 있다.

 

 

 

 

3. XML + PreferenceActivity이용해 Preference 구현하기

 

작성된 Preference XML 파일을 화면에 표현 가능한 Preference로 만드는데 핵심적인 역할을 하는 것이 바로 PreferenceActivity이다. 전에 살펴본 적이 있는 ListActivity, TabActivity와 마찬가지로 Activity로 상속하며, Preference를 화면에 표현하고 운영하는데 특화된 클래스이다.

 

PreferenceActivity는 Preference 생성/운영에 도움이 되는 다음과 같은 편리한 기능이 구현되어 있다.

  • XML부터 Preference 계층 구조를 전달 받는 메서드 제공
  • 타 Activity의 Preference 계층 구조를 받아오는 메서드 제공
  • 제공된 Preference 계층 구조를 ListView 형태로 자동 구성/표현
  • 제공된 Preference 계층 구조에 따라 디바이스에 Preference 데이터 파일 자동 생성
  • 사용자가 변경한 사항들을 Preference 데이터 파일에 자동 저장

 

 

PreferenceActivity를 이용해 Preference를 구현하려면 크게 두 가지 단계만 거치면 되는데 다음과 같다. (물론 PreferenceActivity도 Activity 생명 주기를 따르기 때문에 onPause(), onResume() 메서드 같은 생명 주기 관련 callback의 구현도 신경 써야 하지만 여기서는 Preference를 구현하는 최소한의 기능만 설명한다)

  • PreferenceActivity를 상속하는 커스텀 클래스 정의.
  • onCreate()에 내부에서 Preference 계층 구조를 가져오는 메서드 호출.

 

위에서 두 번째 구현 순서로 설명된 Preference 계층 구조를 가져오는 메서드는 2개가 제공된다.

우선, \res\xml 밑의 Preference XML로부터 Preference 계층 구조를 얻어올 수 있는 메서드는 다음과 같다.

void addPreferencesFromResource(int)

Parameter:

  • int: Preference XML Resource. (예. R.xml.preferences)

 

두 번째로 타 Activity가 사용하는 Preference 계층 구조를 가져와 적용 시킬 수도 있는데, 이 때 사용되는 메서드는 다음과 같다.

(이 부분은 해결이 안 되는 중요한 버그가 있습니다. main preference까지는 잘 가져와 지는데 별도의 dialog를 띄우는 Preference 아이템을 클릭하면 WindowsManager가 BadTokenException을 발생합니다. 자세한 증상은 첨부된 예제 프로젝트에서 확인하시고, 혹시 해결 방법을 아시는 분은 꼭 댓글 부탁 드립니다)

void addPreferencesFromIntent(Intent)

Parameter:

  • Intent: Preference 계층 구조를 제공할 Activity를 지정하는 Intent

 

구현이 완료된 PreferenceActivity는 타 Activity로 부터 호출 되면 ListView 형태의 Preference 화면을 사용자에게 제공하고, 사용자가 정보를 수정하면 자동으로 Preference 데이터에 저장 하게 된다.

 

PreferenceActivity가 많은 편의 기능을 제공하지만 저장된 설정 복원은 개발자의 몫이다. 즉, 포함된 App이 다시 실행될 때, 기존에 저장된 Preferences설정 값을 Preference 데이터 파일로부터 읽어와 App에 적용 시키는 작업 (ex. App시작 시 Preference 파일에 저장된 폰트 색깔 등을 로드 해 App에 적용시키는)은 개발자가 직접 구현해 주어야 한다. 이를 위해 다음 단락에는 저장된 Preference 데이터 파일에 접근하여 저장된 정보를 가져오는 방법에 대해 알아본다.

 

 

 

 

4. Preference 데이터 파일로부터 환경 복원하기

 

안드로이드에서는 SharedPreferences 인터페이스를 이용해 저장된 Preferences 데이터 파일에 접근한다.

 

 

▌SharedPreferences 인터페이스 얻기▐

 

안드로이드 코드 내에서 SharedPreferences 인터페이스를 얻는 방법은 다음 나열된 세 개의 메서드를 통해서 가능하다. 



SharedPreferences Activity.getPreferences(int)

Parameter:

  • int: Preference 데이터 파일의 생성 시 접근 권한을 설정. Context클래스에 상수로 선언된 MODE_PRIVATE, MODE_WORLD_READABLE, MODE_WORLD_WRITABLE을 설정 가능. MODE_PRIVATE는 지금 App 만 접근 가능하도록 파일을 생성하며 (_rw_rw____), MODE_WORLD_READABLE/WRITABLE은 타 App에서도 파일을 읽고/쓸 수 있도록 생성한다 (_rw_rw_r__ 또는 _rw_rw__w_ 또는 |를 이용해 두 속성을 모두 지정하면 _rw_rw_rw_)

Return:

  • SharedPreferences: 메서드가 호출되는 Activity 클래스의 이름으로 저장된 Preference 데이터 파일의 SharedPreferences 인터페이스를 리턴. 같은 이름의 Preference 데이터 파일이 없을 경우 새로 생성

 

예를 들면 Activity를 상속하는 A, B라는 클래스 이름의 커스텀 Activity가 정의 되어 있다. A 내부에서 getPreference를 호출할 경우 디바이스의 Preference 저장 위치(data/data/패키지이름/shared_prefs/)에 A.xml 이라는 Preference 데이터 파일을 검색하고 파일이 존재 한다면 해당 파일에 대한 SharedPreferences 인터페이스를 리턴 하며, 파일이 존재하지 않을 경우에는 A.xml 파일을 생성한 후 생성한 파일에 대한 SharedPreferences 인터페이스를 리턴한다. 마찬가지로, B 내부에서 getPreference를 호출할 경우 디바이스에서 B.xml 파일을 검색 후 해당 파일에 대한 SharedPreferences 인터페이스를 리턴 한다 (없으면 파일 생성 후 인터페이스 리턴).


 

SharedPreferences Context.getSharedPreferences(String, int)

Parameter:

  • String: 사용(생성)할 Preference 데이터 파일의 이름을 지정
  • int: 생성될 Preference 데이터 파일의 생성 시 접근 권한을 설정

Return:

  • SharedPreferences: 첫 번째 인자로 전달되는 문자열과 일치하는 이름의 Preference 데이터 파일의 SharedPreferences 인터페이스를 리턴. 만약 같은 이름의 파일이 없다면 파일을 새로 생성하고 생성된 파일에 대한 SharedPreferences 인터페이스를 리턴

 

한가지 참고할 사항은 이 메서드를 통해 개발자가 생성/사용될 Preference 데이터 파일의 이름을 임의로 설정할 수 있다는 것인데, 예를 들어, A라는 Activity에서 getSharedPreferences("initial_setting", MODE_PRIVATE)라는 메서드를 실행한다면, preference framework는 /data/data/패키지/shared_pref/ 밑에서 initial_setting.xml이라는 파일을 검색하고 없다면 이를 새로 생성 후 initial_setting.xml에 대한 SharedPreferences 인스턴스를 리턴 하게 된다..

 

제일 처음에 설명한 Activity.getPreferences(int)도 실제로는 자기자신을 호출한 Activity(또는 컴포넌트)의 이름과 입력된 int 형 인자를 가지고 본 메서드를 호출하는 형태로 구현되어 있다.




Static SharedPreferences PreferenceManager.getDefaultSharedPreferences(Context)

Parameter:

  • Context: 사용하기 원하는 SharedPreferences 인터페이스를 포함하는 Context 지정

Return:

  • SharedPreferences: 인자로 지정된 Context (app 구동 환경)가 기본으로 생성하는 Preference 데이터 파일에 대한 SharedPreferences를 리턴

 

이 메서드는 App level의 기본 Preference 데이터 파일을 생성/사용하는데 사용된다. 예를 들어 com.holim.test.aaa 라는 패키지에 포함되는 A Activity에서 본 메소드를 호출 하면 Preference 프레임웍은 /data/data/패키지이름/shared_pref/com.holim.test.aaa_preferences.xml 이라는 Preference 데이터 파일을 생성한다. 전달된 Context 인자에 따라 패키지이름_preferences.xml 형태로 생성/사용할 Preference 데이터 파일의 이름이 시스템에 의해 자동으로 지정되게 되기 때문에, 호출되는 위치가 어디이든 같은 Context를 인자로 전달해 호출한 getDefaultSharedPreferences(…)메서드는 항상 같은 SharedPreferences 인스턴스를 리턴한다.

 

한가지 참고할 사항은 본 메서드는 static 메서드이기 때문에 메서드를 제공하는 PreferenceManager 클래스를 따로 생성할 필요 없이 다음과 같이 사용하면 된다.

SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);

 

 

이와 같이 여러 메서드를 사용해 얻어온 SharedPreferences 인터페이스로 무엇을 할 수 있는 지 알아 보자. 다음은 SharedPreferences 인터페이스가 제공하는 중요 메서드들이다.

 

 

▌SharedPreferences 인터페이스▐

 

SharedPreferences 인터페이스가 제공하는 기능은 다음과 같다.

  • 디바이스에 저장되어 있는 Preference 데이터 파일로부터 데이터를 추출하는 여러 메서드
  • Preference 데이터를 수정하는 방법을 제공하는 Editor 내부 인터페이스
  • Preference 데이터가 변경되었을 때 호출되는 callback 인터페이스

 

중요 메서드

SharedPreferences.Editor edit() – SharedPreferences가 연결된 Preference 데이터 파일을 수정 할 수 있게 여러 메서드를 제공하는 Editor 인터페이스 리턴. Editor 인터페이스는 자료 수정을 완료 한 후 Editor.commit() 메소드를 사용해야 파일에 변경내용이 저장됨.

void registerOnSharedpreferenceChangeListener(…) – Preference 변경 이벤트 Listener 등록

void unregisterOnSharedPreferenceChangeListener(…) – 위에 등록한 이벤트 Listener 등록 해지

Map <String, ?> getAll() – 모든 Preferences 값을 Map 자료형으로 리턴

boolean contains(String) – 인자로 제공되는 문자열과 같은 key의 Preference 항목이 있으면 true 리턴

타입 get타입(String, 타입) – Boolean, Float, Int, Long, String 형 메서드가 있으며 첫 인자는 데이터를 가져올 Key 값이며, 두 번째 인자는 찾는 preference 데이터가 존재하지 않을 때 리턴 할 값이다.

Boolean isMarried = sharedPref.getBoolean("결혼여부", false);

int fontSize = sharedPref.getInt("폰트사이즈", 20);

 

중요 인터페이스

  • SharedPreferences.Editor – Preference 파일에 저장된 항목을 수정/제거 할 수 있는 여러 메서드를 제공하는 인터페이스
  • SharedPreferences.OnSharedPreferenceChangeListener – Preference 데이터가 변경되었을 때 호출되는 callback 인터페이스

 

 

예제 프로젝트 중 MainActivity.java의 onCreate(…)와 onPause(…)메서드에서 보면 지금까지 설명한 SharedPreferences 인터페이스 얻기와 인터페이스가 제공하는 여러 메서드를 사용해 Preference 데이터 파일로부터 저장된 정보를 가져와 사용하는 것을 볼 수 있다.


MainActivity.onCreate()

소스 펼치기

 

MainActivity.onResume() 

소스 펼치기

 

 

 

 

참고. Preference XML 문서 작성시 사용되는 클래스

 

전 글들에서 다루었던 XML을 이용한 UI 및 메뉴 구성에서도 봤듯이

XML을 이용하면 디자인과 Logic을 분리 시킬 수 있어 간결한 코드의 구성이 가능할 뿐 아니라 현지화/유지보수 등 작업을 하는데 훨씬 편리 할 수 있음으로 권장되는 구현 방식 이라고 설명 한 적이 있다.

 

Preference에서도 마찬가지로 XML을 이용해 Preferences 구조를 쉽게 정의할 수 있다.

 

Preference XML 문서 작성시 android.preference 패키지 밑의 여러 클래스가 사용되는데 패키지 내부의 중요한 클래스의 상속 구조는 다음과 같다.

 

 

붉은 색 사각형 내부의 클래스가 Preferences 과 직접적으로 연관이 있는 클래스이며, 그 중 Preference 클래스를 비롯한 하위 클래스들이 XML Preference 문서 작성에 사용된다.

 

그럼 Preference 클래스부터 한번 살펴보자

  


 

▌Preference 클래스▐

여러 Preference 관련 클래스의 최상위 부모 클래스 이므로, 제공하는 XML 속성, 메서드는 자식 Preferences 관련 클래스에서도 모두 사용할 수 있다. XML 내부에서 직접적으로 사용되지는 않는다.

 

중요 XML 속성

  • android:key – Preferences 데이터 저장/불러올 때 사용되는 키(key) 지정
  • android:title – Preference 항목의 제목을 지정
  • android:summary – Preference 항목을 자세히 설명하는 문자열 지정
  • android:enabled – Preference 항목의 활성화 비활성화 여부 지정 (true/false)
  • android:selectable – Preference 항목의 선택 가능 여부 결정 (true/false)
  • android:order – Preference 항목이 표시되는 순서를 결정 (0-based 정수 사용: 낮은 값 먼저 보임). 순서가 명시적으로 지정되지 않는다면 XML에 정의된 순서대로 표시됨

 

중요 메서드

  • void setKey/Title/Summary(…) – Preference 항목의 키/제목/설명을 지정
  • void setEnabled(boolean) - Preference항목의 활성화/비활성화 여부 지정
  • void setSelectable(boolean) – Preference 항목의 선택 가능 여부 결정
  • void setLayoutResource(int) – Preference 항목에 표시할 View를 R 클래스로부터 지정
  • void setOnPreferenceChangedListener(…) – Preference 항목에 변화가 있으면 호출될 callback을 지정
  • void setOnPreferenceClickListener(…) – Preference 항목에 click 이벤트가 발생하면 호출될 callback을 지정

 

중요 인터페이스

  • Preference.OnPreferenceChangeListener - 사용자가 Preference를 변경하였을 때 호출되는 callback에 대한 인트페이스
  • Preference.OnPreferenceClickListener - 사용자가 Preference를 클릭하였을 때 호출되는 callback에 대한 인터페이스

 

  


▌PreferenceGroup 클래스▐

 

Preferences를 구성하는 객체들을 담을 수 있는 Container 클래스. PreferenceCategory와 PreferenceScreeen을 파생. XML 내부에서 직접적으로 사용되지는 않음.

 

중요 메서드

  • boolean addPreference(Preference) – 새로운 Preference 항목을 그룹에 추가. 추가 성공/실패 리턴.
  • Preference findPreference(String) – 인자로 전달되는 문자열과 같은 key값을 갖는 group 내 (자기자신포함) Preference 항목 리턴
  • Preference findPreference(int) – 인자(0-based정수)에 명시된 위치의 Preference 항목 리턴. 기본적으로 가장 처음 추가된 항목이 index 0
  • void removeAll() – 이 그룹 내 모든 Preference 항목을 삭제함
  • void removePreference(Preference) - 인자로 전달된Preference 항목을 삭제함
  • void setEnabled(boolean) – 그룹 전체의 활성화/비활성화 여부 지정
  • void setOrderingAsAdded(boolean) – true일 경우 그룹에 추가된 순서로 표현. false일 경우 Preference아이템 내 순서 정보가 있으면 그걸 따르고 순서 정보가 없으면 Preference 아이템의 title을 이용해 알파벳순 정렬.

  


 

▌PreferenceScreen 클래스▐

 

PreferenceGroup에서 파생하며, 실제로 Preference XML 문서 내부에 사용 되는 클래스이다. 여러 Preference 구성 요소를 담을 수 있는 root container역할을 하며 Activity에 표현될 Preference의 계층구조를 나타내는 클래스이다. PreferenceActivity에 전달되어 이 클래스의 인스턴스가 포함하는 것들을 화면에 표현한다.

 

이 클래스가 Preference XML 문서에서 사용될 때는 다음과 같이 두 가지 용도로 사용된다.

  • Preference XML 문서의 root 요소 사용 시 - 경우에는 PreferenceActivity에 전달되어 preference의 전체 구조를 전달하는 용도로 사용. 이 경우 자기 자신은 화면에 표현 안되고 child 요소를 포함하는 Container의 용도로 사용됨 (XML UI에서 Containter는 화면에 표현 안되고 것과 같은 의미)
  • Preference XML 문서에서root가 아닌 child 요소로 사용 시 - Preference 아이템 중 하나로 취급. Preference 아이템 중 하나로 화면에 표시되며, 클릭 시 별도의 preference 화면으로 이동

 

!!! 그림 (main pref. -> sub pref)

 


 

▌PreferenceCategory 클래스▐

 

Preferences를 구성하는 객체들을 특정 category별로 분류 할 수 있게 하는 클래스. 중요한 XML 속성이나 메서드 없음

!!! 그림 (category 분류)

 

  


▌DialogPreference 클래스▐

 

모든 dialog 기반 Preference 객체의 부모 클래스. Preference 화면에는 링크만 표시되고, 링크를 클릭했을 때 실제 Preference를 컨트롤 할 수 있는 Dialog가 popup한다.

!!! 다이얼로그 그림

 

중요 XML 속성

  • android:dialogIcon – Dialog의 Icon 지정
  • android:dialogTitle – Dialog의 제목 지정
  • android:dialogMessage – Dialog 내에 표시될 문자열 내용 지정
  • android:negativeButton – 부정 버튼에 표시될 Text설정 (취소, cancel 등)
  • android:positiveButton – 긍정 버튼에 표시될 Text 설정 (적용, OK 등)

 

중요 메서드

  • void setDialogIcon(Drawable) – Dialog의 Icon 지정
  • void setDialogTitle(…) – Dialog의 제목 지정. 문자열 직접 지정 또는 문자열 리소스 사용
  • void setDialogMessage(…) – Dialog 내에 표시될 문자열 내용 지정
  • void setPositiveButtonText(…) – 부정 버튼에 표시될 Text설정 (취소, cancel 등)
  • void setNegativeButtonText(…) – 긍정 버튼에 표시될 Text 설정 (적용, OK 등)

  


 

▌EditeTextPreference 클래스▐

 

DialogPreference로부터 상속하며, 사용자로부터 문자열을 입력 받아 저장 하기 위한 Preference 아이템을 구현한 클래스.

 

중요 메서드

  • EditText getEditText() – Dialog가 포함하는 EditBox인스턴스를 리턴
  • String getText() – SharedPreferences 객체로부터 저장된 문자열 리턴
  • void setText() – SharedPreferences에 문자열 저장

  


 

▌ListPreference 클래스▐

 

DialogPreference로부터 상속하며, Dialog기반의 ListView 위젯에 미리 제공되는 문자열들을 표현하고 사용자가 그 문자열 중 하나를 선택하여 설정 값을 지정할 수 있도록 구현한 클래스.

 

중요 XML 속성

  • android:entries – ListView의 각 raw에 표현될 문자열을 array 리소스를 통해 공급. 사용자(human)를 위한 정보.
  • android:entryValues – ListView의 특정 raw가 사용자에 의해 선택되었을 때 프로그램 내부적으로 처리 할 문자열 data를 array 리소스 형식으로 제공. 컴퓨터가 처리 하기 위한 정보. (ex. 남/여: 사람을 위한 정보 -> 0/1: 컴퓨터를 위한 정보)
  • android:defaultValue – 초기 선택 항목 지정. (0-based 정수)

 

중요 메서드

  • CharSequence[] getEntries() – ListView의 각 row에 표현될 문자열들을 리턴 (사용자 위한 정보)
  • CharSequence[] getEntryValues() – Entry(사람을 위한 정보)와 연계된 컴퓨터가 처리할 문자열들을 리턴
  • void setEntries(…) – ListView의 각 row에 표현할 문자열 지정. Array리소스 또는 CharSequence[] 전달
  • void setEntryValues(…) – 컴퓨터가 처리할 문자열 지정. Array또는 CharSequence[] 전달

  


 

▌CheckBoxPreference 클래스▐

 

CheckBox 기능의 Preference 아이템을 구현한 클래스. SharedPreferences에 boolean 값으로 정보 저장

 

중요 XML 속성

  • android:summayOn – CheckBox가 check상태일 때 사용자에게 보일 안내문
  • android:summaryOff – CheckBox가 uncheck 상태일 때 사용자에게 보일 안내문

 

중요 메서드

  • boolean isChecked() – 현재 check/uncheck 상태 리턴
  • void setChecked(boolean) – CheckBox를 program 상에서 check/uncheck 함
  • void setSummaryOn(…) – CheckBox가 check상태일 때 사용자에게 보일 안내문 설정. String 리소스나 CharSequence 인스턴스 전달
  • void setSummaryOff(…) – CheckBox가 uncheck상태일 때 사용자에게 보일 안내문 설정

  


 

▌RingtonPreference▐

 

사용자가 디바이스에서 제공하는 전화 벨 소리를 지정할 수 있게 구현된 클래스. Intent를 이용해 어떤 벨 소리 Picker를 화면에 표시할 지 결정함.

 

중요 XML 속성

  • android:ringtoneType – 어떤 종류의 벨 소리 종류를 선택 가능하게 할지 지정. ringtone, notification, alarm, all 중에 하나 또는 복수(| 사용)개 지정 가능
  • android:showDefault – Default 벨소리 항목 표시 여부 결정 (true/false)
  • android:showSilent – 무음(Silent) 항목 표시 여부 결정 (true/false)

 

중요 메서드

  • setRingtoneType(int type) – XML 속성 중 ringtoneType에 대응. RingtoneManager 클래스에 선언되어 있는 상수 TYPE_RINGTONE, TYPE_NOTIFICATION, TYPE_ALARM, TYPE_ALL 중 하나 또는 복수(|사용) 전달
  • setShowDefault(boolean) – XML showDefault 속성에 대응
  • setShowSlient(boolean) – XML showSilent 속성에 대응

  


 

 PreferenceManager 클래스 

 

Activity나 XML로부터 Preferences 계층구조의 생성을 돕는 helper 클래스이지만 이 클래스를 이용하여 직접 Preferences를 구성하기보다는 PreferenceActivity.addPreferenceFromResource(int) 또는 PreferenceActivity.addPreferenceFromIntent(Intent)를 사용하는 것이 일반적이다.

 

혹시, PreferenceActivity를 사용하지 않고 Activity를 이용해 직접 Preferences 화면을 구성해야 한다면 필요한 클래스이다.

 

중요 메서드

  • Preference findPreference(CharSequence) – 인자로 전달되는 Key값을 가지는 Preference 항목의 인스턴스를 가져옴
  • SharedPreferences getDefaultSharedPreferences(Context) – 제공되는 context가 기본으로 사용하는 Preference파일로부터 SharedPreferences인스턴스 생성해 리턴
  • SharedPreferences getSharedPreferences() – this객체가 사용하는 context와 연결될 Preference파일로부터 SharedPreferences 인스턴스 생성해 리턴
  • void setDefaultValues(Context, int resId, boolean) – 제공되는 context가 사용하는 SharedPreferences를 두 번째 인자로 제공되는 XML 리소스에 정의된 기본 속성값들로 설정 함. 세 번째 인자가 false 일 경우엔 이 메서드가 전에 실행된 적이 있다면 무시하고, true일 경우 전에 실행 여부화 상관없이 재 실행된다. 참고: 마지막 인자를 true로 지정 했다고 해서 이 메서드를 이용해 Preferences를 바로 초기화 할 수 있는 것은 아니고, SharedPrefernces.clear() 메서드를 사용해 Preferences 항목을 모두 삭제 후 이 메서드를 사용해 XML에 지정된 본래의 값으로 Preferences를 초기화 할 수 잇다.

 

 

* 대충 필요해보이는 부분만 알아볼수있게 해석

Using the Core API in Python

The Core API exposes the fundamental building blocks that make up Dropbox. It's based on HTTP and OAuth, so it should be very familiar to most developers. There are faster and more powerful ways to integrate with Dropbox, such as Drop-ins and the Sync API, but if you need low-level control, the Core API is the best choice.

Core API는 HTTP와 OAuth에 기반을 둔 좀 더 기본적인 접근이다. 개발자들에게 더 익숙할 것임.

If you want to follow along, first register a new app on the App Console. You'll need the app key to access the Core API. Then install the Python SDK and you'll be ready to go.

처음 시작이라면 App Console 에서 새로운 앱을 등록해야함. Core API를 사용하려면 App Key가 필요하다. 그리고 python sdk 설치


Authenticating your app

The Core API uses OAuth v2, but the Python SDK will take care of most of it so you don't have to start from scratch.

You'll need to provide your app key and secret to the new DropboxOAuth2FlowNoRedirect object.

OAuth v2 파이썬이 알아서 해줄듯? 그리고 app_key, app_secret 발급 받자. DropboxOAuth2FlowNoRedirect

# Include the Dropbox SDK
import dropbox

# Get your app key and secret from the Dropbox developer website
app_key = 'INSERT_APP_KEY'
app_secret = 'INSERT_APP_SECRET'

flow = dropbox.client.DropboxOAuth2FlowNoRedirect(app_key, app_secret)

Now we're all set to start the OAuth flow. The OAuth flow has two parts:

  1. Ask the user to authorize linking your app to their Dropbox account.
  2. Once authorized, exchange the received authorization code for an access token, which will be used for calling the Core API.

OAuth Flow는 두개의 부분

1. 사용자 계정에 내 앱을 연결하도록 인증요청

2. 인증되면, Core API호츨을 위해 사용되는 접근 Token을 위한 인증코드를 교환함 .(?)

start 메소드로 인증 URL을 생성하는 DropboxOAuth2FlownoRedirect 를 사용하여 시작한다.

We'll start by using the DropboxOAuth2FlowNoRedirect object to generate an authorization URL with the start method.

authorize_url = flow.start()

With the authorization URL in hand, we can now ask the user to authorize your app. To avoid the hassle of setting up a web server in this tutorial, we're just printing the URL and asking the user to press the Enter key to confirm that they've authorized your app. However, in real-world apps, you'll want to automatically send the user to the authorization URL and pass in a callback URL so that the user is seamlessly redirected back to your app after pressing a button.

# Have the user sign in and authorize this token
authorize_url = flow.start()
print '1. Go to: ' + authorize_url
print '2. Click "Allow" (you might have to log in first)'
print '3. Copy the authorization code.'
code = raw_input("Enter the authorization code here: ").strip()

Once the user has delivered the authorization code to our app, we can exchange that code for an access token via finish:

# This will fail if the user enters an invalid authorization code
access_token, user_id = flow.finish(code)

The access token is all you'll need to make API requests on behalf of this user, so you should store it away for safe-keeping (even though we don't for this tutorial). By storing the access token, you won't need to go through these steps again unless the user reinstalls your app or revokes access via the Dropbox website.

Now that the hard part is done, all we need to do to authorize our API calls is to to pass the access token to DropboxClient. To test that we have got access to the Core API, let's try calling account_info, which will return a dictionary with information about the user's linked account:

client = dropbox.client.DropboxClient(access_token)
print 'linked account: ', client.account_info()

If you've made it this far, you now have a simple app that uses the Core API to link to a Dropbox account and make an API call to retrieve account info. Next, we'll upload a file to Dropbox, get its metadata, and then download it back to our app.

Uploading files

Let's say we're building a text editing app and we want to use it to save your latest magnum opus to Dropbox. Let's browse the methods in the Python docs to see which one will do that for us. This page lists all the methods supported in the SDK. If you scroll down, you'll find put_file.

put_file takes a path pointing to where we want the file on our Dropbox, and then a file-like object or string to be uploaded there. For this example, let's upload a local copy of working-draft.txt:

f = open('working-draft.txt')
response = client.put_file('/magnum-opus.txt', f)
print "uploaded:", response

If all goes well, the data in your local working-draft.txt will now be in the root of your app folder (or Dropbox folder, depending on your app's access type). The variable response will be a dictionary containing the metadata of the newly uploaded file. It will look something like this:

{
    'bytes': 77,
    'icon': 'page_white_text',
    'is_dir': False,
    'mime_type': 'text/plain',
    'modified': 'Wed, 20 Jul 2011 22:04:50 +0000',
    'path': '/magnum-opus.txt',
    'rev': '362e2029684fe',
    'revision': 221922,
    'root': 'dropbox',
    'size': '77 bytes',
    'thumb_exists': False
}

Listing folders

If you peruse the various entries in the metadata above, you'll get a good sense for all info we can gather from the file. You can get this info for an entire folder by using the metadata call.

folder_metadata = client.metadata('/')
print "metadata:", folder_metadata

The returned dictionary will list out the files and folders in the path given. It looks something like this:

{'bytes': 0,
 'contents': [{'bytes': 0,
               'icon': 'folder',
               'is_dir': True,
               'modified': 'Thu, 25 Aug 2011 00:03:15 +0000',
               'path': '/Sample Folder',
               'rev': '803beb471',
               'revision': 8,
               'root': 'dropbox',
               'size': '0 bytes',
               'thumb_exists': False},
              {'bytes': 77,
               'icon': 'page_white_text',
               'is_dir': False,
               'mime_type': 'text/plain',
               'modified': 'Wed, 20 Jul 2011 22:04:50 +0000',
               'path': '/magnum-opus.txt',
               'rev': '362e2029684fe',
               'revision': 221922,
               'root': 'dropbox',
               'size': '77 bytes',
               'thumb_exists': False}],
 'hash': 'efdac89c4da886a9cece1927e6c22977',
 'icon': 'folder',
 'is_dir': True,
 'path': '/',
 'root': 'app_folder',
 'size': '0 bytes',
 'thumb_exists': False}

In the example above, the app folder root contains a directory named Sample Folder and the file we just uploaded named magnum-opus.txt. You'll also see other various but vital bits of information such as the exact location of the file (path), file sizes (bytes), last modified date (modified), and more. If you want to tell if something has changed in the directory, you'll want to store and compare thehash parameter. Similarly, a file's rev parameter tells you if the file has changed. It's useful to keep track of the status of your files, as we'll see in the following example.

Downloading files

Some time has passed and you're ready to start editing that magnum opus of yours again. We'll need the get_file_and_metadatamethod to download the file.

f, metadata = client.get_file_and_metadata('/magnum-opus.txt')
out = open('magnum-opus.txt', 'w')
out.write(f.read())
out.close()
print metadata

get_file_and_metadata, like other calls that return file data, returns an httplib.HTTPResponse that you should .read() from to get the full response.

In addition to the file, the method also returns the file's metadata at its current revision. Every time a change is made to the file, the revfield of the file's metadata changes as well. By saving the revision when you download the file, you'll be able to tell if that file has been updated by another computer or device and choose to download the newer revision of that file.

The complete code

For those keeping score at home, here's the full source to this guide. Make sure to create a magnum-opus.txt file to get it to work fully. Also remember to insert your app key, app secret, and access type.

# Include the Dropbox SDK
import dropbox

# Get your app key and secret from the Dropbox developer website
app_key = 'INSERT_APP_KEY'
app_secret = 'INSERT_APP_SECRET'

flow = dropbox.client.DropboxOAuth2FlowNoRedirect(app_key, app_secret)

# Have the user sign in and authorize this token
authorize_url = flow.start()
print '1. Go to: ' + authorize_url
print '2. Click "Allow" (you might have to log in first)'
print '3. Copy the authorization code.'
code = raw_input("Enter the authorization code here: ").strip()

# This will fail if the user enters an invalid authorization code
access_token, user_id = flow.finish(code)

client = dropbox.client.DropboxClient(access_token)
print 'linked account: ', client.account_info()

f = open('working-draft.txt')
response = client.put_file('/magnum-opus.txt', f)
print 'uploaded: ', response

folder_metadata = client.metadata('/')
print 'metadata: ', folder_metadata

f, metadata = client.get_file_and_metadata('/magnum-opus.txt')
out = open('magnum-opus.txt', 'w')
out.write(f.read())
out.close()
print metadata

Python SDK Documentation

You can also dive into the Python SDK documentation, or if you're feeling ambitious, browse the Python SDK source code. The source code is found in the folder labeled dropbox within the SDK package. The file you'll be interfacing the most with is client.py. You won't need to deal with rest.py unless you intend to implement your own calls, but it's responsible for actually making API requests and parsing the server's response.


'공부 > Python' 카테고리의 다른 글

SQLAlchemy Tutorial(한글) - 2  (0) 2013.08.14
SQLAlchemy Tutorial(한글) - 1  (4) 2013.08.14

Fade, 이미지(CCSprite)를 점점 흐려지게 혹은 점점 진해지게 만들어 주는 액션이다.

 

※ 미리 선행되어야 할 작업 : setOpacity( 0~255가능 ) 메소드를 통해 Opacity값을 지정해주어야 함


■ FadeIn   : 현재의 opacity부터 최대opacity 255까지 지정한 duration 시간동안 점점 어두워짐 

                   ex) CCFadeIn*     in = CCFadeIn::actionWithDuration(1);

■ FadeOut : 현재의 opacity부터 최저 opacity 0까지 지정한 duration 시간동안 점점 밝아짐

                   ex) CCFadeOut* out = CCFadeOut::actionWithDuration(2);

■ FadeTo   : 현재의 opacity부터 내가 지정한 opacity까지 밝거나 혹은 어두워짐
                   ex) CCFadeTo* fadeto = CCFadeTo::actionWithDuration(3150);

 

   나는 메뉴가 선택되었을 때 팝업이 뜨면서 그 뒷 배경들은 점점 어두워지는 느낌을 표현하고 싶었다. 

   즉, 팝업은 FadeOut으로 밝아지고- 배경은 FadeIn으로 어두워지는것!

   그런데 내 생각과 다르게 FadeIn 메소드는 점점 어두워지면서 아예 까만색으로 아랫 노드를 완전히 가려버리는 문제가 있었다. 나는 뒤에 있는 배경을 반투명하게 가리고 싶은 건데 말이지. 알고보니 특정 Opacity까지만 내려가도록 설정해주는 FadeTo 라는 기특한 메소드가 있다. 현재의 opcity값과 메소드에 파라매터로 주어지는 Opacity값 중 어느 쪽이 크고 작냐에 따라 혼자서 FadeIn/Out 양쪽을 다 소화해낸다. 0 → 150 으로 Opacity를 주니 뒷 배경을 적절하게 가려주어서 만족.

 

   아래는 소스코드.

   쇼 팝업/ 하이드 팝업 메소드를 만들어 주고, 터치이벤트에서 해당 함수들을 호출하도록 한다. 배경을 반쯤 어둡게 덮는 레이어(alphaLayer)는 어두워지고, 팝업(popupbox)은 밝아진다. 랄까 팝업은 최초 Opacitiy가 0이라 더 밝아질 것도 없어서 '점점 밝아지는' 느낌은 안 나는데, 그런게 필요하다면 Opacity값을 수정하면 되겠다.

 

[팝업 컨트롤]

void XXX::showPopupBox(int menu) 
{

    alphaLayer->setIsVisible(true);

    popupBox->setIsVisible(true);

    
     // init에서 0값을 주었다고 여기에서 opacity셋팅을 빼먹으면

     // 처음 한 번만 페이드효과가 먹고, 그 이후부터는 페이드없이 바로 나타나는 불상사 발생 

    alphaLayer->setOpacity(0);

    popupBox ->setOpacity(0);

      

    CCFadeTo*  fadeto = CCFadeTo::actionWithDuration(2150);

    CCFadeIn*   fadein = CCFadeIn::actionWithDuration(2);   

  

    alphaLayer->runAction(fadeto);

    popupBox ->runAction(fadein);

}


void XXX::hidePopupBox() 
{

    popupBox->setIsVisible(false);

    alphaLayer->setIsVisible(false);

    // 이론적으로 opacty를 여기에서 0으로 주면 감춰져서 보이지 않을 것으로 생각했는데 그렇게 안 된다.

    // 왜일까- 잘 모르겠지만 우선 show와 hide에 각각 setIsVisible설정

}

 

[팝업 Sprite생성]

void XXX::init()
{ 

    ////////// Pop-up

   //////////////////////////////////////////////////////////////////////////////////////////

    popupBox = new CCSprite();

    popupBox->init();

    popupBox->setPosition(ccp(0,0));

    

    CCSprite* bgBox = CCSprite::spriteWithFile("pop_box.png");

    bgBox->setScale(0.7);

    bgBox->setPosition(ccp((float)screenSize.width/2, (float)screenSize.height/2));

    popupBox->addChild(bgBox,  10);

    

    btnCancle = CCMenuItemImage::itemFromNormalImage("btn_cancle.png","btn_cancle.png",

                                                                                this,menu_selector(EbookHome::popupCancleCallback) );

    btnCancle->setPosition(ccp((float)screenSize.width/2+210, (float)screenSize.height/2+80));

    

    btnRestart = CCMenuItemImage::itemFromNormalImage("btn_start.png","btn_start.png",

                                                                                this,menu_selector(EbookHome::popupRestartCallback) );

    btnRestart->setPosition(ccp((float)screenSize.width/2-100, (float)screenSize.height/2-40));

    

    btnResume = CCMenuItemImage::itemFromNormalImage("btn_resume.png","btn_resume.png",

                                                                                 this,menu_selector(EbookHome::popupResumeCallback) );

    btnResume->setPosition(ccp((float)screenSize.width/2+100, (float)screenSize.height/2-40));

    

    CCMenu* popupBtns = CCMenu::menuWithItems(btnCanclebtnRestartbtnResumeNULL);

    popupBtns->setPosition(ccp(0,0));

    popupBox->setOpacity(0);

    popupBox->addChild(popupBtns, 11);

    this->addChild(popupBox10);

    

    alphaLayer = new CCLayerColor();

    alphaLayer->initWithColor(ccc4f(0,0,0,0));

    alphaLayer->setOpacity(0);

    this->addChild(alphaLayer9);

    

    hidePopupBox();

}


출처 : http://deng-i.net/category/%EA%B3%B5%EB%B6%80%ED%95%98%EB%8A%94%20%EA%B0%9C%EB%B0%9C%EC%9E%90/Cocos%202d-x%20

앞서 작성한 SQLAlchemy Tutorial – Part 1에서 이어지는 번역이다.

(여기서 뭔가 모자란 부분이나 틀린게 있으면 틀린게 맞으므로 언제든 지적해주시고, 애매한 표현은 원본 문서를 봐주시면 감사하겠습니다. 원본 문서는 SQLAlchemy Tutorial. 한글로 된 sqlalchemy 튜토리얼 있으면 알려주세요!)


리스트와 Scalars 반환하기

Query 객체의 all()one()first() 메소드는 즉시 SQL을 호출하고 non-iterator 값을 반환한다.all()은 리스트를 반환한다.

query = session.query(User).filter(User.name.like('%air')). order_by(User.id)
query.all()
# [<User('haruair', 'Edward Kim', '1234')>, <User('wendy','Wendy Williams', 'foobar')>]

first()는 첫째를 리밋으로 설정해 scalar로 가져온다.

query.first()
# <User('haruair', 'Edward Kim', '1234')>

one()은 모든 행을 참조해 식별자를 값으로 가지고 있지 않거나 여러 행이 동일한 값을 가지고 있는 경우 에러를 만든다.

from sqlalchemy.orm.exc import MultipleResultsFound
try:
    user = query.one()
except MultipleResultsFound, e:
    print e


from sqlalchemy.orm.exc import NoResultFound
try:
    user = query.filter(User.id == 99).one()
except NoResultFound, e:
    print e

문자로 된 SQL 사용하기

문자열을 Query와 함께 유연하게 쓸 수 있다. 대부분 메소드는 문자열을 수용한다. 예를 들면filter()와 order_by()에서 쓸 수 있다.

for user in session.query(User).\
            filter("id<224").\
            order_by("id").all():
    print user.name

연결된 파라미터에서는 콜론을 이용한, 더 세세한 문자열 기반의 SQL를 사용할 수 있다. 값을 사용할 때 param() 메소드를 이용한다.

session.query(User).filter("id<:value and name=:name").\
    params(value=1234, name='fred').order_by(User.id).one()

문자열 기반의 일반적인 쿼리를 사용하고 싶다면 from_statement()를 쓴다. 대신 컬럼들은 매퍼에서 선언된 것과 동일하게 써야한다.

session.query(User).from_statement(
                    "SELECT * FROM users WHERE name=:name").\
                    params(name='haruair').all()

또한 from_statement() 아래와 같은 문자열 SQL 방식으로도 쓸 수 있다.

session.query("id", "name", "thenumber12").\
        from_statement("SELECT id, name, 12 as "
                "thenumber12 FROM users WHERE name=:name").\
        params(name='haruair').all()

문자열 SQL의 장단점

Query로 생성해서 쓰는건 sqlalchemy의 이점인데 그렇게 쓰지 않으면 당연히 안좋아지는 부분이 있다. 직접 쓰면 특정하게 자기가 필요한 결과물을 쉽게 만들어낼 수 있겠지만 Query는 더이상 SQL구조에서 아무 의미 없어지고 새로운 문맥으로 접근할 수 있도록 변환하는 능력이 상실된다.

예를 들면 User 객체를 선택하고 name 컬럼으로 정렬하는데 name이란 문자열을 쓸 수 있다.

q = session.query(User.id, User.name)
q.order_by("name").all()

지금은 문제 없다. Query를 쓰기 전에 뭔가 멋진 방식을 사용해야 할 때가 있다. 예를 들면 아래처럼from_self() 같은 고급 메소드를 사용해, 사용자 이름의 길이가 다른 경우를 비교할 때가 있다.

from sqlalchemy import func
ua = aliased(User)
q = q.from_self(User.id, User.name, ua.name).\
    filter(User.name < ua.name).\
    filter(func.length(ua.name) != func.length(User.name))

Query는 서브쿼리에서 불러온 것처럼 나타나는데 User는 내부와 외부 양쪽에서 불러오게 된다. 이제Query에게 name으로 정렬하라고 명령하면 어느 name을 기준으로 정렬할지 코드로는 예측할 수 없게 된다. 이 경우에는 바깥과 상관없이 aliased된 User를 기준으로 정렬된다.

q.order_by("name").all()
# [(3, u'fred', u'haruair'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy'), (3, u'fred', u'wendy'), (4, u'haruair', u'wendy')]

User.name 또는 ua.name 같이 SQL 요소를 직접 쓰면 Query가 알 수 있을 만큼 충분한 정보를 제공하기 때문에 어떤 name을 기준으로 정렬해야할지 명확하게 판단하게 된다. 그래서 아래 두가지와 같은 차이를 볼 수 있다.

q.order_by(ua.name).all()
# [(3, u'fred', u'haruair'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy'), (3, u'fred', u'wendy'), (4, u'haruair', u'wendy')]

q.order_by(User.name).all()
# [(3, u'fred', u'wendy'), (3, u'fred', u'haruair'), (4, u'haruair', u'wendy'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy')]

숫자세기

Query는 count()라는 숫자를 세는 편리한 메소드를 포함한다.

session.query(User).filter(User.name.like('haru%')).count()

count()는 몇개의 행이 반환될지 알려준다. 위 코드로 생성되는 SQL을 살펴보면, SQLAlchemy는 항상 어떤 쿼리가 오더라도 거기서 행의 수를 센다. SELECT count(*) FROM table 하면 단순해지지만 최근 버전의 SQLAlchemy는 정확한 SQL로 명시적으로 판단할 수 있는 경우 추측해서 처리하지 않는다.

숫자를 세야 할 필요가 있는 경우에는 func.count()로 명시적으로 작성하면 된다.

from sqlalchemy import func
session.query(func.count(User.name), User.name).group_by(User.name).all()
# [(1, u'fred'), (1, u'haruair'), (1, u'mary'), (1, u'wendy')]

SELECT count(*) FROM table만 하고 싶으면

session.query(func.count('*')).select_from(User).scalar()

User의 primary key를 사용하면 select_from 없이 사용할 수 있다.

session.query(func.count(User.id)).scalar() 

관계(relationship) 만들기

이제 User와 관계된, 두번째 테이블을 만들 것이다. 계정당 여러개 이메일 주소를 저장할 수 있게 만들 것이다. users 테이블과 연결되는, 일대다 테이블이므로 테이블 이름을 addresses라고 정하고 전에 작성했던 것처럼 Declarative로 address 클래스를 작성한다.

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, backref

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))

    user = relationship("User", backref=backref('addresses', order_by=id))

    def __init__(self, email_address):
        self.email_address = email_address

    def __repr__(self):
        return "<Address('%s')>" % self.email_address

위 클래스는 ForeignKey를 어떻게 만드는지 보여준다. Column에 직접 넣은 지시자는 이 컬럼의 내용이 대상된 컬럼을 따르도록 만든다. 이 점이 관계 데이터베이스의 주요 특징 중 하나인데 풀과 같은 역할을 해, 연결되지 않은 테이블 사이를 잘 붙여준다. 위에서 작성한 ForeignKey는 addresses.user_id컬럼이 users.id 컬럼을 따르도록 만든다.

두번째 지시자인 relationship()은 ORM에게 Address 클래스 자체가 User 클래스에 연결되어 있다는 사실을 Address.user 속성을 이용해 알 수 있게 해준다. relationship()은 외래키 연결에서 두 테이블 사이에 Address.user로 다대일 관계임을 결정한다.

덧붙여 relationship()내에서 호출하는 backref()는 역으로 클래스를 이용할 수 있도록, 즉 Address객체에서 User를 참조할 수 있도록 User.addresses를 구현한다. 다대일 관계의 반대측은 항상 일대다의 관계이기 때문이다. 자세한건 기본 관계 패턴 문서를 참고.

Address.user와 User.addresses의 관계는 양방향 관계(bidirectional relationship) SQLAlchemy ORM의 주요 특징이다. Backref로 관계 연결하기 에서 backref에 대한 자세한 정보를 확인할 수 있다.

relationship()을 원격 클래스를 객체가 아닌 문자열로 연결하는 것에 대해 Declarative 시스템에서 사용하는 것으로 문제가 될 수 있지 않나 생각해볼 수 있다. 전부 맵핑이 완료된 경우, 이런 문자열은 파이썬 표현처럼 다뤄지며 실제 아규먼트를 처리하기 위해 사용된다. 위의 경우에선 User 클래스가 그렇다. 이런 이름들은 이것이 만들어지는 동안에만 허용되고 모든 클래스 이름은 기본적으로 선언될 때 사용이 가능해진다. (주. 클래스의 선언이 순차적으로 진행되기 때문에 클래스 선언 이전엔 에러가 나므로 이런 방식을 사용하는 것으로 보인다.)

아래는 동일하게 “addresses/user” 양방향 관계를 User 대신 Address로 선언한 모습이다.

class User(Base):
    # ...
    addresses = relationship("Address", order_by="Address.id", backref="user")

상세한 내용은 relationship()를 참고.

이건 알고 계시나요?

  • 대부분의 관계형 데이터베이스에선 외래키 제약이 primary key 컬럼이나 Unique 컬럼에만 가능하다.
  • 다중 컬럼 pirmary key에서의 외래키 제약은 스스로 다중 컬럼을 가지는데 이를 합성외래키(composite foreign key)라고 한다. 이 또한 이 컬럼의 서브셋을 레퍼런스로 가질 수 있다.
  • 외래키 컬럼은 연결된 컬럼이나 행의 변화에 자동으로 그들 스스로를 업데이트 한다. 이걸 CASCADE referential action이라고 하는데 관계형 데이터베이스에 내장된 함수다.
  • 외래키는 스스로의 테이블을 참고할 수 있다. 이걸 자기참조(self-referential) 외래키라고 한다.
  • 외래키에 대해 더 알고 싶다면 위키피디아 외래키 항목을 참고.

addresses 테이블을 데이터베이스에 생성해야 하므로 metadata로부터 새로운 CREATE를 발행한다. 이미 생성된 테이블은 생략하고 생성한다.

Base.metadata.create_all(engine)

관계된 객체 써먹기

이제 User를 만들면 빈 addresses 콜렉션이 나타난다. 딕셔너리나 set같은 다양한 컬랙션이 있는데 기본으로 컬랙션은 파이썬의 리스트다. (컬렉션 접근을 커스터마이징 하려면 이 문서 참고)

jack = User('jack', 'Jack Bean', 'sadfjklas')
jack.addresses # [] 빈 리스트를 반환

자유롭게 Address 객체를 User 객체에 넣을 수 있다. 그냥 리스트 사용법이랑 똑같다.

jack.addresses = [
                Address(email_address='jack@gmail.com'),
                Address(email_address='jack@yahoo.com')]

양방향 관계인 경우 자동으로 양쪽에서 접근할 수 있게 된다. 별도의 SQL 없이 양쪽에 on-change events로 동작한다.

jack.addresses[1]       # <Address(email_address='jack@yahoo.com')>
jack.addresses[1].user  # <User('jack', 'Jack Bean', 'sadfjklas')>user는 user = relationship("User", backref=backref('addresses', order_by=User.id))

데이터베이스에 저장해보자. User인 Jack Bean을 저장하면 두 Address도 알아서 cascading으로 저장된다.

session.add(jack)
session.commit()

Jack을 쿼리해서 다시 불러보자. 이렇게 Query하면 아직 주소들은 SQL을 호출하지 않은 상태다.

jack = session.query(User).\
filter_by(name='jack').one()
Jack        # <User('jack', 'Jack Bean', 'sadfjklas')>

하지만 addressses 컬랙션을 호출하는 순간 SQL이 만들어진다.

jack.addresses
# [<Address(email_address='jack@gmail.com')>, <Address(email_address='jack@yahoo.com')>]

이렇게 뒤늦게 SQL로 불러오는걸 게으른 불러오기 관계(lazy loading relationship)라고 한다. 이addresses는 이제 불러와 평범한 리스트처럼 동작한다. 이렇게 컬랙션을 불러오는 방법을 최적화하는 방법은 나중에 살펴본다.

Join과 함꼐 쿼리하기

두 테이블이 있는데 Query의 기능으로 양 테이블을 한방에 가져오는 방법을 살펴볼 것이다. SQL JOIN에 대해 join 하는 방법과 여러가지 좋은 설명이 위키피디아에 있으니 참고.

간단하게 User와 Address 두 테이블을 완전 조인하는 방법은 Query.filter()로 관계있는 두 컬럼이 동일한 경우를 찾으면 된다.

for u, a in session.query(User, Address).\
                    filter(User.id==Address.user_id).\
                    filter(Address.email_address=='jack@gmail.com').\
                    all():
    print u, a
# <User('jack', 'Jack Bean', 'sadfjklas')> <Address('jack@gmail.com')>

반면 진짜 SQL JOIN 문법을 쓰려면 Query.join()을 쓴다.

session.query(User).join(Address).\
        filter(Address.email_address=='jack@gmail.com').\
        all()
# [<User('jack', 'Jack Bean', 'sadfjklas')>]

Query.join()은 User와 Address 사이에 있는 하나의 외래키를 기준으로 join한다. 만약 외래키가 없거나 여러개라면 Query.join() 아래같은 방식을 써야한다.

query.join(Address, User.id==Address.user_id)   # 정확한 상태를 적어줌
query.join(User.addresses)                      # 명확한 관계 표기 (좌에서 우로)
query.join(Address, User.addresses)             # 동일, 명확하게 목표를 정해줌
query.join('addresses')                         # 동일, 문자열 이용

외부 join은 outerjoin()을 쓴다.

query.outerjoin(User.addresses)     # left outer join

join()이 궁금하면 문서를 참고하자. 어떤 SQL에서든 짱짱 중요한 기능이다.

별칭(aliases) 사용하기

여러 테이블을 쿼리하면 같은 테이블을 여러개 불러와야 할 떄가 있는데 그럴 때 동일 테이블명에 별칭(alias)를 지정해 다른 테이블과 문제를 이르키지 않도록 해야한다. Query는 별칭으로 된 녀석들도 잘 알아서 처리해준다. 아래 코드는 Address 엔티티를 두번 조인해서 한 행에 두 이메일 주소를 가져오도록 하는 예시다.

from sqlalchemy.orm import aliased
adalias1 = aliased(Address)
adalias2 = aliased(Address)
for username, email1, email2 in \
    session.query(User.name, adalias1.email_address, adalias2.email_address).\
    join(adalias1, User.addresses).\
    join(adalias2, User.addresses).\
    filter(adalias1.email_address=='jack@gmail.com').\
    filter(adalias2.email_address=='jack@yahoo.com'):
    print username, email1, email2
# jack jack@gmail.com jack@yahoo.com

서브쿼리 사용하기

Query는 서브쿼리 만들 때에도 유용하다. User 객체가 몇개의 Address를 가지고 있는지 알고 싶을 때 서브쿼리는 유용하다. SQL을 만드는 방식으로 생각하면 주소 목록의 수를 사용자 id를 기준으로 묶은 후(grouped by), User와 join하면 된다. 이 상황에선 LEFT OUTER JOIN이 사용자의 모든 주소를 가져오므로 적합하다. SQL의 예를 보자.

SELECT users.*, adr_count.address_count
FROM users
LEFT OUTER JOIN (
        SELECT user_id, count(*) AS address_count
        FROM addresses GROUP BY user_id
    ) AS adr_count
    ON users.id = adr_count.user_id

Query를 사용하면 명령문을 안에서 밖으로 빼내듯 쓸 수 있다. 명령문 접근자는 일반적인 Query를 통해 SQL 표현을 나타내는 명령문을 생성해 반환한다. 이건 select()를 쓰는 것과 비슷한데 자세한건SQL 표현 언어 튜토리얼 문서를 참고.

from sqlalchemy.sql import func
stmt = session.query(Address.user_id, func.count('*').label('address_count')).\
        group_by(Address.user_id).subquery()

func 키워드는 SQL 함수를 만들고 subquery() 메소드는 별칭을 이용해 다른 query에 포함할 수 있는 SELECT 명령문의 형태로 반환해준다. (query.statement.alias()를 줄인 것)

이렇게 만든 서브쿼리는 Table처럼 동작한다. 아래 코드를 잘 모르겠으면 튜토리얼 앞부분에서 Table을 어떻게 다뤘는지 살펴보면 도움이 된다. 여기서는 컬럼에 접근할 때 table.c.컬럼명으로 접근했던, 그 방법처럼 사용한다.

for u, count in session.query(User, stmt.c.address_count).\
    outerjoin(stmt, User.id==stmt.c.user_id).order_by(User.id):
    print u, count
# <User('wendy', 'Wendy Williams', 'foobar')> None
# <User('mary', 'Mary Contrary', 'xxg527')> None
# <User('fred', 'Fred Flinstone', 'blar')> None
# <User('haruair', 'Edward Kim', '1234')> None
# <User('jack', 'Jack Bean', 'sadfjklas')> 2

서브쿼리서 엔티티 선택하기

위에서는 서브쿼리서 컬럼을 가져와서 결과를 만들었다. 만약 서브쿼리가 엔티티를 선택하기 위한 맵이라면 aliased()로 매핑된 클래스를 서브쿼리로 활용할 수 있다.

stmt = session.query(Address).\
                filter(Address.email_address != 'jack@yahoo.com').\
                subquery()
adalias = aliased(Address, stmt)
for user, address in session.query(User, adalias).\
        join(adalias, User.addresses):
    print user, address
# <User('jack', 'Jack Bean', 'sadfjklas')> <Address('jack@gmail.com')>

EXISTS 사용하기

SQL에서 EXISTS 키워드는 불린 연산자로 조건에 맞는 행이 있으면 True를 반환한다. 이건 많은 시나리오에서 join을 위해 쓰는데, join에서 관계 테이블서 적합한 값이 없는 행을 처리하는데에도 유용하다.

외부 EXISTS는 이런 방식으로 할 수 있다.

from sqlalchemy.sql import exists
stmt = exists().where(Address.user_id==User.id)
for name, in session.query(User.name).filter(stmt):
    print name
# jack

Query의 기능 중 몇가지 연산자에서는 EXISTS를 자동으로 사용한다. 위 같은 경우는 User.addresses관계에 any()를 사용하면 가능하다.

for name, in ssession.query(User.name).\
        filter(User.addresses.any()):
    print name
# jack

any()는 특정 기준이 있어 제한적으로 매치해준다.

for name, in session.query(User.name).\
    filter(User.addresses.any(Address.email_address.like('%gmail%'))):
    print name
# jack

has()도 any()와 동일한 기능을 하는데 대신 다대일 관계에서 사용한다. (~연산자는 NOT이란 뜻이다.)

session.query(Address).\
    filter(~Address.user.has(User.name=='jack')).all()
# []

일반 관계 연산자

관계(relationship)에서 사용할 수 있는 모든 연산자인데 각각 API 문서에서 더 자세한 내용을 볼 수 있다.

__eq__() 다대일에서의 equals 비교

query.filter(Address.user == somuser)

__ne__() 다대일에서의 not equals 비교

query.filter(Address.user != someuser)

IS NULL 다대일 비교 (__eq__())

query.filter(Address.user == None)

contains() 일대다 컬렉션에서 사용

query.filter(User.addresses.contains(someaddress))

any() 컬렉션에서 사용

query.filter(User.addresses.any(Address.email_address == 'bar'))

# 키워드 아규먼트도 받음
query.filter(User.addresses.any(email_address='bar'))

has() 스칼라 레퍼런스서 사용 (뭐지 ㅜㅠ)

query.filter(Address.user.has(name='ed'))

Query.with_parent() 어던 관계서든 사용

session.query(Address).with_parent(someuser, 'addresses')

선행 로딩 (Eager Loading)

lazy loading의 반대 개념으로 User.addresses를 User 호출할 때 바로 불러오도록 하는 방법이다. eager loading으로 바로 불러오면 쿼리 호출의 수를 줄일 수 있다. SQLAlchemy는 자동화와 사용자정의 기준을 포함해 3가지 타입의 선행 로딩(eager loading)를 제공한다. 3가지 모두 query options로 제어하는데 Query에 불러올 때 Query.options() 메소드를 통해 쓸 수 있다.

서브쿼리 로딩

선행로딩하도록 User.addresses에 표기하는 방법이다. orm.subqueryload()를 이용해서 서브쿼리를 불러올 떄 한번에 연계해 불러오도록 처리한다. 기존의 서브쿼리는 재사용이 가능한 형태지만 이것는 바로 Query를 거쳐 선택되기 때문에 관계된 테이블을 선택하는 것과 상관없이 서브쿼리가 동작한다. 복잡해보이지만 아주 쉽게 쓸 수 있다.

from sqlalchemy.orm import subqueryload
jack = session.query(User).\
                options(subqueryload(User.addresses)).\
                filter_by(name='jack').one()
jack
# <User('jack', 'Jack Bean', 'sadfjklas')>
jack.addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]

연결된 로딩 (Joined Load)

또 다른 자동 선행로딩 함수로 orm.joinedload()가 있다. join할 때 사용할 수 있는 방법으로 관계된 객체나 컬렉션을 불러올 때 한번에 불러올 수 있다. (LEFT OUTER JOIN이 기본값) 앞서의 addresses를 동일한 방법으로 불러올 수 있다.

from sqlalchemy.orm import joinedload

jack = session.query(User).\
                options(joinedload(User.addresses)).\
                filter_by(name='jack').one()
jack
# <User('jack', 'Jack Bean', 'sadfjklas')>
jack.addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]

사실 OUTER JOIN 결과라면 두 행이 나타나야 하는데 여전히 User 하나만 얻을 수 있다. 이 이유는Query는 엔티티를 반환할 때 객체 유일성을 위해 “유일하게 하기(uniquing)” 전략을 취한다.

joinedload()는 오랜동안 써왔지만 subqueryload() 메소드가 더 새로운 형태의 선행로딩 형태다. 둘 다 한 행을 기준으로 관계된 객체를 가져오는 것은 동일하지만 subqueryload()는 적합한 관계 컬렉션을 가져오기에 적합하고 반면 joinedload()가 다대일 관계에 적합하다.

joinedload()는 join()의 대체재가 아니다.

joinedload()으로 join을 생성하면 익명으로 aliased되어 쿼리 결과에 영향을 미치지 않는다.Query.order_by()나 Query.filter() 호출로 이런 aliased된 테이블을 참조할 수 없기 때문에 사용자 공간에서는 Query.join()을 사용해야 한다. joinedload()은 단지 관계된 객체 또는 콜랙션의 최적화된 내역을 불러오기 위해 사용하는 용도이기 때문에 추가하거나 제거해도 실제 결과엔 영향을 미치지 않는다. 더 궁금하면 선행 로딩의 도를 참고.

명시적 Join + 선행로딩

세번째 스타일의 선행 로딩은 명시적 Join이 primary 행에 위치했을 때 추가적인 테이블에 관계된 객체나 컬렉션을 불러온다. 이 기능은 orm.contains_eager()를 통해 제공되는데 다대일 객체를 미리 불러와 동일 객체에 필터링 할 경우에 유용하게 사용된다. 아래는 Address행에 연관된 User 객체를 가져오는 코드인데 “jack”이란 이름의 User를 orm.contains_eager()를 사용해 user 컬럼을 Address.user속성으로 선행로딩한다.

from sqlalchemy.orm import contains_eager
jack_addresses = session.query(Address).\
                            join(Address.user).\
                            filter(User.name=='jack').\
                            options(contains_eager(Address.user)).\
                            all()
jack_addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]
jack_addresses[0].user
# <User('jack', 'Jack Bean', 'sadfjklas')>

기본적으로 어떻게 불러오는지 설정하는 다양한 방법 등 선행 로딩의 추가적인 정보는 관계 불러오기 테크닉 문서를 참고.

삭제하기

jack을 삭제해보자. 삭제하고나면 count는 남은 행이 없다고 표시한다.

session.delete(jack)
session.query(User).filter_by(name='jack').count()
# 0

여기까진 좋다. Address 객체는 어떤지 보자.

session.query(Address).filter(
    Address.email_address.in_(['jack@gmail.com','jack@yahoo.com'])
).count()
# 2

여전히 남아있다. SQL을 확인해보면 해당 Address의 user_id 컬럼은 모두 NULL로 되어 있지만 삭제되진 않았다. SQLAlchemy는 제거를 종속적으로(cascade) 하지 않는데 필요로 한다면 그렇게 할 수 있다.

삭제/삭제-외톨이 종속처리 설정하기

cascade 옵션을 변경하기 위해서는 User.addresses의 관계에서 행동을 변경시켜야 한다. SQLAlchemy는 새 속성을 추가하는 것과 관계를 맵핑하는 것은 언제나 허용되지만 이 경우에는 존재하는 관계를 제거하는게 필요하므로 맵핑을 완전히 새로 시작해야한다. 먼저 Session을 닫는다.

session.close()

그리고 새 declarative_base()를 사용한다.

Base = declarative_base()

다음으로 User 클래스를 선언하고 addresses 관계를 종속처리 설정과 함께 추가한다. (생성자는 대충 두자)

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    password = Column(String)

    addresses = relationship("Address", backref='user', cascade="all, delete, delete-orphan")

    def __repr__(self):
        return "<User('%s','%s','%s'>" % (self.name, self.fullname, self.password)

그리고 Address도 다시 생성한다. 이 경우에는 이미 User에서 관계를 생성했기 때문에Address.user는 따로 생성할 필요가 없다.

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))

    def __repr__(self):
        return "<Address('%s')>" % self.email_address

이제 Jack을 불러오고 삭제하면 Jack의 addresses 컬랙션은 Address에서 삭제된다.

# jack을 primary key로 불러옴
jack = session.query(User).get(5)
# 첫 Address를 삭제 (지연 로딩이 동작한다)
del jack.addresses[1]
# address는 하나만 남는다
session.query(Address).filter(
    Address.email_address.in_(['jack@gmail','jack@yahoo.com'])
).count()
# 1

Jack을 지우면 Jack과 남은 Address도 삭제된다.

session.delete(jack)
session.query(User).filter_by(name='jack').count()
# 0
session.query(Address).filter(
    Address.email_address.in_(['jack@gmail.com','jack@yahoo.com'])
).count()
# 0

종속처리(cascade)에 대해

종속처리에 대한 더 자세한 설정은 Cascades 문서를 참고. 종속처리는 함수적으로 관련된 데이터베이스가 자연스럽게 ON DELETE CASCADE될 수 있도록 통합할 수 있다. Using Passive Deletes 문서 참고

다대다 관계(Many To Many Relationship) 만들기

일종의 보너스 라운드로 다대다 관계를 만드는 방법을 살펴본다. 블로그와 같은걸 만들 때를 예로 들면 BlogPost와 그에 따른 Keyword를 조합해야 하는 경우가 있다.

평범한 다대다 관계를 위해, 맵핑되지 않은 Table 구조를 조합 테이블로 만들 수 있다.

from sqlalchemy import Table, Text
# 조합 테이블
post_keywords = Table('post_keywords', Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id')),
    Column('keyword_id', Integer, ForeignKey('keywords.id'))
)

위 코드는 맵핑된 클래스를 선언하는 것과는 약간 다르게 Table를 직접 선언했다. Table은 생성자 함수로 각각 개별의 Column 아규먼트를 쉼표(comma)로 구분한다. Column 객체는 클래스의 속성명을 가져오는 것과 달리 이름을 명시적으로 작성해준다.

다음은 BlogPost와 Keyword를 relationship()으로 post_keywords 테이블에 연결해 정의한다.

class BlogPost(Base):
    __tablename__ = 'posts'

    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    headline = Column(String(255), nullable=False)
    body = Column(Text)
    # 다대다 관계 : BlogPost <-> Keyword
    keywords = relationship('Keyword', secondary=post_keywords, backref='posts')

    def __init__(self, headline, body, author):
        self.author = author
        self.headline = headline
        self.body = body

    def __repr__(self):
        return "<BlogPost('%r', '%r', '%r')>" % (self.headline, self.body, self.author)

class Keyword(Base):
    __tablename__ = 'keywords'

    id = Column(Integer, primary_key=True)
    keyword = Column(String(50), nullable=False, unique=True)

    def __init__(self,keyword):
        self.keyword = keyword

위에서 BlogPost.keywords는 다대다 관계다. 다대다 관계를 정의하는 기능은 secondary 키워드로 연관 테이블인 Table객체를 참조한다. 이 테이블은 단순히 양측의 관계를 참고하는 형태며 만약 다른 컬럼이 있다면, 예를 들어 자체 primary key가 있거나 foreign key를 가진다면 연관 객체(association object) 라는 다른 형태의 사용패턴을 사용해야 한다. 연관 객체 문서 참고.

그리고 BlogPost 클래스는 author필드를 가진다. 그래서 다른 양방향 관계를 만들 것인데 단일 사용자가 엄청나게 많은 블로그 포스트를 가질 수 있다는 문제점을 처리해야한다. 다시 말해User.posts에 접근하면 모든 포스트를 불러올 것이 아니라 일부 필터된 결과만 가져와야 한다. 이런 경우를 위해 relationship()은 lazy='dynamic'을 지원하는데 속성을 불러오는 전략의 대안 중 하나다. 이것을 relationship()의 역방향으로 사용하려면 backref()를 사용하면 된다.

from sqlalchemy.orm import backref
# User에서의 관계를 "다이나믹" 로딩 처리
BlogPost.author = relationship(User, backref=backref('posts', lazy='dynamic'))

그리고 새 테이블을 생성한다.

Base.meta.create_all(engine)

사용 방법은 크게 다르지 않다.

wendy = session.query(User).\
                filter_by(name='wendy').\
                one()
post = BlogPost("Wendy's Blog Post", "This is a test", wendy)
session.add(post)

지금 키워드는 데이터베이스에 각각 유일하게 저장한다. 아직 뭔가 거창한걸 한건 아니고 그냥 생성할 뿐이다.

post.keywords.append(Keyword('wendy'))
post.keywords.append(Keyword('firstpost')) 

이제 키워드가 ‘firstpost’인 모든 글을 찾아볼 것이다. 여기서 any 연산자로 ‘firstpost’인 글을 찾는다.

session.query(BlogPost).\
        filter(BlogPost.keywords.any(keyword='firstpost')).\
        all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

만약 Wendy의 포스트만 보고 싶다면,

session.query(BlogPost).\
        filter(BlogPost.author=wendy).\
        filter(BlogPost.keywords.any(keyword='firstpost')).\
        all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

또는 Wendy가 소유하고 있는 posts 관계 즉 dyanmic 관계를 이용해 불러오는 방법은 아래와 같다.

wendy.posts.\
    filter(BlogPost.keywords.any(keyword='firstpost')).\
    all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

이후 읽어볼 만한 문서

(주. 아마 아래 문서 중 세션과 관계 문서를 먼저 옮길 것 같습니다.)


'공부 > Python' 카테고리의 다른 글

Dropbox API 사용하기 (with python)  (0) 2013.08.16
SQLAlchemy Tutorial(한글) - 1  (4) 2013.08.14

오라일리 책을 구입해두고 안보고 있다가 이제야 보니 다른 부분이 너무나도 많아서 문서 보면서 배우기로 급 선회했다. 한글 문서로 먼저 훑어보면 좋을텐데 검색 능력이 부족해서 찾질 못하겠더라. 문서 보면서 대충 날림 번역으로 남겨놨다. 하루면 페이지 다 따라해볼 수 있을 것 같았는데 딴짓하느라 하루에 다 완료를 못해서 파트를 쪼개기로. 주중에 짬짬이 나머지를 보기로 하고 일단 먼저 업로드!

(여기서 뭔가 모자란 부분이나 틀린게 있으면 틀린게 맞으므로 언제든 지적해주시고, 애매한 표현은 원본 문서를 봐주시면 감사하겠습니다. 원본 문서는 SQLAlchemy Tutorial. 한글로 된 sqlalchemy 튜토리얼 있으면 알려주세요!)


SQLAlchemy 객체 관계형 매퍼는 데이터베이스 테이블을 이용해 사용자가 정의한 파이썬 클래스의 메소드와 각각의 행을 나타내는 인스턴스로 표현된다. 객체와 각 연관된 행들의 모든 변경점들이 자동으로 동기되어 인스턴스에 반영되며, 그와 동시에 사용자가 정의한 클래스와 각 클래스 사이에 정의된 관계에 대해 쿼리할 수 있는 (Unit of work이라 하는)시스템을 포함하고 있다.

이 ORM에서 사용하는 SQLAlchemy 표현 언어는 ORM의 구성 방식과도 같다. SQL언어 튜토리얼에서는 직접적인 의견을 배제한 채 데이터베이스들의 초기에 어떻게 구성해 나가야 하는지에 대해 설명하는 반면 ORM은 고수준의, 추상적인 패턴의 사용 방식과 그에 따른 표현 언어를 사용하는 방법을 예로 보여준다.

사용 패턴과 각 표현 언어가 겹쳐지는 동안, 초기와 달리 공통적으로 나타나는 사항에 대해 표면적으로 접근한다. 먼저 사용자가 정의한 도메인 모델서부터 기본적인 저장 모델을 새로 갱신하는 것까지의 모든 과정을 일련의 구조와 데이터로 접근하게 해야한다. 또 다른 접근 방식으로는 문자로 된 스키마와 SQL 표현식이 나타내는 투시도로부터 명쾌하게 구성해, 각 개별적인 데이터베이스를 메시지로 사용할 수 있게 해야 한다.

가장 성공적인 어플리케이션은 각각 독자적인 객체 관계형 매퍼로 구성되야 한다. 특별한 상황에서는, 어플리케이션은 더 특정한 데이터베이스의 상호작용을 필요로 하고 따라서 더 직접적인 표현 언어를 사용할 수 있어야 한다.

(제 실력이 미천해 깔끔하게 번역이 안되네요. 공통된 부분에만 집중하고 각 데이터베이스의 특징을 몰개성화 하며 단순히 저장공간으로 치부하는 다른 ORM과 달리 SQLAlchemy는 각 데이터베이스의 특징도 잘 살려내 만든 ORM이다, 대충 이런 내용입니다. 원문 보세요. ㅠㅠ)


버전 확인하기

import sqlalchemy
print sqlalchemy.__version__

접속하기

이 예시는 메모리서만 사용하는 sqlite 데이터베이스를 사용. create_engine()을 이용해 접속.

from sqlalchemy import create_engine
engine = create_engine('mysql://root:password@localhost/dbname', echo=True)

echo는 로그를 위한 플래그. 파이썬 표준 logging 모듈 사용. 순수 SQL 코드를 보여준다.

engine은 선언만 해서 바로 연결되는게 아니라 첫 실행이 될 때 연결이 됨.

print engine.execute("select 1").scalar()

ORM을 사용할 때는 위처럼 engine을 직접 이용할 필요는 없다. 맨 처음 연결 할 때 작성하고 ORM 사용하면 됨.

매핑 선언

ORM에서는 처음에 데이터베이스 테이블을 써먹을 수 있게 설정한 다음 직접 정의한 클래스에 맵핑을 해야한다. sqlalchemy에서는 두가지가 동시에 이뤄지는데 Declarative 란걸 이용해 클래스를 생성하고 실제 디비 테이블에 연결을 한다.

from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

이러면 준비 끝. 이렇게 해두면 몇개고 매핑 클래스를 만들 수 있다. 매핑 클래스 내에서 디비의 컬럼을 나타내는 Column 클래스, 각 컬럼의 데이터타입을 나타내는 IntegerString 클래스를 불러와야한다.

from sqlalchemy import Column, Integer, String

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    password = Column(String)

    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

    def __repr__(self):
        return "<User('%s', '%s', '%s')>" % (self.name, self.fullname, self.password)

 User 클래스는 __tablename__에서 정의한 테이블에 맵핑되고 primary key인 id와 name, fullname, password 컬럼을 가진다.

메소드는 마음껏 만들어도 상관없다. 파이썬 기본 class와 똑같음. __init__와 __repr__도 만들어도 되고 안만들어도 된다. Base를 상속하지만 이는 단지 최소의 설정만 담당할 뿐이다.

Declarative system으로 만들어진 이 클래스는 table metadata를 가지게 되는데 이게 사용자정의 클래스와 테이블을 연결해주는 구실을 한다. 예전엔 이 metadata를 만들고 클래스에 맵핑해서 썼는데 그 방식을 Classical Mapping이라고 얘기한다. 그 예전 방식에서는 Table이라는 데이터 구조와 Mapper 객체로 클래스와 맵핑한다. (오라일리에서 나온 sqlalchemy 책에선 이 구방식으로 설명한다 ;ㅅ;)

metadata를 보고 싶다면,

User.__table__

mapper 클래스는,

User.__mapper__

Declarative 기반 클래스는 모든 Table 객체들을 MetaData로 정의해두고 .metadata 속성을 통해 접근할 수 있게 도와준다.

아직 위의 예제 클래스는 테이블이 생성이 되지 않은 상태인데 MetaData를 통해 손쉽게 생성할 수 있도록 도와준다. 테이블을 생성할 때 MetaData.create_all() 로 생성할 수 있는데 이 메소드를 호출하면 Engine으로 연결된 데이터베이스에 테이블을 생성해준다.

Base.metadata.create_all(engine)

최소 테이블 묘사 vs. 완전 상세돋는 묘사

sqlite나 postgresql은 테이블을 생성할 때 varchar 컬럼을 길이를 설정하지 않아도 별 문제 없이 데이터타입으로 쓸 수 있지만 그 외 데이터베이스에서는 허용되지 않는다. 그러므로 컬럼 길이가 필요한 데이터베이스의 경우 length가 필요하다.

Column(String(50))

IntegerNumeric 같은 경우에도 위와 동일하게 쓸 수 있다.

덧붙여 Firebird나 오라클에서는 PK를 생성할 때 sequence가 필요한데 Sequence 생성자를 써야 한다. (auto increment 로 생성되는 뭐 그런류 같은 의미인거같다.)

from sqlalchemy import Sequence
Column(Integer, Sequence('user_id_seq'), primary_key=True)

위에서의 User 클래스를 다시 작성해보면,

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
    name = Column(String(50))
    fullname = Column(String(50))
    password = Column(String(12))

    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

    def __repr__(self):
        return "<User('%s', '%s', '%s')>" % (self.name, self.fullname, self.password)

파이썬 안에서만 쓸꺼라면, 그리고 디비를 밖에서 이미 생성했다면 이전에 작성한 대로만 작성해도 상관 없다.

매핑된 클래스로 인스턴스 만들기

ed_user = User('haruair', 'Edward Kim', '1234')
ed_user.name        # 'haruair'
ed_user.password    # '1234'
str(ed_user.id)     # 'None'

id는 __init__()에서 정의되지 않았지만 맵핑을 해뒀기 때문에 None으로 존재한다. 기본적으로 ORM에서 생성된 클래스 속성들은 테이블에 맵핑된 것으로 표현된다. 이런 클래스 속성들은 descriptors로서 존재하는데 맵핑된 클래스를 위해 instrumentation을 정의해둔다. 이 instrumentaion은 이벤트를 바꾸거나 변경을 추적하거나 자동으로 새로운 데이터를 불러온다거나 할 때 도움을 주는 기능을 한다.

위의 값에서 ‘Edward Kim’을 디비에 넣기 전까진 id는 None이다. 디비에 넣으면 id값은 알아서 들어오게 된다.

세션 만들기

ORM은 데이터베이스를 session을 이용해 다룰 수 있는데 처음 앱을 작성할 때 create_engine()과 같은 레벨에서 Session 클래스를 factory 패턴으로 생성할 수 있다.

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)

모듈레벨에서 작성하고 있어서 Engine이 아직 존재하지 않는다면,

Session = sessionmaker()

이후에 engine을 생성하고 session의 configure를 이용한다.

Session.configure(bind=engine)

위처럼 작성한 Session 클래스는 새 객체를 만들어서 데이터베이스와 연결이 된다. 다른 트랜잭션을 위한 것들은 sessionmaker()에서 호출될 때 정의되야 하는데 자세한건 이후 챕터에서 알려준다고. 이제부터 언제든 데이터베이스와의 대화가 필요할 때 Session을 불러서 쓰면 된다.

session = Session()

위 Session은 Engine과 연결이 되어 있지만 아직 연결이 열린 상태는 아니다. 앞서와 같이 처음으로 사용될 때 Engine과 연결되고 모든 변경을 커밋하고 세션을 종료할 때까지 열려있게 된다.

세션 생성하는 패턴들

Session은 다양한 기반에 다양한 타입의 어플리케이션, 프래임워크에서 다양한 요구사항에서 짱짱 좋다. 그러니까 Session은 오브젝트와 일반적인 데이터베이스 접속에서 쓰면 된다. 어플리케이션 스레드를 저녁 만찬이라 생각하면, 세션은 손님의 접시이고 객체는 놓여질 음식이라 볼 수 있다. (디비는 주방쯤?) 세션을 어떻게 써야할지 고민한다면 다음 링크 참조.

새 객체 추가하기

ed_user= User('haruair', 'Edward Kim', '1234')
session.add(ed_user)

여기선 실제로 데이터베이스에 추가된게 아니라 pending인 상태다. 아직 데이터베이스에 발행되지는 않은 상태인데 입력이 필요한 순간에는 flush라는 과정을 통해 입력이 된다. 만약 디비에 쿼리를 하면 모든 pending 된 정보는 flush되고 접근 가능한 상태가 된다. (실제로 저장된 상태는 아님. 여전히 pending.)

예를 들면 아래 코드는 User 인스턴스를 로드해 새 Query 객체를 생성한다.

our_user = session.query(User).filter_by(name='haruair').first()
our_user     # <User('haruair', 'Edward Kim', 'secret')>

사실 Session은 내부적으로 맵구조의 객체라 반환하는 값이 우리가 기존에 집어넣은 인스턴스랑 동일하다.

ed_user is our_user     # True

ORM의 컨셉이 identity map이라서 session에서 하는 모든 처리들이 실제 데이터셋과 함께 동작한다. Session에서 PK를 가지면 PK 가진 같은 파이썬 객체를 반환한다. 그러니까 이미 있는 PK를 입력하면 에러가 난다.

add_all()로 한방에 추가할 수도 있다.

session.add_all([
    User('wendy', 'Wendy Williams', 'foobar'),
    User('mary', 'Mary Contrary', 'xxg527'),
    User('fred', 'Fred Flinstone', 'blar')])

비밀번호를 함 바꿔보자.

ed_user.password = 'test1234'

Session은 계속 연결되어있는 객체를 계속 주시하고 있다. 위처럼 수정하면 session은 이미 알고있다.

session.dirty        # IdentitySet([<User('Edward', 'Edward Kim', 'test1234')>])

새로 추가한 애들도 볼 수 있다.

session.new
# IdentitySet([<User('mary', 'Mary Contrary', 'xxg527')>,
#              <User('wendy', 'Wendy Williams', 'foobar')>,
#              <User('fred', 'Fred Flinstone', 'blar')>])

Session에 pending된 애들을 실행시키려면,

session.commit()

commit()은 모든 변경, 추가 이력을 반영한다. 이 트랜잭션이 모두 실행되면 세션은 다시 connection pool을 반환하고 물려있던 모든 객체들을 업데이트 한다. 실제 데이터베이스에 삽입함.

암튼, 앞서 id가 ‘None’ 이었던 녀석을 다시 보면,

ed_user.id    # 1

Session이 새 행을 데이터베이스에 입력한 이후에 새로 생성된 행들은 식별자들과 데이터베이스에서 기본으로 설정된 값들을 instance에서 사용할 수 있게 된다. 즉시 사용할 수 있거나 첫 액세스에 로딩될 때 모두 사용할 수 있다. 위 경우엔 commit()을 실행한 이후 새 트랜잭션이 실행되어 모든 행이 다시 로드된 상태다.

sqlalchemy에서는 기본적으로 이전 트랜잭션에서 새 트랜잭션으로 처음 실행될 때 모든 데이터를 새로 가져온다. 그래서 가장 최근의 상태를 바로 사용할 수 있다. 다시 불러오는 레벨을 설정하고 싶으면세션 사용하기 문서를 확인하자.

세션 객체의 상태들

User 객체가 Session 외부에서 PK 없이 Session 안에 들어가고 실제로 데이터베이스에 추가될 때 까지 각 “객체 상태” 를 가지고 있다. transient, pending, persistent 세가지. 이 상태들을 알고 있으면 도움이 되므로 객체 상태에 대한 설명을 잽싸게 읽어보자.

롤백하기

Session이 트랜잭션으로 동작하고 나서 우린 롤백 하는 것도 가능하다. 롤백해보기 위해 값을 변경해보자.

ed_user.name = 'edkim'

그리고 가짜 유저를 하나 생성한다.

fake_user = User('fakeuser', 'Invalid', '12345')
session.add(fake_user)

Session을 query하면 일단 flush된 현재의 트랜잭션을 확인할 수 있다.

session.query(User).filter(User.name.in_(['edkim', 'fakeuser'])).all()
#[<User('edkim', 'Edward Kim', 'test1234')>, <User('fakeuser', 'Invalid', '12345')>]

롤백을 실행하면 변경하기 전 상태로 돌아간다.

session.rollback()
ed_user.name            # 'haruair'
fake_user in session    # False

쿼리 보내기

Query 객체는 session에서 query() 메소드로 생성한다. 이 함수는 다양한 수의 아규먼트를 가질 수 있는데 다양한 클래스의 조합과 클래스 descriptor를 사용할 수 있다. 사실 Query는 User 인스턴스를 부를 때 이미 써봤다. iterative context를 evaluated할 때, User 객체 리스트를 반환한다.

for instance in session.query(User).order_by(User.id):
    print instance.name, instance.fullname

Query는 KeyedTuple 클래스 통해 튜플로 반환하는데 일반적인 파이썬 객체처럼 활용할 수 있다. 각 저장된 값들은 클래스 이름이나 속성 이름과 동일하다.

for row in session.query(User, User.name).all():
    print row.User, row.name

label()을 이용하면 컬럼 이름을 다르게 쓸 수 있다. 어떤 클래스 속성이든 매핑해서 쓸 수 있다.ColumnElement-derived object.

for row in session.query(User.name.label('name_label')).all():
    print row.name_label

컬럼은 위 방식으로 하지만 User 같은 클래스 엔티티는 aliased를 이용해 제어할 수 있다.

from sqlalchemy.orm import aliased
user_alias = aliased(User, name='user_alias')
for row in session.query(user_alias, user_alias.name).all():
    print row.user_alias

LIMIT이나 OFFSET을 포함한 기본적인 Query 동작은 order by와 함께 파이썬 배열에서 쪼개는(slice) 방식처럼 쓰면 된다.

for user in session.query(User).order_by(User.id)[1:3]:
    print user

결과물을 filter 할 때에는 filter_by()를 쓰면 된다.

for name in session.query(User.name).filter_by(fullname='Edward Kim'):
    print name

또는 filter()를 쓰면 되는데 좀더 유연한 SQL 표현을 쓸 수 있다. 매핑클래스에서 사용한 클래스 단위의 속성과 파이썬 표준 연산자를 쓸 수 있다.

for name in session.query(User.name).filter(User.fullname=='Edward Kim'):
    print name

Query 객체는 완전 생산적이라 대부분의 메소드 호출은 새 Query 객체를 반환한다. 따라서 아래와 같이 꼬리를 무는 체이닝 방식으로 사용이 가능하다. (Where … And … 식으로 된다.)

for name in session.query(User).\
            filter(User.name=='haruair').\
            filter(User.fullname=='Edward Kim'):
    print user

일반 필터(filter) 연산자들

equals

query.filter(User.name == 'ed')

not equals

query.filter(User.name != 'ed')

LIKE

query.filter(User.name.like('%ed%'))

IN

query.filter(User.name.in_(['ed', 'wendy', 'jack']))

서브쿼리식으로도 됨

query.filter(User.name.in_(session.query(User.name).filter(User.name.like('%ed%'))))

NOT IN

query.filter(~User.name.in_(['ed', 'wendy', 'jack']))

IS NULL

filter(User.name == None)

IS NOT NULL

filter(User.name != None)

AND

from sqlalchemy import and_
filter(and_(User.name == 'ed', User.fillname == 'Edward Kim'))

또는 위에서 본 체이닝 메소드로

filter(User.name == 'ed').filter(User.fullname == 'Edward Kim')

OR

from sqlalchemy import or_
filter(or_(User.name == 'ed', User.name == 'wendy'))

match

매치 파라미터는 백엔드에서 특정된다.. (무슨 말인지 잘;)

query.filter(User.name.match('wendy'))

출처 : http://haruair.com/blog/1682

'공부 > Python' 카테고리의 다른 글

Dropbox API 사용하기 (with python)  (0) 2013.08.16
SQLAlchemy Tutorial(한글) - 2  (0) 2013.08.14

apt-get install netatalk

nano /etc/netatalk/afpd.conf


add this line

- -tcp -noddp -uamlist uams_dhx.so,uams_dhx2_passwd.so -nosavepassword -setuplog "default log_info /var/log/afpd.log"


nano /etc/netatalk/AppleVolumes.default

~/                      "Home Directory"

=>

~/                      "$u@$h"

service netatalk restart

cocos2d-x 에서 레이어를 통해 게임 화면을 구성합니다.


이미지 출처 : http://www.learn-cocos2d.com


이런식으로 말이죠. 레이어를 풀스크린으로 쓰면 상관이 없지만 간혹 특정 크기에 맞춰야 할때가 있습니다. 화면을 분할하여 좌측, 우측 레이어를 구분해서 사용한다던가 할때 말이죠. cocos2d-x의 Layer 클래스는 상위 Node 클래스를 상속 받기 때문에 setContentSize 인터페이스를 사용할 수 있습니다.


하지만 이 인터페이스를 통해 크기를 조절한다 해도 실제 화면에서 보면 아무런 변화가 없습니다.


아래의 그림은 960 x 640 해상도에서 집과 캐릭터를 메인 레이어에 출력한 화면입니다. 이 메인 레이어 사이즈는 가로 해상도의 반인 480 x 640으로 설정해준 상태입니다(하늘 배경은 백그라운드 레이어 ).



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool CGameScene::InitLayer( void )
{
    bool bRet = false;
    do
    {
        // super init first
        CC_BREAK_IF( !CCScene::init() );
 
        // 배경 레이어
        CGameSceneBGLayer* pBGLayer = CGameSceneBGLayer::create();
        CC_BREAK_IF( !pBGLayer );
        this->addChild( pBGLayer, 0, CHILD_LAYER_BACKGROUND );
 
        // 메인 레이어
        CGameSceneMainayer* pMainLayer = CGameSceneMainLayer::create();
        CC_BREAK_IF( !pMainLayer );
        pMainLayer->setContentSize( CCSizeMake( 480, 640 ) );
        this->addChild( pMainLayer, 0.5, CHILD_LAYER_MAIN );
 
        bRet = true;
    } while (0);
 
    return bRet;
}

하지만 실제 화면은 960 x 640 풀 사이즈로 나오고 있습니다. 가로 사이즈 480으로 설정한대로라면 오른쪽에 있는 집, 대머리 캐릭터는 짤려야 정상인데 말이죠. 레이어가 설정해준 480 x 640의 크기로 출력되게 하려면 Layer 클래스를 상속 받아 visit 함수를 아래와 같이 재정의 해주어야합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CClippingLayer::visit()
{
    glPushMatrix();
    glEnable( GL_SCISSOR_TEST );
 
    CCSize contentSize = getContentSize();
    CCEGLView::sharedOpenGLView()->setScissorInPoints( getPosition().x, getPosition().y,
        contentSize.width, contentSize.height );
 
    CCNode::visit();
 
    glDisable( GL_SCISSOR_TEST );
    glPopMatrix();
}

이 후 메인레이어를 새로 정의한 CClippingLayer를 상속 받아 사용하면...



이제 원했던대로 메인 레이어가 반만 출력됩니다.



출처 : http://kindtis.tistory.com/489

필요 사항

OS : OSX 10.7 이상 Lion


시작 합니다. ~


1. http://developer.android.com 에서 Download 항목을 클릭 합니다.



2. NDK 항목을 선택하고, 오른쪽에서 Mac OS X 항목을 클릭 하여 자료를 다운받습니다.


3. 받은 자료를 적당한 곳에 압축을 풀어 둡니다.


4. 터미널을 열어서 자신의 홈디렉토리에 .bash_profile 이 있는지 확인합니다.

( ls -al 명령을 치면 리스트가 나옵니다. 저는 없네요^^)



5. 있든 없든 일단 vi .bash_profile 이라고 입력합니다.


6. 편집 화면이 나오는데 빈화면인 사람은 ~ 표시들만 있고 빈화면 일 테고, 파일이 존재하는 사람은 export PATH=${PATH}:블라블라:블라블라 라고 되어 있을 겁니다.

(기본 vi 입력 방법 - i키를 누르면 입력을 하실 수 있습니다. 입력을 다 하셨으면 esc 키를 눌러 입력을 끝을 냅니다. :wq 를 입력하면 (w:write , q:quit) 저장하고 밖으로 나가실 수 있습니다.)

6.1 화면에 아무것도 없다면, export 부터 끝까지 다 쳐줍니다. 

기본은 export PATH:${PATH}: 여기까지 입니다. 그 뒤는 아까 다운로드 받은 ndk의 주소입니다. 

아래는 제 NDK가 설치되어 있는 주소입니다.

6.2 화면에 export  가 있으신 분은 맨 뒤에 :/ndk 주소/를 넣어주시면 되겠습니다.


7. 이제 ls -al 명령으로 제대로 만들어졌는지 확인을 해봅시다. 저는 잘 생성되었네요~



8. 이제 적용을 해봅시다. source .bash_profile 을 입력하여 path가 적용되도록 합시다.



9. 이제 완성입니다. ndk-build 를 입력하여 어느 곳에서나 잘 동작하는지 확인해봅니다.


출처 : http://blog.naver.com/hello20/150136506664


XE 코어 백업과 복원하기

XE의 백업과 복원은 포장이사로...

XE 코어로 만든 웹사이트를 업그레이드하거나 다른 서버로 이사를 가야 할 때 백업과 복원은 필수입니다. 그런데 FTP 프로그램을 이용해 파일을 하나하나 내려받고 다시 서버에 올린다면 XE 코어는 정상적으로 작동하지 않습니다. 왜냐하면 여러분이 호스팅 서비스를 이용해 계정을 할당 받고 사용하게 되면 아래 그림과 같이 html이라는 사용자 폴더(root, 서버환경에 따라 public_html 또는 www라는 폴더를 사용)를 이용하게 되는데 이 폴더 안에는 여러분이 사용중인 폴더와 XE 코어가 만들어서 사용하는 files 디렉터리가 함께 있기 때문입니다. files 디렉터리는 xe 디렉터리 안에 있고 웹사이트의 운영내용 및 설정 파일들을 XE 코어가 수시로 점검하고 수정하고 첨부 파일들을 저장하는데 사용하고 있습니다. 이 files 디렉터리는 FTP를 이용해 다운로드 하거나 이동, 복사 할 수 없습니다.

웹사이트 운영시 함께 할당 받는 또 하나는 데이터베이스(DB)입니다. DB는 쉽게 말해 엑셀문서를 닮았다고 설명드린 적이 있습니다. 데이터 하나하나를 쪼개어 저장하는 아주 강력한 친구입니다. 하지만 무척 까탈스러운 친구라 말붙이기도 어렵고 접근하기 조차 쉽지 않습니다. 하지만 XE 코어를 도와 항상 열심히 일하는 아주 근면성실한 친구이기 때문에 믿음직스럽습니다.

image

웹사이트를 백업한다는 것은 보통 html 폴더(root) 전체를 하나의 파일로 묶는 것을 말하는데 반드시 잊지말아야 하는 것은 DB에 저장되어 있는 자료 역시 압축파일로 받아 두어야 합니다. 만약 DB를 백업 받아두고 다시 복원해 주지 않으면 XE 코어는 정상적으로 작동하지 않습니다. 따라서 계정의 백업은 디렉터리의 백업과 DB 백업으로 나누어 작업하게 됩니다. XE 코어와 DB의 상호작용은 관리자 페이지에 있는 캐시파일 재생성 버튼으로 연동을 유지하게 됩니다. 이렇게 XE 코어를 백업 받거나 복원하기 위해서는 반드시 포장을 잘 해 두어야하며 또는 부득이하게 다른 서버 계정으로 이사를 가야한다면 반드시 포장이사를 맡겨야 합니다...^^ (필요에 따라서는 XE 디렉터리만 포장해도 상관 없으며 index.html 문서는 별도로 작성하고 필요한 파일만 가져가도 됩니다.)


1. 파일 및 디렉터리의 백업과 복원

계정의 백업 작업은 FTP를 이용해서 할 수 없습니다. 오직 SSH 또는 Putty 프로그램을 이용해 서버에 접속하고 텔넷 명령어를 사용해서 백업 및 복원 명령어를 입력해야 합니다. SSH를 이용해 서버에 접속하게 되면 최상위 디렉터리 바깥에서 디렉터리 전체를 하나의 파일로 묶어주는 명령어를 입력합니다. 최상위 디렉터리(root)는 서버 환경에 따라서 html, www 또는 public_html로 사용할 수 있습니다. ls 명령어를 입력해 보고 html, www, public_html 디렉터리만 보인다면 루트 디렉터리 바깥에 있는 것이 맞습니다.

tar -cvfpz backuphtml.tar.gz html

html은 압축하고자 하는 디렉터리의 이름이고 backuphtml.tar.gz는 백업 작업의 결과로 만들어질 압축파일 이름입니다. tar의 옵션의 c는 파일 및 디렉터리를 하나로 묶어 새 저장 파일을 만들라는 뜻입니다.

이렇게 만들어진 backuphtml.tar.gz 압축파일은 루트 디렉터리 바깥에 있고 서버에 그대로 두어도 상관은 없지만(서버용량이 가능하다면) 추후에 작업을 위해서는 FTP 프로그램을 이용해 다운로드 받아 두는 것이 좋습니다. 그리고 압축파일 네이밍을 할 때 백업일자를 같이 써주면 언제 백업 받아둔 것이지 쉽게 확인할 수도 있겠지요. 예) backuphtml_20110630.tar.gz

디렉터리의 백업파일을 복원하는 것은 마찬가지로 루트 디렉터리 바깥에서 아래의 명령어를 입력하는 것으로 쉽게 작업하실 수 있습니다. 이번에는 옵션에 c가 아니라 압축을 해제하는 옵션 x를 주게 됩니다. 만약 다른 계정에서 해제하려면 FTP를 이용해 백업 받은 압축 파일을 미리 업로드 해 두어야 합니다.

tar -xvfpz backuphtml.tar.gz

압축이 풀리게 되면 자동으로 html 디렉터리 안에 모든 파일이 원상태로 복구됩니다. 만약 다른 계정에서 html 디렉터리가 없다면 html 디렉터리를 만들고 압축된 파일을 그 안에 풀어 놓게 되는데 이때 html 디렉터리 안의 모든 내용을 새로운 계정 환경의 루트(root) 디렉터리 안으로 옮겨 주어야 합니다.(※ 아래 "다른 서버 계정으로 이사가기" 참고) 옵션 -p는 모든 퍼미션(권한) 정보를 포함하여 압축을 하기도 하고 해제하기도 하지만 만약을 위해 chmod 707 xe 명령을 실행하여 xe 디렉터리의 권한설정을 다시한번 실행해 주어도 좋습니다.

chmod 707 xe

TIP - 간혹 xe 디렉터리 안에 xe가 만들고 사용하는 files 디렉터리의 권한 문제로 오류가 발생하기도 합니다.
이런 경우 chmod -R 707 xe/files 명령어로 files 디렉터리를 포함하여 하위 폴더까지 권한을 재설정해 줍니다. 기타 오류에 대해서는 이용중인 호스팅 웹서버의 root 권한이 필요한 경우가 있습니다. 이런 경우 서비스 제공 회사와 상의하는 것이 바람직합니다.


2. 데이터베이스(DB)의 백업과 복원

DB의 백업은 파일을 압축하는 방법이 아니라 DB에서 사용할 수 있는 sql 문서를 한장 만들어 받아 두는 것입니다. DB는 까탈스러운 친구라고 했죠? 자료를 좀 백업해 달라고 요청을 하면 달랑 서류 한장만 넘겨 줍니다...^^ 그런데 이것을 압축 파일이라고도 부르는 이유는 모든 내용을 텍스트로만 작성하기 때문에 압축한다는 의미로 표현하는 것입니다. CD를 굽는다고 표현하는 것과 같습니다. 이 문서는 나중에 복원을 할때도 DB에게 보여주기만 하면 된답니다. DB를 백업하는 명령어는 아래와 같습니다.

mysqldump -u 아이디 -p 디비네임 > backupdb.sql

DB의 본래 이름은 데이터베이스 관리 시스템(Database Management System, DBMS)인데 이 친구가 쓰는 말은 SQL(Structured Query Language, 구조화 질의어)이라는 언어를 씁니다. 좀 유별납니다...^^ 그래서 정중하게 mysql님 DB를 좀 출력(dump)해 주시죠!(dump is a Unix program used to backup file systems.) 라고 해야 합니다. 명령어가 아닌 정중한 부탁을 해야 합니다. 그러면 backupdb.sql 문서를 내놓습니다. 이 문서 안에는 XE 코어에서 사용하는 테이블의 종류와 갯수 및 내용(스키마), 그리고 그동안 누가 로그인해서 어떤 글들을 썼는지, 그리고 어떤 첨부파일이 어느 디렉터리에 보관되고 있었고, 레이아웃은 어떤 것을 자주 쓰는지, 메뉴는 어떤 것들이 있는지 하는 아주 소소한 것들까지 적어 놓은 가계부와 같습니다.

DB를 복원하려면 위에서 받아둔 sql 문서를 다시 DB에게 보여주기만 하면 됩니다. 이때는 화살표를 반대로 꺽어주면 되죠!

mysql -u 아이디 -p 디비네임 < backupdb.sql

출력(dump) 해 달라는 부탁은 할 필요없습니다. 화살표만 mysql 쪽으로 꺽어서 sql 문서를 보여주기만 하면 됩니다. -p 다음에 비밀번호가 없는 것은 나중에 password: 라고 입력을 기다리기 때문에 그때 입력하면 됩니다. 아이디와 디비네임은 여러분의 계정 아이디와 DB의 네임을 입력하시면 됩니다.(서버에 따라 계정의 아이디와 DB네임, 비밀번호가 다를 수 있습니다.)

TIP - 아이디와 옵션 -u는 붙여 쓰기도 합니다. 즉 옵션 -u 다음의 문자열은 DB의 아이디로 인식합니다.


캐시파일 재생성 하기

루트 디렉터리와 그 안의 모든 파일들을 백업하고 DB 역시 백업한 후에 다시 복원 작업을 거치게 되면 반드시 XE 관리자로 로그인 한 후에 캐시파일을 재생성 해 주어야 합니다. 만약 관리자로 로그인이 되지 않는 경우, 하얀 백지로 웹사이트가 표시 된다면 xe/files/cache 디렉터리를 삭제(rm -rf cache)한 후에 아래 관리자 주소를 웹브라우저 주소 입력칸에 직접 입력하여 관리자로 로그인 합니다. /xe/는 코어 설치폴더 이름입니다.

  • http://웹사이트 주소/xe/?module=admin
  • http://웹사이트 주소/xe/?module=admin&act=dispAdminConfig

관리자 로그인 후 캐시파일을 재생성하게 되면 XE 코어가 정상적으로 작동하게 됩니다.

TIP - 텔넷 명령어 rm -rf cache 로도 xe/files/cache 디렉터리가 삭제되지 않으면 nobody 권한문제 때문입니다. 호스팅 회사에 문의하여 삭제를 요청하는 것이 좋습니다. 간혹 php 문서를 활용한 권한수정도 통하지 않는 경우가 있습니다. 이런 경우 SuperUser 권한으로 nobody권한을 다시 조정해 줘야 합니다.


다른 서버 계정으로 이사가기

위의 과정은 같은 서버의 계정에서 필요에 따라 계정을 백업하거나 복원할 때 사용하는 방법입니다. 그럼 다른 서버의 계정으로 이사를 가야 한다면 어떻게 할까요?

image

백업된 디렉터리 압축파일과 DB에게서 받아 두었던 sql 문서를 복원하는 방법은 위와 동일합니다. 다만, 새로운 서버의 설정값들이 변경되기 때문에 이에 따른 수정할 부분이 추가됩니다. 우선 최상위 디렉터리(root)의 이름이 html이 아니라고 한다면 FTP로 업로드한 후 압축을 풀었을 때 html 디렉터리 안에 모든 파일과 폴더가 풀어져 있습니다. 이것을 새로 이사 간 서버의 루트 디렉터리(public_html 또는 www) 안으로 옮겨 주어야 합니다. 이때 사용하는 명령어는 아래와 같습니다.

mv html/* public_html

html 안의 모든(*) 파일과 폴더를 public_html 디렉터리 안으로 이동하라!(move) 는 뜻입니다. 완료가 되면 FTP를 이용해서 xe 디렉터리 안에 .htaccess 파일이 제대로 있는지도 확인해 보시고 재 확인차 chmod 707 xe 명령을 이용해 권한설정을 한번 더 확인해 줍니다.

TIP 1 - 리눅스 명령어 mv 에서 와일드카드(*)를 사용하면 도트(.)로 시작되는 파일이름(숨김파일)을 포함하도록 확장되지 않습니다. 이런경우 [mv 디렉터리/* 이동할 디렉터리] 와 [mv 디렉터리/.htaccess 이동할 디렉터리] 이렇게 2번 나누어서 실행하거나 또는 [mv 디렉터리/{*,.htaccess} 이동할 디렉터리/]처럼 여러 대상을 포함시켜 이동할 수 있습니다. 아래 예제와 같이 명령어를 실행하면 .htaccess 파일도 함께 이동할 수 있습니다.
예제) mv html/{*,.htaccess} public_html/
또는 mv html/{*,.*} public_html/ 도 같은 역할이지만 서버환경 옵션설정에 따라 허용되지 않을 수 있습니다.
숨김파일은 ls -a 옵션을 사용하여 확인할 수 있습니다.

TIP 2 - 숨김파일까지 한꺼번에 이동하려면 shopt -s dotglob 명령어를 우선 실행한후 mv 명령어를 이용해 와일드카드(*)를 사용하면 한번에 이동이 가능합니다.
shopt -s dotglob
mv html/* public_html/

그리고 다른 서버로 이사를 간 경우에는 XE 코어가 이전 서버에서 사용했던 xe/files/cache 디렉터리가 더이상 필요없습니다. 새로운 설정값을 다시 만들어야 하기 때문에 rm -rf cache 명령을 이용해 cache 디렉터리를 완전히 삭제해야 합니다.

★중요★
새로운 서버의 아이디와 DB네임으로 DB를 복원한 후에는 반드시 /xe/files/config/db.config.php 문서를 FTP를 이용해 서버에서 내려받고 그 안에 적힌 이전 서버의 내용을 새로운 서버의 아이디와 비밀번호, DB네임, 사이트 주소 등을 수정하여 XE 코어에게 이곳은 새로운 서버라는 것을 알려 주세요. 만약 아래 내용처럼 수정을 하여 업로드 한 후에도 문제가 발생하게 되면 설정 중에 localhost 등과 같이 기타 서버에서 사용하는 설정 방법을 호스팅사에 문의하여 수정해야 합니다.

db.config.php 파일 수정 :

<?php if(!defined("__ZBXE__")) exit();
$db_info->master_db = array('db_type' => 'mysql','db_port' => '3306','db_hostname' => 'localhost','db_userid' => 'DB아이디','db_password' => 'DB비밀번호','db_database' => 'DB이름','db_table_prefix' => 'xe_');
$db_info->slave_db = array(array('db_type' => 'mysql','db_port' => '3306','db_hostname' => 'localhost','db_userid' => 'DB아이디','db_password' => 'DB비밀번호','db_database' => 'DB이름','db_table_prefix' => 'xe_'));
$db_info->default_url = 'http://홈페이지 URL/xe/';
$db_info->lang_type = 'ko';
$db_info->use_rewrite = 'Y';
$db_info->time_zone = '+0900';
?>
  • 'db_hostname' => 'localhost'
  • 'db_userid' => 'DB아이디'
  • 'db_password' => 'DB비밀번호'
  • 'db_database' => 'DB이름'

db.config.php 파일을 수정한 후 다시 업로드하여 원본 파일을 덮어씌운 후에 관리자로 로그인하게 되면 반드시 캐시파일을 재생성하여 변경된 서버 계정의 환경 설정값들을 새로운 캐시파일로 생성하도록 하고 XE 코어와 DB가 연동하게 되면 XE 포장이사는 무사히 마치게 됩니다. 만약 /xe/files/cache 디렉터리가 rm -rf 명령어로도 삭제되지 않는다면 호스팅 회사에 문의하여 삭제를 요청하시면 곧바로 삭제를 해 줍니다.


포장이사 도움말

XE 코어의 백업과 복원, 서버 계정의 이전은 그리 쉬운 작업은 분명히 아닙니다. 왜냐하면 같은 계정 안에서의 백업 및 복원작업은 상대적으로 문제가 적은 반면에 다른 서버의 계정으로 이사를 가는 것은, 이사라는 일이 늘 그렇듯 그릇이 깨지기도 하고 가구에 스크레치가 나기도 하는 등 새로운 문제점들이 늘 발생할 수 있습니다. 새로운 서버의 설정과 운영에 따라 문제가 발생된 경우에는 호스팅사의 도움을 요청하는 것이 바람직합니다.

★ 무작정 XE를 백업 받고 새로운 서버로 이사를 가기보다는 호스팅 회사에 문의를 하여 백업 받은 XE 코어를 복원할 수 있는지에 대한 여부와 환경 설정에 필요한 자문을 구하는 것이 바람직합니다. 일반적인 경우 호스팅 서비스 회사는 이에 대한 안내를 자세히 해주며 백업파일이 있는 경우 호스팅 회사가 무료로 직접 압축파일을 해제하고 복원 해 주기도 합니다.

일반적인 문제발생의 원인은 이전 서버에서 파일 및 폴더을 압축할 때 nobody 권한에 따른 설정들이 새로운 서버에서 제대로 적용되지 않기 때문에 작은 문제들이 발생 되곤 한답니다.(호스팅 회사의 안내) 따라서 XE 코어 운영에 최적화된 좋은 호스팅 서비스 회사를 선택하고 XE 코어의 백업 파일을 제대로 포장한 후에 서버 이전을 진행하는 것이 가장 확실한 방법임을 추천합니다.
(※ 위와 같은 기본적인 텔넷 명령어들이 받아들여지지 않는 서버 계정은 추천하지 않습니다.)



출처 : http://www.xeschool.com/xe/step1_52

요즘 스마트폰의 데이터 쓸수 있는 양이 많아져서 효용이 있을지 모르겠지만..

한번 정리하는 마음으로 기록 해 봅니다.

"Connectify Hotspot"이란 유틸리티도 있습니다만.... 상용이라서...

 

노트북의 유선은 연결 되어 있고, 무선 랜카드가 있는 상태에서 스마트폰으로 인터넷을 3G가 아닌

노트북의 무선랜 -> 유선 -> 인터넷을 사용 하는 방법이 되겠네요..

 

 

준비물: 유선으로 인터넷이 연결된 노트북, 무선 랜카드(호스트된 네트워크 지원 되어야 함), 스마트폰

 

관리자 권한으로 cmd 창을 열어놓고 다음 붉은색 배경의 명령어는 cmd 창에서 입력 합니다. 

 

1. 무선 네트웍 카드가 무선 AP로 만들 수 있는지 확인 

netsh wlan show drivers

  => 호스트된 네트워크 지원  : 예


2. 무선 AP로 만들기: SSID, Key는 임의 값 입력

netsh wlan set hostednetwork mode=allow ssid=AdhocTest key=testtest

netsh wlan start hostednetwork


3. 이더넷 공유

네트웍 등록 정보 > 이더넷 > 속성 > 공유 > 인터넷 연결 공유 > 홈 네트워킹 연결에 신규로 생성된 인터페이스 선택 (Wi-Fi 2)


4. 스마트폰으로 접속

SSID=AdhocTest

암호=testtest


5. 노트북에서 연결 된 내역 확인

netsh wlan show hostednetwork


<<명령어 실행 전의 네트워크 연결 정보>>


<<1. 무선 네트웍 카드가 무선 AP로 만들 수 있는지 확인 >>

 

<<2. 무선 AP로 만들기: SSID, Key는 임의 값 입력>>

 

 

<<네트웍에 Wi-Fi 2 인터페이스 생성>>연결 상태가 네트웍크에 연결 되어 있지 않음

 

 



<<3. 이더넷 공유>>


<<이더넷 공유 후 Wi-Fi 2의 연결이 인터넷 엑세스 상태>>


<<5. 노트북에서 연결 된 내역 확인>>


출처 : http://blog.naver.com/e1seung/110172460781


'Storage > 정보' 카테고리의 다른 글

부트스트랩 테마  (0) 2013.07.19

사용자를 추가하기 위해서는 반드시 루트 계정이 있어야 한다.

1. grant all privileges on *.* to 'testuser'@'localhost'

   identified by '설정패스워스' with grant option;

2. grant all privileges on *.* to 'testuser'@'%'

   identified by '설정패스워스' with grant option;

3. grant reload,process on *.* to 'testuser'@'locahost';

4. grant usage on *.* to 'testuser'@'locahost';

1번과 2번의 경우는 슈퍼 계정!! root와 같은 모든 권한을 가지는 계정을 가지게 된다.

1번 계정은 로컬호스트에서 접속을 할 경우에만 사용되는것이고,

2번 계정은 다른 호스트에서 접속을 하기 위해 사용된다.

testuser란 계정으로 어디에서든지 접속을 하려면 testuser이름으로 1,2번 계정을 모두 가지고 있는 것이 필요하다.

3번 계정은 패스워드가 정의되어있지 않다.

이것은 로컬호스트에서 reload와 process관리 권한을 가지고있다.

mysqladmin reload,mysqladmin refresh, mysqladmin flush-xxx, mysqladmin processlist 도 실행할수 있다고 한다. 하지만 어떤 데이터 베이스에도 접급할수있는 권한은 없다!

나중에 grant명령문을 입력하새ㅓ 권한을 추가할수도 있다.

4번 계정은 3번가 마찬가지 이지만 아무런 권한이 없는 계정을 만든것이다.

USAGE가 그런 뜻을 의미하는데, 이것은 모든 글로번 권한을  'N'으로 설정한 것이라고 한다.

이계정에 특정 권한을 나중에 승인할 것이라는 가정을 한다.

계정을 생성하는 또 한가지 방법은 INSERT 문을 사용하는 것이다.

1. insert into user values('locahost','계정명',password('패스워드'),

   'y','y','y','y','y','y','y','y','y','y','y','y','y','y','y','y');

2. insert into user values('%','계정명',password('패스워드'),

   'y','y','y','y','y','y','y','y','y','y','y','y','y','y','y','y');

3. insert into user set Host='locahost',User='계정명',Reload_priv='y',Process_priv='y';

4. insert into user(Host,User,Password) values('locahost','계정명','');

5. flush privileges;

1,2,3,4번 계정의 의미는 위에서 grant를 이용하여 만든 계정과 의미가 같다.

5번의 flush privileges; 는 서버가 grant 테이블을 다시 읽어 오도록 만들기 위해서 이다.

그렇지 않으면 서버를 재 구동 시키기 전에는 변경 사항이 적용되지 않는다.

하지만 grant를 사용하는 경우는 flush pricileges가 필요 없다.

1,2번의 insert문의 패스워드 입력에서 password('패스워드')라고 사용한것은 패스워드를 암호화 하기 위해서 이다. grant명령문은 알아서 암호화가 된다!

이번에는 데이터베이스 접근 권한을 설정해보자.

1. grant select, insert, update, delete, create, drop

   on bankaccount.*

   to 'custom'@'localhost'

   identified by 'obscure';

2. grant select, insert, update, delete, create, drop

   on expenses.*

   to 'custom'@'whitehouse.gov'

   identified by 'obscure';

3. grant select, insert, update, delete, create, drop

   on customer.*

   to 'custom'@'server.domain'

   identified by 'obscure';

1번 계정은 bankaccount 데이터 베이스에 접근할 수는 있으나, 로컬 호스트에서만 가능하다.

2번 계정은 expenses 데이터 베이스에 접근할 수 있으나, 호스트 whitehouse.gov에서만 가능하다.

3번 계정은 customer 데이터 베이스에 접근할 수 있으나, server.domain에서만 가능하다.

insert를 사용한 방법.

1-1. insert into user(Host,User,Password) values('locahost','custom',password('obscure'));

1-2. insert into user(Host,User,Password) values('whitehouse.gov','custom',password('obscure'));

1-3. insert into user(Host,User,Password) values('server.domain','custom',password('obscure'));

2-1. insert into db

     (Host,Db,User,Select_priv,Insert_priv,Update_priv,Delete_pric,Create_pric,Drop_pric)

     values('localhost','bankaccount','custom','y','y','y','y','y','y');

2-2. insert into db

     (Host,Db,User,Select_priv,Insert_priv,Update_priv,Delete_pric,Create_pric,Drop_pric)

     values('whitehouse.goc','expenses','custom','y','y','y','y','y','y');

2-3. insert into db

     (Host,Db,User,Select_priv,Insert_priv,Update_priv,Delete_pric,Create_pric,Drop_pric)

     values('server.domain','customer','custom','y','y','y','y','y','y');

flush privileges;

이것도 마찬가지로 flush pricileges; 를 반드시 해줘야 적용이 된다~

한가지 팁이라면... % 와일드 카드 문자를 사용하여 grant명령문을 입력하면 모든 곳에서 허용이 된다.

예) grant ... on *.* to 'user'@'%.mydomain.com' identified by 'myboss';

   (이렇게 하면 .mydomain.com앞에  뭐가 붙어서 오든지 허용이 된다는 뜻.)

    grant ... on test_%.* to ... identified by ...;

   (이건 데이터베이스 이름이 test_ 로 시작하는 모든것에 사용권한을 부여한다는 뜻이다.)

 원본 레퍼런스 참조.

 

mysql 실행

>mysql -u 계정명 -p 

>비밀번호


>show databases;

>use 데이터베이스명;

>show tables;

>select 속성1, 속성2, ...., 속성n from 테이블명;

>select * from 테이블명;


사용자 계정 추가

>use mysql;

>insert into user(host,user,password) values('localhost','계정명',password('비밀번호'));로컬접근 허용

>insert into user(host,user,password) values('%','계정명',password('비밀번호'));외부접근 허용

>flush privileges;


db 생성후 db에 계정연결

>grant all privileges on DB명.* to 계정명@localhost identified by '비밀번호' with grant option;

>flush privileges;



- 특정 사용자의 외부접근을 허용 (이미 만들어진 계정에서)

>update user set host='%' where host='localhost' and user='계정명';

>flush privileges;

+ Recent posts