관리 메뉴

nkdk의 세상

symfony Model: 데이터 접근(Accessing Data) 본문

My Programing/PHP

symfony Model: 데이터 접근(Accessing Data)

nkdk 2010. 1. 20. 21:17

http://kr.blog.yahoo.com/hslee49/1337026


symfony 에서 데이터는 객체를 통해서 접근이 된다. 만약 관계형 모델과 SQL 을 통한 데이터 수정 및 선택에익숙하다면, 객체 모델 방법은 복잡해 보일 수 있다. 그러나 일단 한 번 데이터 접근을 위한 객체지향의 힘에 맛을 들이게된다면, 이 방법을 더욱 좋아하게 될 것이다. 그러나 먼저 같은 용어에 대해서 확실히 할 필요가 있겠다. 관계형 그리고 객체데이터 모델은 비슷한 개념을 사용하지만 각자의 명칭을 가지고 있다.

Rerational    Object-Oriented
Table              Class
Row,record      Object
Field, column   Property

 ** 컬럼 값 가져오기 **
symfony가 모델을 형성할 때, 하나의 기본 객체 클래스를 schema.yml 파일에 정의된 각 테이블 마다 생성을 한다. 이들 각각의클래스들은 기본적으로 형성자 new, 접근자 getXXX() 그리고 컬럼정의에 의거한 변이형성자 setXXX() 를 제공한다.이 메쏘드들은 객체를 생성하고 객체의 자산에 대한 접근을 제공한다.

* 생성된 객체 클래스 메쏘드들 *
$article = new Article();
$article->setTitle('My first article');
$article->setContent('This is my very first article.\n Hope you enjoy it!');
 
$title   = $article->getTitle();
$content = $article->getContent();

여러개의 필드값을 설정하기 위해서 fromArray() 메쏘드를 사용할 수 있으며 이는 각 객체 클래스를 위해서 생성이 되어 있다.

$article->fromArray(array(
  'title'   => 'My first article',
  'content' => 'This is my very first article.\n Hope you enjoy it!',
));


 ** 관련된 기록들을 가져오기 **
blog_comment테이블의 article_id 컬림은 함축적으로 blog_article 에 대한 외부 키임을 정의하고 있다. 각 comment 는하나의 article 에 연관되어 있고 하나의 article 은 다수의 comment 를 가질 수 있다. 생성된 클래스들은다섯개의 메소드를 포함하고 있으며 이들은 객체지향 방법으로 이들 관계를 해석하게 한다 :
    * $comment->getArticle(): Article 객체 얻기
    * $comment->getArticleId(): 관련된 Article 객체의 ID 얻기
    * $comment->setArticle($article): 관련된 Article 객체 규정
    * $comment->setArticleId($id): ID 로 부터 관련된 Article 객체 규정하기
    * $article->getComments(): 관련된 Comment 객체 얻기

getArticleId(),setArticleId() 메소드들은, article_id 컬럼을 일반 컬럼으로 여길 수 있지만 편하게 관계를 설정하는 것으로생각할 수 있게 한다. 객체지향적인 접근의 장점은 다른 세 메소드안에 더 명확해 진다.

* 외부키는 틀별한 세터로 해석이 된다 *
$comment = new Comment();
$comment->setAuthor('Steve');
$comment->setContent('Gee, dude, you rock: best article ever!');
 
// Attach this comment to the previous $article object
$comment->setArticle($article);
 
// Alternative syntax
// Onl y makes sense if the object is already saved in the database
$comment->setArticleId($article->getId());

생성된 getter 들을 어떻게 사용하는지 보자. 연쇄 메소드 호출의 예제는 다음과 같다.

// Many to one  relationship
echo $comment->getArticle()->getTitle();
 => My first article
echo $comment->getArticle()->getContent();
 => This is my very first article.
    Hope you enjoy it!
 
// One  to many relationship
$comments = $article->getComments();

getArticle()메쏘드는 Article 클래스의 객체를 반환하며, 이 객체는 getTitle() 접근자를 사용할 수 있게 해준다. 이는$comment->getArticleId() 호출을 시작으로 하는 두세 라인의 코드를 사용하는 결합하는 것보다 훨씬 나을것이다.  $comment 변수는 Comment 클래스 객체의 배열을 포함하고 있다. 첫 배열의 값을 $comment[0] 을이용하거나 foreach 를 이용한 반복을 사용하여 출력할 수 있다.

 ** 데이터 저장 및 삭제 **
new생성자 호출을 통해 새로운 객체를 생성하지만 blog_article 테이블 에서의 실제 기록을 하는 것은 아니다. 객체를수정하는 것은 데이터베이스에 영항을 주는 것 또한 아니다. 데이터베이스에 데이터를 저장하기 위해서는 save() 메쏘드를호출해야만 한다.

 $article->save();

ORM 은 객체들 사이의 관계를 판단하기에충분히 현명하여 $article 객체를 저장함과 동시에 관련된 $comment 객체도 저장한다. 또한 저장된 객체가 데이터베이스내에 짝을 이루는 테이블이 존재하는지 알고 있기때문에 save() 호출은 때때로 SQL 의 INSERT 로 해석이 될 수도있으며, 때로는 UPDATE 로 해석될 수 도 있다. Primary key 는 자동적으로 save() 메쏘드에 의해서 설정되고저장된 후에는 $article->getId() 를 이용하여 새 primary key 를 얻어올 수 있다.
isNew()를 이용하여 객체가 새로운 것인지 확인할 수 있으며 isModified() 를 이용하여 객체가 수정되어서 저장해야 할 필요가있는지 확인할 수 있다. delete() 메쏘드를 이용해서는 원하는 comment 들을 삭제할 수 있다.

foreach ($article->getComments() as $comment)
{
  $comment->delete();
}

때로는 delete() 메쏘드 호출후에도 객체는 request 가 끝날때 까지 유효할 수 있다. 데이터베이스에서 삭제되었는지 확인하기 위해서 isDeleted() 메쏘드를 사용하면 된다.

 ** Primary key 를 사용하여 데이터 가져오기 **
 특별한 데이터에 대해서 primary key 를 알고 있다면 동료 클래스의 retrieveByPk() 메쏘드를 이용하라.
$article = ArticlePeer::retrieveByPk(7);

schema.yml파일은 id 를 blog_article 테이블의 primary key 로서 정의하며, 이 구문이 실제로 id 값 7 을 가진article 을 반환하게 된다. Primary key 를 사용하면 오로지 하나의 레코드 값만이 반환이 된다.
어떤경우에 primary key 는 하나의 컬럼 이상을 포함 할 수 있다. 이러한 경우네 retrieveByPk() 메쏘드는 다중매개변수값을 허용한다. 또한 retrieveByPKs() 메쏘드를 통해 primary key 값들에 근거한 다중 객체들을 선택할수 도 있다.

 ** 조건에 의거한 레코드 가져오기 **
하나 이상의 레코드를 가져오기를 원한다면 해당하는 동료 클래스의 doSelect() 메쏘드를 호출 해야 한다. 예를들어 ArTicle 클래서의 객체를 가져와야 한다면 ArticlePeer::doSelect() 를 호출한다.
doSelect()의 첫 매개변수는 Criteria 클래스의 객체인데 이 객체는 간단히 데이터베이스 추상화를 위해서 SQL 을 가지고 있지는 않는질의 정의 클래스라 할 수 있다. 빈 Criteria 는 클래스의 객체들을 반환한다. 예를 들어 모든 article 들을가져오려면...

$c = new Criteria();
$articles = ArticlePeer::doSelect($c);
 
// Will result in the following SQL query
SELECT blog_article.ID, blog_article.TITLE, blog_article.CONTENT,
       blog_article.CREATED_AT
FROM   blog_article;

doSelect()를 호출하는 것은 단순 SQL 질의 보다 더욱 강력하다. 첫째로 SQL 이 선택한 DBMS 를 위해 최적화 되어 있다. 둘째로어떤 값이 Criteria 에 전달이 되더라도 SQL 에 전달되기 이전에 SQL injection 위험에 대비하여 값이안전화되어 있다. 셋째로 결과 set 보다는 객체들의 배열을 반환한다. ORM 은 자동적으로 데이터베이스 결과 값에 근거한객체를 생성한다. 이것을 hydrating 이라 부른다.


좀더 복잡한 객체 선택을 위해 WHERE,ORDER BY, GROUP BY 그리고 다른 SQL 구문들을 대신할 수 있는 것이 필요하다. Criteria 객체는 이러한모든 조건들에 대한 메쏘드와 매개변수들을 가지고 있다. 예를 들어 steve 에 의해 작성된 모든 comment 들을, 날짜순으로 가져와야 한다면 Criteria 를 다음과 같이 구성한다.

$c = new Criteria();
$c->add(CommentPeer::AUTHOR, 'Steve');
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$comments = CommentPeer::doSelect($c);
 
// Will result in the following SQL query
SELECT blog_comment.ARTICLE_ID, blog_comment.AUTHOR, blog_comment.CONTENT,
       blog_comment.CREATED_AT
FROM   blog_comment
WHERE  blog_comment.author = 'Steve'
ORDER BY blog_comment.CREATED_AT ASC;

add()메쏘드에 매개변수로 전달된 클래스 상수값들은 자산의 이름을 나타낸다. 위에서 보듯이 각 컬럼들의 대문자로 이루어졌음을 알 수있다. 예를 들어 blog_article 테이블의 컬럼 content 를 나타태기 위해서 ArticlePeer::CONTENT와 같은 클래스 상수를 언급했다.

blog_comment.AUTHOR 대신에 CommentPeer::AUTHOR 를사용하는 이유는 그것이 SQL 질의의 출력이 되기 때문이다. author 필드의 name을 contributor로 변경한다고가정해보자. 이때 blog_comment.AUTHOR 를 사용했다면 매번 모델(model)을 호출 할 때마다 이 부분을 바꿔줘야할 것이다. 반면에 CommentPeer:AUTHOR 를 사용하면 간단히 schema.yml 파일의 한 컬럼만 바꿔주고모델(model)을 다시 형성해주면 된다.

* SQL 구문과 Criteria 객체 구문 비교 *
<pre>
SQL                                   Criteria
WHERE column = value          ->add(column, value);
WHERE column <> value        ->add(column, value, Criteria::NOT_EQUAL);
Other Comparison Operators  
> , <                                    Criteria::GREATER_THAN, Criteria::LESS_THAN
>=, <=                                  Criteria::GREATER_EQUAL, Criteria::LESS_EQUAL
IS NULL, IS NOT NULL           Criteria::ISNULL, Criteria::ISNOTNULL
LIKE, ILIKE                           Criteria::LIKE, Criteria::ILIKE
IN, NOT IN                           Criteria::IN, Criteria::NOT_IN
Other SQL Keywords  
ORDER BY column ASC         ->addAscendingOrderByColumn(column);
ORDER BY column DESC       ->addDescendingOrderByColumn(column);
LIMIT limit                            ->setLimit(limit)
OFFSET offset                       ->setOffset(offset)
FROM table1, table2
WHERE table1.col1 = table2.col2  ->addJoin(col1, col2)
FROM table1 LEFT JOIN table2 ON
table1.col1 = table2.col2              ->addJoin(col1, col2, Criteria::LEFT_JOIN)
FROM table1 RIGHT JOIN table2 ON
table1.col1 = table2.col2         ->addJoin(col1, col2, Criteria::RIGHT_JOIN)
</pre>

형성된 클래스에서 유효한 메쏘드(method)들을 이해하고 찾아내는 가장 좋은 방법은 lib/model/om/ 폴더에 있는기본(base) 파일들을 보는 것이다. 메쏘드 이름들은 다소 명확하다, 그러나 그위에 좀더 추가적인 코멘트를 하는것이 필요하면config/propel.ini 파일의 propel.builder.addComments 매개변수를 true 로 설정하고 모델을재생성 하면 된다.

다수의 조건을 가진 Criteria 의 다른 예를 보자. "enjoy"라는 단어를 포함하는 모든 기사에 대해 Steve가 추가한 의견들을 날짜순으로 가져오는 것이다.

$c = new Criteria();
$c->add(CommentPeer::AUTHOR, 'Steve');
$c->addJoin(CommentPeer::ARTICLE_ID, ArticlePeer::ID);
$c->add(ArticlePeer::CONTENT, '%enjoy%', Criteria::LIKE);
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$comments = CommentPeer::doSelect($c);
 
// Will result in the following SQL query
SELECT blog_comment.ID, blog_comment.ARTICLE_ID, blog_comment.AUTHOR,
       blog_comment.CONTENT, blog_comment.CREATED_AT
FROM   blog_comment, blog_article
WHERE  blog_comment.AUTHOR = 'Steve'
       AND blog_article.CONTENT LIKE '%enjoy%'
       AND blog_comment.ARTICLE_ID = blog_article.ID
ORDER BY blog_comment.CREATED_AT ASC

SQL이 매우 복잡한 질의를 만들 수 있도록 해주는 간단한 언어인것 처럼, Criteria 객체도 복잡한 수준의 조건을 다룰 수있다. 그러나 많은 개발자들이 조건을 객체지향의 논리로 해석하기 이전에 먼저 SQL을 가지고 생각하기 때문에 Criteria객체는 아마도 처음에는 이해하기 어려울 수 있겠다. 이해하기 가장 쉬운 방법은 예제들로 부터 배우는 것이겠다. Symfony 의홈페이지는 여러각도로 여러분을 개발시켜줄 Criteria 생성의 예제로 가득하다. doSelect() 메쏘드이외에 모든 동료클래스는 doCount() 메쏘드를 를 가지고 있는데 이는 매개변수로 전달된 기준을 만족시키는 데이터의 숫자를 단순히 세어정수로 반환해준다. 객체를 반환하는 경우는 없기 때문에 doCount() 는 doSelect() 보다 훨씬 빠르다. 
동료 클래스는 또한 doDelete(), doInsert(), doUpdate() 메쏘드들도 제공하며, Criteria 를매개변수를 받아들인다. 이 메쏘드들은 DELETE, INSERT, UPDATE 질의를 데이터베이스에 보낼 수 있게 해준다. 이Propel 메쏘드들에 대한 더 자세한 내용은 모델안에 생성되어있는 동료 클래스들을 확인하면 된다.
마지막으로 첫반환되는 객체만을 원한다면 doSelect() 대신에 doSelectOne() 를 사용하면 된다. 아마도 Criteria 가 단하나의 결과만을 반환한다는 것을 알고 있을때일 것이며, 장점은 이 메쏘드가 객체의 배열보다는 하나의 객체만 반환한 다는 것이다.

doSelect() 질의가 많은 수의 결과를 반환할 때, 단지 응답에는 그 일부만 보여주고 싶어할 수 있다.Symfony 는 sfPropelPager 라 불리는 페이징기능을 제공하는 클래스를 제공하는데, 이는 결과를 자동으로페이징한다. 더 자세한것은 http://www.symfony-project.org/cookbook/1_0/en/pager 을 참고하자.

** 원형 그대로의 SQL 질의 사용하기 **
때로는 객체를 가져오기 보다는 데이터베이스에 의해 계산되어진 단순하게 합성된 결과들만을 원할 수 있다. 예를 들어 가장 최신의모든 기사들이 생성된 날짜를 얻고자 할 때, 모든 기사들을 가져와서 배열상에서 반복작업을 하는 것은 그리 효과적이라 할 수없다.  아마도 데이터베이스에 객체 보다는 단지 원하는 결과만을 돌려줄것을 요청하고 싶어할 것이다.
반면에 데이터베이스관리를 직접적으로 하기위해서 PHP 명령을 호출하고 싶지 않을 것이다, 왜냐하면 그렇게 하면 데이터베이스 추상화의 장점을잃어버리게 될 것이기 때문이다. 이것은 ORM 인 Propel 을 우회해 간다는 것을 의미하지만 데이터베이스 추상인 Croel을 피해가는 것은 아니다.
데이터베이스 질의를 Croel 을 사용해서 하려면 다음을 따라야 한다 :
 1. 데이터베이스 연결
 2. 질의 생성
 3. 선언문 생성
 4. 선언문 실행으로 부터의 결과 셋 반복
이것이 무슨말인지 잘 모르겠다면 아래의 예제가 좀더 분명하게 해줄 것이다.

 * Croel 을 이용한 SQL 질의
$connection = Propel::getConnection();
$query = 'SELECT MAX(%s) AS max FROM %s';
$query = sprintf($query, ArticlePeer::CREATED_AT, ArticlePeer::TABLE_NAME);
$statement = $connection->prepareStatement($query);
$resultset = $statement->executeQuery();
$resultset->next();
$max = $resultset->getInt('max');

Propel selection 처럼, Croel 질의들또한 처음 사용시에는 어렵다. 다시 말하지만 기존의 응용프로그램 예제로 부터, 또 지도서로 부터 바른사용법을 보게 될 것이다.

만약 이 프로세스와 데이터베이스의 직접 접근하는 방법을 지나쳐가고 싶다면 Croel 에 의해 제공되는 보안과 추상화를 잃어버리는부담을 갖게 될 것이다. Croel 방법으로 하는 것이 더 길지만 퍼포먼스와 이동성 그리고 보안에 대한 보장을 위한 훌륭한연습이 될 것이다. 이는 특히 질의들이 신뢰할 수 없는 곳으로 부터의 매개변수를 포함하고 있을때 더더욱 확실해 진다.Croel은 필요한 모든 데이터 가공과 데이터베이스의 안전화를 수행한다. 직접적인 데이터베이스 접근은 SQL-injection공격의 위험에 바로 노출되게 하기도 한다.

** 특별 날짜 컬럼 사용하기 **
 일반적으로 테이블이 created_at 컬럼을 가지고 있을때, 이것은 해당 기록이 생성된 날의 시간기록을 저정하는데 사용된다.이와 같이 updated_at 컬럼도 동일하게 적용이 되는데, 각 기록이 갱신이 될 때마다 updated_at 값이 갱신이되겠다.
 장점으로는 symfony 가 이러한 컬럼들의 이름을 인지하고 그들의 갱신을 다루어준다. 수동으로created_at 과 updated_at 컬럼들을 설정할 필요 없이 자동으로 갱신이 될 것이다. 동일하게 created_on,updated_on 으로 명명된 컬럼들에게도 적용이 된다.

 * created_at, updated_at 컬럼들은 자동으로 다루어진다. *
$comment = new Comment();
$comment->setAuthor('Steve');
$comment->save();
 
// Show the creation date
echo $comment->getCreatedAt();
  => [date of the database INSERT operation]

추가적으로, 날짜 컬럼덜에 대한 getter 들은 매개변수로 날자형을 받아들인다.
echo $comment->getCreatedAt('Y-m-d');


<데이터 계층으로 재요소화 하기>
symfony프로젝트를 개발할 때, 때때로 action 안에 영역 로직 코드를 작성함으로 시작하게 된다. 그러나 데이터베이스 질의와 모델생성은 콘트롤러 계층에 저장이 되어서는 안된다. 따라서 모든 데이터와 관련된 로직들은 모델 계층으로 옮겨와야 한다. action에서 하나 이상의 곳에서 같은 요청을 할 필요가 있을 때마다, 모델로의 관련 코드를 전달하는 것에 대해서 생각해보라. 이것이action 을 간결하고 읽기 쉬운 코드로 유지할 수 있게 해준다.
예를 들어 블로그에서 주어진 태그에서 가장 인기있는 열개의 블로그를 가져오는 코드를 생각해보자. 코드는 action 이 아닌 모델에 존재해야 한다. 만약 템플릿에 출력할 필요가 있따면 action 은 단순히 다음과 같아야 한다 :
public function executeShowPopularArticlesForTag()
{
  $tag = TagPeer::retrieveByName($this->getRequestParameter('tag'));
  $this->foward404Unless($tag);
  $this->articles = $tag->getPopularArticles(10);
}
action은 request 매개변수로 부터 tag 클래스의 객체를 생성하고, 데이터베이스 질의에 필요한 모든 코드들은getPopularArticles() 에 위치하게 된다. 이것은 action 을 더욱 읽기 쉬운 코드로 만들어 주고, 모델코드가 다른 action 에서도 쉽게 재사용 될 수 있게 해준다. 코드들을 더 적당한 위치로 옮기는 것이 재요소화의 기법중하나이다. 만약 더 자주 이것을 한다면 코드가 더욱 유지보수 하기 쉬울 것이고 다른 개발자들이 이해하기도 쉬울 것이다. 데이터계층 재요소화의 첫째 가는 원칙은 action 의 코드는 php 코드로서 10줄 이상을 넘어서는 일이 거의 없어야 한다는것이다.