Object Relation Mapping
오늘학습목표 ORM 에대한이해 Spring Data JPA 를활용한 ORM 구현
태그기반질답게시판
요구사항 설계실습 사용자는질문을할수있어야한다. 질문에대한답변을할수있어야한다. 질문을할때태그를추가할수있어야한다. 태그는태그풀에존재하는태그만추가할수있다. 태그가추가될경우해당태그수는 +1 증가, 삭제될경우해당태그수는 -1 증가해야한다.
어디서부터시작할것인가? 객체 or 테이블
User 1 0..n Question 0..n 0..n Tag 1 1 0..n Answer 0..n
비즈니스로직구현
테이블설계로부터시작한다면 요구사항 질문할때태그를추가할수있다. ( 예 java eclipse) 질문을수정할때태그를수정할수있다. ( 예. eclipse ant)
질문추가시 java eclipse 태그추가할경우 insert into question values(?,?,?,?,?); => question_id = 1 select id, name from tag where name= java ; => tag_id = 11 select id, name from tag where name= eclipse ; => tag_id = 12 insert into question_tag values( 1, 11 ); insert into question_tag values( 1, 12 ); update tag set tagged_count = tagged_count + 1 where name= java ; update tag set tagged_count = tagged_count + 1 where name= eclipse ;
질문수정시 eclipse ant 태그추가할경우 update question set title=?, contents=? where question_id = 1; delete from question where question_id = 1; select id, name from tag where name= eclipse ; => tag_id = 12 select id, name from tag where name= ant ; => tag_id = 13 insert into question_tag values( 1, 12 ); insert into question_tag values( 1, 13); update tag set tagged_count = tagged_count + 1 where name= ant ; update tag set tagged_count = tagged_count - 1 where name= java ;
객체간의관계는사라지고단순 CRUD 만존재하게된다.
객체설계로부터시작한다면 요구사항 질문할때태그를추가할수있다. ( 예 java eclipse) 질문을수정할때태그를수정할수있다. ( 예. eclipse ant)
orm_start 브랜치 일단테이블구조는의식하지않고비즈니스로직구현한다.
구현실습 질문할때태그를추가할수있다. ( 예 java eclipse) Question 에 Tag 를추가한다. 태그풀 (TagRepository 로추상화 ) 에서 Tag 추출한다. Tag 가존재하는경우에만 Tag 로인정한다. Tag 를추가하는경우 Tag 의 taggedcount 를 +1 증가시킨다.
구현실습 질문을수정할때태그를수정할수있다. ( 예. eclipse ant) 수정할 Question 을생성한다. Tag 를추가한다. 태그풀 (TagRepository 로추상화 ) 에서 Tag 추출한다. Tag 가존재하는경우에만 Tag 로인정한다. 추가된 Tag 는 Tag 의 taggedcount 를 +1 증가, 삭제된 Tag 는 Tag 의 taggedcount 를 -1 감소한다.
자바객체와테이블매핑
@Entity @Entity public class User { } @Entity public class Question { }
@Entity public class User { @Id private String userid = null; } @Id
객체로부터테이블스키마 Export public class JPASchemaExport { public static void main(string[] args) { Ejb3Configuration cfg = new Ejb3Configuration(); HashMap<String, String> props = new HashMap<String, String>(); props.put("hibernate.format_sql", "true"); Ejb3Configuration configured = cfg.configure("slipp.qna", props); SchemaExport se = new SchemaExport(configured.getHibernateConfiguration()); se.setdelimiter(";"); se.create(true, false); } } 객체와테이블을매핑하면테이블스키마를자동으로 Export 할수있다.
@Entity public class Question { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long questionid; } @Id 자동증가
@Column @Entity public class Question { @Column(name = "title", length=100, nullable = false) private String title; }
@Entity public class Employee { @OneToOne @JoinColumn(name="PSPACE_ID") private ParkingSpace parkingspace; } @OneToOne
@Entity public class Question { @OneToMany(mappedBy = "question") private List<Answer> answers; } @OneToMany
@ManyToOne @Entity public class Answer { @ManyToOne @org.hibernate.annotations.foreignkey(name = "fk_answer_parent_id") private Question question; }
@Entity public class Question { @ManyToMany private Set<Tag> tags = Sets.newHashSet(); } @ManyToMany
@ManyToMany Join Table @Entity public class Question { @ManyToMany @JoinTable(name = "question_tag", joincolumns = @JoinColumn(name = "question_id"), inversejoincolumns = @JoinColumn(name = "tag_id")) @org.hibernate.annotations.foreignkey (name = "fk_question_tag_question_id", inversename = "fk_question_tag_tag_id") private Set<Tag> tags = Sets.newHashSet(); }
@Transient @Entity public class Question { @Transient private String plaintags; } 객체의필드는기본적으로테이블과매핑한다. 필드중테이블칼럼과매핑하고싶지않은경우 @Transient 를사용한다.
Lazy Loading @Entity public class Question { @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "question_tag", joincolumns = @JoinColumn(name = "question_id"), inversejoincolumns = @JoinColumn(name = "tag_id")) @org.hibernate.annotations.foreignkey (name = "fk_question_tag_question_id", inversename = "fk_question_tag_tag_id") private Set<Tag> tags = Sets.newHashSet(); } @OneToMany(mappedBy = "question", fetch = FetchType.LAZY) @OrderBy("answerId DESC") private List<Answer> answers;
ORM 설정및 Repository 구현 JPA( 표준 ) + Hibernate( 구현체 )
JPA 설정 의존관계추가 <dependency> <groupid>org.springframework</groupid> <artifactid>spring-orm</artifactid> <version>${org.springframework.version}</version> </dependency> <dependency> <groupid>org.hibernate.javax.persistence</groupid> <artifactid>hibernate-jpa-2.0-api</artifactid> <version>1.0.0.final</version> </dependency> <dependency> <groupid>org.hibernate</groupid> <artifactid>hibernate-entitymanager</artifactid> <version>${hibernate.version}</version> </dependency> <dependency> <groupid>org.springframework.data</groupid> <artifactid>spring-data-jpa</artifactid> <version>1.0.3.release</version> </dependency>
JPA 설정 META-INF/persistence.xml <?xml version="1.0" encoding="utf-8" standalone="no"?> <persistence [...]> <persistence-unit name="slipp.qna" transaction-type="resource_local"> <provider>org.hibernate.ejb.hibernatepersistence</provider> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.h2dialect" /> <property name="hibernate.hbm2ddl.auto" value="create" /> <property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.improvednamingstrategy" /> <property name="hibernate.format_sql" value="true" /> <property name="hibernate.show_sql" value="false"/> </properties> </persistence-unit> </persistence>
JPA 설정 infrastructure.xml <?xml version="1.0" encoding="utf-8"?> <beans [...]> <bean id="jpadialect" class="org.springframework.orm.jpa.vendor.hibernatejpadialect" /> <bean id="entitymanagerfactory" class="org.springframework.orm.jpa.localcontainerentitymanagerfactorybean" p:persistencexmllocation="classpath:meta-inf/persistence.xml" p:persistenceunitname="slipp.qna" p:datasource-ref="datasource" p:jpadialect-ref="jpadialect" /> <bean id="transactionmanager" class="org.springframework.orm.jpa.jpatransactionmanager" p:entitymanagerfactory-ref="entitymanagerfactory" /> <bean id="datasource" class="org.apache.commons.dbcp.basicdatasource" p:driverclassname="org.h2.driver" p:url="jdbc:h2:~/slipp-user" p:username="sa" p:password="" destroy-method="close" /> <tx:annotation-driven transaction-manager="transactionmanager" /> </beans>
Spring 설정 applicationcontext.xml <?xml version="1.0" encoding="utf-8"?> <beans [...]> <import resource="classpath:infrastructure.xml"/> <jpa:repositories base-package="net.slipp.repository"/> </beans>
실습 package net.slipp.repository.user; import net.slipp.domain.user.user; import org.springframework.data.repository.crudrepository; public interface UserRepository extends CrudRepository<User, String> { }
orm_finish 브랜치 실습 QnA 게시판완료
JPA 와 Hibernate
JPA Query - JPQL -- 기본쿼리 SELECT e FROM Employee e -- Filtering 쿼리 SELECT e FROM Employee e WHERE e.department.name = 'NA42' AND e.address.state IN ('NY','CA') -- Join 쿼리 SELECT p.number FROM Employee e, Phone p WHERE e = p.employee AND e.department.name = 'NA42' AND p.type = 'Cell'
JPA Query - JPQL -- 인자전달 SELECT e FROM Employee e WHERE e.department =?1 AND e.salary >?2 SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :base JPQL 은각데이터베이스별로다른 SQL 을표준화한 Query Language 이다. Hibernate 만사용한다면 HQL 이존재한다.
JPA Query 실행 - NamedQuery -- NamedQuery 정의 @NamedQuery(name="findEmployeesAboveSal", query="select e " + "FROM Employee e " + "WHERE e.department = :dept AND " + " e.salary > :sal") -- NamedQuery 사용 public List<Employee> findemployeesabovesal(department dept, long minsal) { return em.createnamedquery("findemployeesabovesal", Employee.class).setParameter("dept", dept).setparameter("sal", minsal).getresultlist(); }
JPA Query Criteria API -- JPQL SELECT e FROM Employee e WHERE e.name = 'John Smith' -- Criteria API CriteriaBuilder cb = em.getcriteriabuilder(); CriteriaQuery<Employee> c = cb.createquery(employee.class); Root<Employee> emp = c.from(employee.class); c.select(emp).where(cb.equal(emp.get("name"), "John Smith"));
Spring Data JPA public interface UserRepository extends JpaRepository<User, Long> { @Query("select u from User u where u.emailaddress =?1") User findbyemailaddress(string emailaddress); } public interface UserRepository extends JpaRepository<User, Long> { @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname") User findbylastnameorfirstname(@param("lastname") String lastname, @Param("firstname") String firstname); }
Spring Data JPA 쿼리 http://www.springsource.org/spring-data/jpa
Spring Data JPA 사용규칙 특정칼럼을활용한 select 나단순쿼리는 method naming convention 을따른다. 쿼리복잡도가높아 method naming convention 에의해처리하지못하는경우, 하지만정적인쿼리는 Named Query(@Query Annotation) 을활용한다. 동적인쿼리의경우에는 Criteria API 를활용한다.
Spring Data JPA 복잡한쿼리 public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor { } JpaSpecificationExecutor 인터페이스를상속하면 Specification 인터페이스를실행할수있는추가메소드를실행할수있다. public interface Specification<T> { Predicate topredicate(root<t> root, CriteriaQuery<?> query, CriteriaBuilder builder); }
Spring Data JPA 복잡한쿼리 public class CustomerSpecs { public static Specification<Customer> islongtermcustomer() { return new Specification<Customer>() { Predicate topredicate(root<t> root, CriteriaQuery<?> query, CriteriaBuilder builder) { LocalDate date = new LocalDate().minusYears(2); return builder.lessthan(root.get(customer_.createdat), date); } }; } public static Specification<Customer> hassalesofmorethan(montaryamount value) { return new Specification<Customer>() { Predicate topredicate(root<t> root, CriteriaQuery<?> query, CriteriaBuilder builder) { } } } }; // build query here
JPA Query Criteria API
JPA Query Criteria API
ORM 사용시테이블스키마관리
기능안정화단계전까지 데이터베이스를매번초기화한다. <?xml version="1.0" encoding="utf-8" standalone="no"?> <persistence [...]> <persistence-unit name="slipp.qna" transaction-type="resource_local"> <provider>org.hibernate.ejb.hibernatepersistence</provider> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.h2dialect" /> <property name="hibernate.hbm2ddl.auto" value="create" /> <property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.improvednamingstrategy" /> <property name="hibernate.format_sql" value="true" /> <property name="hibernate.show_sql" value="false"/> </properties> </persistence-unit> </persistence>
기능안정화단계 CarbonFive 와같은플러그인을활용해테이블스키마변경관리한다. <?xml version="1.0" encoding="utf-8" standalone="no"?> <persistence [...]> <persistence-unit name="slipp.qna" transaction-type="resource_local"> <provider>org.hibernate.ejb.hibernatepersistence</provider> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.h2dialect" /> <property name="hibernate.hbm2ddl.auto" value= update" /> <property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.improvednamingstrategy" /> <property name="hibernate.format_sql" value="true" /> <property name="hibernate.show_sql" value="false"/> </properties> </persistence-unit> </persistence>
Maven Carbon Five 플러그인활용 https://code.google.com/p/c5-db-migration/
schema_version 테이블
mvn db-migration:new Dname= 생성쿼리설명 mvn db-migration:migrate
Spring Security 권한처리
applicationcontext-security.xml <beans:beans [...]> <global-method-security pre-post-annotations="enabled" /> <http pattern="/resources*/**" security="none"/> <http use-expressions="true"> <intercept-url pattern="/qna/form" access="hasrole('role_user')"/> [ ] <intercept-url pattern="/**" access="permitall" /> <form-login login-processing-url="/security/login" login-page="/security/form" default-target-url="/" always-use-default-target="false" /> <logout logout-url="/security/logout" logout-success-url="/" /> </http> <authentication-manager alias="authenticationmanager"> <authentication-provider ref="authenticationprovider"/> </authentication-manager> <beans:bean id="authenticationprovider" class="net.slipp.support.security.slippauthenticationprovider" /> <beans:bean id="userdetailsservice" class="net.slipp.support.security.slippuserdetailsservice"/> </beans:beans>
Spring Security 설정 web.xml <filter> <filter-name>springsecurityfilterchain</filter-name> <filter-class>org.springframework.web.filter.delegatingfilterproxy</filter-class> </filter> <filter-mapping> <filter-name>springsecurityfilterchain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Controller 와로그인사용자연결하기 @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LoginUser { boolean required() default true; }
Controller 와로그인사용자연결하기 @RequestMapping(value="", method=requestmethod.post) public String create(@loginuser User user, HttpServletRequest request, Question question) { logger.debug("question : {}", question); } qnaservice.createquestion(user, question); return "redirect:/qna"; [...] @RequestMapping(value="", method=requestmethod.put) public String update(@loginuser User user, Question question) { logger.debug("question : {}", question); qnaservice.updatequestion(user, question); return "redirect:/qna"; }