[백업][가리사니] spring 서로 다른 종류의 db에 @transactional [예제포함]
java, postgresql, solr, spring

이 문서는 가리사니 개발자 포럼에 올렸던 글의 백업 파일입니다. 오래된 문서가 많아 현재 상황과 맞지 않을 수 있습니다.

서론

Spring @Transactional Method 적용범위 : rollback 주의

스프링 부트 1.4.x 부터는 솔라 설정법이 조금 달라집니다.

참고 : /2016/08/04/%EB%B0%B1%EC%97%85-%EA%B0%80%EB%A6%AC%EC%82%AC%EB%8B%88-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-1.4.x-%EC%86%94%EB%9D%BC-(spring-data-solr)-%EC%97%B0%EB%8F%99-%EB%B3%80%EA%B2%BD%EC%82%AC%ED%95%AD/html

결론

한쪽에서 오류시 나머지 한쪽도 정상적으로 롤백됩니다.!! 갑자기 왜 결론부터 말씀드리냐면.. 이번 예제 소스는 조금 깁니다... (중간에 삽질해서 실험하는데 오래걸렸네요.. 벌써 새벽.... 오늘도 코딩하다.. 이력서 넣는 시간을 놓혀 버렸..;;;)

메이븐 추가

<!-- 히카리 CP -->
<dependency>
	<groupId>com.zaxxer</groupId>
	<artifactId>HikariCP</artifactId>
</dependency>
<!-- PG-SQL -->
<dependency>
	<groupId>org.postgresql</groupId>
	<artifactId>postgresql</artifactId>
</dependency>
<!-- 롬복 -->
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.16.8</version>
	<scope>provided</scope>
</dependency>
<!-- 하이버네이트 -->
<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-jpamodelgen</artifactId>
	<version>5.2.1.Final</version>
</dependency>
<!-- 솔라 -->
<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-solr</artifactId>
	<version>1.5.4.RELEASE</version>
</dependency>
<!-- JPA -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

연결설정

<프로젝트>/src/main/resources/database.properties

# 히카리 - PostgreSQL
hikari.gs.jdbcUrl: jdbc:postgresql://호스트:포트/데이터베이스?charSet=UTF-8&prepareThreshold=1
hikari.gs.username: 계정
hikari.gs.password: 암호
hikari.gs.maximumPoolSize: 10
# 솔라
solr.gs.host: http://호스트:포트/solr

테이터베이스 연결설정

/**
 * 데이터베이스 설정<br>
 * 2016-07-16 박용서 작성 :
 */
public class DatabaseConfiguration
{
	/**
	 * 가리사니 (PostgreSQL) 히카리 데이터소스
	 */
	@Configuration
	@ConfigurationProperties(prefix = "hikari.gs", locations="classpath:database.properties")
	public static class GsHikariDataSource extends HikariConfig
	{
		@Bean(name = "GsHikariDataSource")
		public DataSource getDataSource()
		{
			System.out.println("데이터 소스 생성");
			return new HikariDataSource(this);
		}
	}

	/**
	 * 가리사니 (PostgreSQL) 하이버네이트
	 */
	@Configuration
	@EnableJpaRepositories
	(
		basePackages = "db.hn.gs.repositories",
		entityManagerFactoryRef = "GsHibernateFactoryBean",
		transactionManagerRef = "GsHibernateTransactionManager"
	)
	public static class GsHibernate
	{
		@Autowired
		@Qualifier("GsHikariDataSource")
		DataSource dataSource;

		@Bean(name = "GsHibernateFactoryBean")
		public LocalContainerEntityManagerFactoryBean getFactoryBean(EntityManagerFactoryBuilder builder)
		{
			return builder.dataSource(dataSource).packages("db.hn.gs.entities").build();
		}

		@Bean(name = "GsHibernateTransactionManager")
		public PlatformTransactionManager getTransactionManager(EntityManagerFactoryBuilder builder)
		{
			return new JpaTransactionManager(getFactoryBean(builder).getObject());
		}
	}

	@Configuration
	@EnableSolrRepositories
	(
		basePackages="db.solr.gs.repositories",
		solrServerRef="GsSolr",
		solrTemplateRef="GsSolrTemplate",
		multicoreSupport=true
	)
	@PropertySource("classpath:database.properties")
	public static class GsSolrConfig
	{
		@Value("${solr.gs.host}")
	    String host;

		@Bean(name = "GsSolr")
	    public SolrServer getSolrServer()
		{
	        return new HttpSolrServer(host);
	    }

		@Bean(name = "GsSolrTemplate")
	    public SolrTemplate getSolrTemplate()
		{
	        return new SolrTemplate(getSolrServer());
	    }
	}
}

서비스 컨포넌트를 스캔

@Configuration
@ComponentScan(basePackages="db.service")
public class SerivceScan
{
	// 단순히 스캔시킬려고 만들어둔것으로 적당한 위치가 떠오르지 않아 일단 따로 빼둠.
}

테이터베이스 기본테이블

PostgreSQL

CREATE TABLE users
(
  no bigint NOT NULL,
  name character varying(64) NOT NULL
);
CREATE SEQUENCE users_no_seq;

솔라 - managed-schema

  • 이 실험에선 한글 형태소를 쓸 필요가 없음으로 기본 text 자료형이 있는상태에서 사용하셔도됩니다.
  • 한글형태소 : http://cafe.naver.com/korlucene 에서 다운로드
  • 스키마 이름은 gs_test
<?xml version="1.0" encoding="UTF-8"?>
<schema version="1.6">

	<field name="_version_" type="long" indexed="true" stored="true" />
	<field name="no" type="tlong" indexed="true" stored="true" />
	<field name="name" type="ko" indexed="true" stored="true" />

	<uniqueKey>no</uniqueKey>
	<defaultSearchField>name</defaultSearchField>

	<fieldType name="long" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0" />
	<fieldType name="tlong" class="solr.TrieLongField" precisionStep="8" positionIncrementGap="0" />

	<fieldType name="ko" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">
		<analyzer>
			<filter class="solr.LowerCaseFilterFactory" />
			<tokenizer class="org.apache.lucene.analysis.ko.KoreanTokenizerFactory" />
			<filter class="solr.LengthFilterFactory" min="1" max="50" />
			<filter class="org.apache.lucene.analysis.ko.KoreanFilterFactory" hasOrigin="true" hasCNoun="true" />
			<filter class="org.apache.lucene.analysis.ko.WordSegmentFilterFactory" hasOrijin="true" />
			<filter class="org.apache.lucene.analysis.ko.HanjaMappingFilterFactory" />
			<filter class="org.apache.lucene.analysis.ko.PunctuationDelimitFilterFactory" />
			<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
		</analyzer>
	</fieldType>

</schema>

JPA 엔티티

package db.solr.gs.entities;

import org.apache.solr.client.solrj.beans.Field;
import org.springframework.data.annotation.Id;
import org.springframework.data.solr.core.mapping.SolrDocument;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

@Data
@SolrDocument(solrCoreName = "gs_test")
@Getter @Setter
public class GsTest
{
	@Id
	@Field
	private long no;

	@Field
	private String name;

	public String toString()
	{
		return no + " : " + name;
	}
}
package db.hn.gs.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name="users")
@Data @Getter @Setter
public class User
{
	@Id
	@SequenceGenerator(name="seq", sequenceName="users_user_seq", allocationSize=1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq")
	long no;

	@Column
	String name;

	public String toString()
	{
		return no + " : " + name;
	}
}

JPA 리포지토리

package db.solr.gs.repositories;

import org.springframework.data.domain.Page;
import org.springframework.data.solr.repository.Query;
import org.springframework.data.solr.repository.SolrCrudRepository;

import db.solr.gs.entities.GsTest;

public interface GsTestRepository extends SolrCrudRepository<GsTest, String>
{
	@Query(value = "*:*", filters = { "" })
	Page<GsTest> findAll();
}
package db.hn.gs.repositories;

import org.springframework.data.jpa.repository.JpaRepository;

import db.hn.gs.entities.User;

public interface UsersRepository extends JpaRepository<User, Long>
{
}

서비스

  • 만들고나서 든 생각이지만.. 구조적으로 한단계를 더 거치는게 맞지 않을까 생각됩니다.....
package db.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import db.hn.gs.entities.User;
import db.hn.gs.repositories.UsersRepository;
import db.solr.gs.entities.GsTest;
import db.solr.gs.repositories.GsTestRepository;

@Component
public class UserTestService
{
	@Autowired
	UsersRepository usersRepository;

	@Autowired
	GsTestRepository gsTestRepository;

	@Transactional
	public void addUser(String name)
	{
		User user = new User();
		user.setName(name);
		User saveUser = usersRepository.save(user);

		GsTest test = new GsTest();
		test.setNo(saveUser.getNo());
		test.setName(saveUser.getName());
		gsTestRepository.save(test);
	}

	@Transactional(readOnly=true)
	public void allPrint()
	{
		System.out.println("pg-sql users");
		usersRepository.findAll().forEach(System.out::println);
		System.out.println("solr users");
		gsTestRepository.findAll().forEach(System.out::println);
	}
}

실행

@SpringBootApplication
public class App implements CommandLineRunner
{
	Logger logger = LoggerFactory.getLogger(App.class);

	@Autowired
	UserTestService userTestService;

	@Override
	public void run(String... args) throws Exception
	{
		logger.info("유저입력!!");
		userTestService.addUser("가리사니");
		userTestService.addUser("개발자공간");
		logger.info("출력!!");
		userTestService.allPrint();
	}

	public static void main(String[] args)
	{
		SpringApplication.run(App.class, args);
	}
}

결과 날짜 INFO ~~ --- [ main] saro.web.App : 유저입력!! 날짜 INFO ~~ --- [ main] saro.web.App : 출력!! pg-sql users 1 : 가리사니 2 : 개발자공간 solr users 1 : 가리사니 2 : 개발자공간

고의로 오류를 내보자!!

UserTestService 클래스

@Transactional
public void addUser(String name)
{
	User user = new User();
	user.setName(name);
	User saveUser = usersRepository.save(user);

	GsTest test = new GsTest();
	test.setNo(saveUser.getNo());
	test.setName(saveUser.getName());
	gsTestRepository.save(test);

	// 오류내기!!
	int error = 0 / 0;
}

결과 PostgreSQL 특성상 SEQUENCE 는 롤백되지 않기 때문에 SEQUENCE 만 1 증가한 상태이며, 나머지 양쪽 모두 롤백되었습니다.