/* * Copyright 2011-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package kr.debop4j.data.ogm.dao; import com.google.common.collect.Lists; import kr.debop4j.core.Guard; import kr.debop4j.core.Local; import kr.debop4j.core.collection.IPagedList; import kr.debop4j.core.collection.PaginatedList; import kr.debop4j.core.parallelism.AsyncTool; import kr.debop4j.core.tools.ArrayTool; import kr.debop4j.core.tools.StringTool; import kr.debop4j.data.hibernate.tools.HibernateTool; import kr.debop4j.data.hibernate.unitofwork.UnitOfWorks; import kr.debop4j.search.tools.SearchTool; import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.hibernate.*; import org.hibernate.search.FullTextQuery; import org.hibernate.search.FullTextSession; import org.hibernate.search.ProjectionConstants; import org.hibernate.search.Search; import org.hibernate.search.query.DatabaseRetrievalMethod; import org.hibernate.search.query.ObjectLookupMethod; import org.hibernate.search.query.dsl.QueryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.Future; import static kr.debop4j.core.Guard.shouldNotBeNull; /** * Hibernate-OGM 용 DAO 입니다.<br /> * hibernate의 Criteria 기능을 제공할 수 없어, Criteria 부분은 hibernate-search를 사용합니다. * * @author 배성혁 ( sunghyouk.bae@gmail.com ) * @since 13. 4. 15. 오후 5:42 */ @Component @SuppressWarnings( "unchecked" ) public class HibernateOgmDao implements IHibernateOgmDao { private static final Logger log = LoggerFactory.getLogger(HibernateOgmDao.class); private static final boolean isTraceEnabled = log.isTraceEnabled(); private static final boolean isDebugEnabled = log.isDebugEnabled(); @Override public synchronized final Session getSession() { return UnitOfWorks.getCurrentSession(); } @Override public synchronized final FullTextSession getFullTextSession() { FullTextSession fts = Local.get(IHibernateOgmDao.FULL_TEXT_SESSION_KEY, FullTextSession.class); if (fts == null || !fts.isOpen()) { fts = Search.getFullTextSession(getSession()); Local.put(IHibernateOgmDao.FULL_TEXT_SESSION_KEY, fts); log.debug("새로운 FullTextSession을 생성했습니다."); } return fts; } @Override public final QueryBuilder getQueryBuilder(Class<?> clazz) { return getFullTextSession().getSearchFactory().buildQueryBuilder().forEntity(clazz).get(); } @Override public FullTextQuery getFullTextQuery(Query luceneQuery, Class<?>... entities) { if (isTraceEnabled) log.trace("FullTextQuery를 생성합니다... luceneQuery=[{}], entities=[{}]", luceneQuery, StringTool.listToString(entities)); FullTextQuery ftq = getFullTextSession().createFullTextQuery(luceneQuery, entities); // 필수!!! object lookup 및 DB 조회 방법 설정 // ftq.initializeObjectsWith(ObjectLookupMethod.SKIP, DatabaseRetrievalMethod.FIND_BY_ID); return ftq; } @Override public <T> T get(Class<T> clazz, Serializable id) { return (T) getFullTextSession().get(clazz, id); } @Override public <T> List<T> find(Class<T> clazz) { return find(clazz, getQueryBuilder(clazz).all().createQuery()); } @Override public <T> List<T> find(Class<T> clazz, Query luceneQuery) { return find(clazz, luceneQuery, -1, -1, null, null); } @Override public <T> List<T> find(Class<T> clazz, Query luceneQuery, Sort luceneSort) { return find(clazz, luceneQuery, -1, -1, luceneSort, null); } @Override public <T> List<T> find(Class<T> clazz, Query luceneQuery, int firstResult, int maxResults, Sort luceneSort) { return find(clazz, luceneQuery, firstResult, maxResults, luceneSort, null); } @Override public <T> List<T> find(Class<T> clazz, Query luceneQuery, int firstResult, int maxResults, Sort luceneSort, Criteria criteria) { if (isTraceEnabled) log.trace("엔티티 조회. clazz=[{}], luceneQuery=[{}], fitstResult=[{}], maxResults=[{}], sort=[{}], criteria=[{}]", clazz, luceneQuery, firstResult, maxResults, luceneSort, criteria); if (luceneQuery == null) luceneQuery = getQueryBuilder(clazz).all().createQuery(); FullTextQuery ftq = getFullTextQuery(luceneQuery, clazz); HibernateTool.setPaging(ftq, firstResult, maxResults); if (luceneSort != null) ftq.setSort(luceneSort); if (criteria != null) ftq.setCriteriaQuery(criteria); return ftq.list(); } @Override public List<Serializable> findIds(Class<?> clazz, Query luceneQuery) { return findIds(clazz, luceneQuery, -1, -1, null, null); } @Override public List<Serializable> findIds(Class<?> clazz, Query luceneQuery, int firstResult, int maxResults, Sort luceneSort) { return findIds(clazz, luceneQuery, firstResult, maxResults, luceneSort, null); } @Override public List<Serializable> findIds(Class<?> clazz, Query luceneQuery, int firstResult, int maxResults, Sort luceneSort, Criteria criteria) { List<Object[]> fields = findProjections(clazz, luceneQuery, new String[] { ProjectionConstants.ID }, firstResult, maxResults, luceneSort, criteria); List<Serializable> ids = new ArrayList<>(); for (Object[] field : fields) { ids.add((Serializable) field[0]); } return ids; } @Override public List<Object[]> findProjections(Class<?> clazz, Query luceneQuery, String[] fields) { return findProjections(clazz, luceneQuery, fields, -1, -1, null, null); } @Override public List<Object[]> findProjections(Class<?> clazz, Query luceneQuery, String[] fields, Sort luceneSort) { return findProjections(clazz, luceneQuery, fields, -1, -1, luceneSort, null); } @Override public List<Object[]> findProjections(Class<?> clazz, Query luceneQuery, String[] fields, Sort luceneSort, Criteria criteria) { return findProjections(clazz, luceneQuery, fields, -1, -1, luceneSort, criteria); } @Override public List<Object[]> findProjections(Class<?> clazz, Query luceneQuery, String[] fields, int firstResult, int maxResults, Sort luceneSort, Criteria criteria) { shouldNotBeNull(fields, "fields"); Guard.shouldBe(fields.length > 0, "조회할 필드 수가 있어야합니다."); if (isTraceEnabled) log.trace("Project 조회. clazz=[{}], luceneQuery=[{}], fields=[{}], firstResult=[{}], maxResults=[{}], luceneSort=[{}], criteria=[{}]", clazz, luceneQuery, StringTool.listToString(fields), firstResult, maxResults, luceneSort, criteria); if (luceneQuery == null) luceneQuery = getQueryBuilder(clazz).all().createQuery(); FullTextQuery ftq = getFullTextQuery(luceneQuery, clazz); HibernateTool.setPaging(ftq, firstResult, maxResults); ftq.setProjection(fields); if (luceneSort != null) ftq.setSort(luceneSort); if (criteria != null) ftq.setCriteriaQuery(criteria); return ftq.list(); } @Override public <T> IPagedList<T> getPage(Class<T> clazz, Query luceneQuery, int pageNo, int pageSize) { return getPage(clazz, luceneQuery, pageNo, pageSize, null, null); } @Override public <T> IPagedList<T> getPage(Class<T> clazz, Query luceneQuery, int pageNo, int pageSize, Sort luceneSort) { return getPage(clazz, luceneQuery, pageNo, pageSize, luceneSort, null); } @Override public <T> IPagedList<T> getPage(Class<T> clazz, Query luceneQuery, int pageNo, int pageSize, Sort luceneSort, Criteria criteria) { log.trace("엔티티 페이징 조회. clazz=[{}], luceneQuery=[{}], pageNo=[{}], pageSize=[{}], sort=[{}], criteria=[{}]", clazz, luceneQuery, pageNo, pageSize, luceneSort, criteria); long totalCount = count(clazz, luceneQuery); FullTextQuery ftq = this.getFullTextQuery(luceneQuery, clazz); HibernateTool.setPaging(ftq, (pageNo - 1) * pageSize, pageSize); if (luceneSort != null) ftq.setSort(luceneSort); if (criteria != null) ftq.setCriteriaQuery(criteria); // fetching strategy 같은 겻을 제공할 수 있다. return new PaginatedList<T>(ftq.list(), pageNo, pageSize, totalCount); } @Override public IPagedList<Serializable> getIdPage(Class<?> clazz, Query luceneQuery, int pageNo, int pageSize) { return getIdPage(clazz, luceneQuery, pageNo, pageSize, null, null); } @Override public IPagedList<Serializable> getIdPage(Class<?> clazz, Query luceneQuery, int pageNo, int pageSize, Sort luceneSort) { return getIdPage(clazz, luceneQuery, pageNo, pageSize, luceneSort, null); } @Override public IPagedList<Serializable> getIdPage(Class<?> clazz, Query luceneQuery, int pageNo, int pageSize, Sort luceneSort, Criteria criteria) { IPagedList<Object[]> list = getProjectionPage(clazz, luceneQuery, new String[] { FullTextQuery.ID }, pageNo, pageSize, luceneSort, criteria); List<Serializable> ids = Lists.newArrayList(); for (Object[] fields : list.getList()) { ids.add((Serializable) fields[0]); } return new PaginatedList<>(ids, pageNo, pageSize, list.getItemCount()); } @Override public IPagedList<Object[]> getProjectionPage(Class<?> clazz, Query luceneQuery, String[] fields, int pageNo, int pageSize) { return getProjectionPage(clazz, luceneQuery, fields, pageNo, pageSize, null, null); } @Override public IPagedList<Object[]> getProjectionPage(Class<?> clazz, Query luceneQuery, String[] fields, int pageNo, int pageSize, Sort luceneSort) { return getProjectionPage(clazz, luceneQuery, fields, pageNo, pageSize, luceneSort, null); } @Override public IPagedList<Object[]> getProjectionPage(Class<?> clazz, Query luceneQuery, String[] fields, int pageNo, int pageSize, Sort luceneSort, Criteria criteria) { long count = this.count(clazz, luceneQuery); List<Object[]> projections = this.findProjections(clazz, luceneQuery, fields, (pageNo - 1) * pageSize, pageSize, luceneSort, criteria); return new PaginatedList<>(projections, pageNo, pageSize, count); } @Override public long count(Class<?> clazz) { return count(clazz, getQueryBuilder(clazz).all().createQuery(), null); } @Override public long count(Class<?> clazz, Query luceneQuery) { return count(clazz, luceneQuery, null); } @Override public long count(Class<?> clazz, Query luceneQuery, Criteria criteria) { FullTextQuery ftq = getFullTextQuery(luceneQuery, clazz); if (criteria != null) ftq.setCriteriaQuery(criteria); int count = ftq.getResultSize(); if (isTraceEnabled) log.trace("entity counting. entity=[{}], query=[{}], count=[{}]", clazz.getSimpleName(), luceneQuery, count); return count; } @Override public boolean exists(Class<?> clazz) { return exists(clazz, getQueryBuilder(clazz).all().createQuery()); } @Override public boolean exists(Class<?> clazz, Query luceneQuery) { List<Serializable> ids = findIds(clazz, luceneQuery, 0, 1, null); return (ids != null && ids.size() > 0); } @Override public void persist(Object entity) { getFullTextSession().persist(entity); } @Override public Object merge(Object entity) { return getFullTextSession().merge(entity); } @Override public Serializable save(Object entity) { return getFullTextSession().save(entity); } @Override public void saveOrUpdate(Object entity) { getFullTextSession().saveOrUpdate(entity); } @Override public void update(Object entity) { getFullTextSession().update(entity); } @Override public void delete(Object entity) { if (entity == null) return; getFullTextSession().delete(entity); } @Override public void deleteById(Class<?> clazz, Serializable id) { Object entity = getFullTextSession().load(clazz, id); delete(entity); } @Override public void deleteByIds(Class<?> clazz, Collection<? extends Serializable> ids) { if (isTraceEnabled) log.trace("Id 컬렉션에 해당하는 엔티티들을 삭제합니다. clazz=[{}], ids=[{}]", clazz, StringTool.listToString(ids)); FullTextSession fts = getFullTextSession(); for (Serializable id : ids) delete(fts.load(clazz, id)); } @Override public void deleteAll(Class<?> clazz) { deleteAll(clazz, getQueryBuilder(clazz).all().createQuery()); } @Override public void deleteAll(Class<?> clazz, Query luceneQuery) { deleteByIds(clazz, findIds(clazz, luceneQuery)); } @Override public void deleteAll(Collection<?> entities) { if (ArrayTool.isEmpty(entities)) return; if (isDebugEnabled) log.debug("엔티티 컬렉션을 모두 삭제합니다... entity count=[{}]", entities.size()); FullTextSession fts = getFullTextSession(); for (Object entity : entities) { fts.delete(entity); } } @Override public void purge(Class<?> clazz, Serializable id) { if (isTraceEnabled) log.trace("인덱스를 제거합니다. clazz=[{}], id=[{}]", clazz, id); getFullTextSession().purge(clazz, id); } @Override public void purgeAll(Class<?> clazz) { log.info("지정된 수형의 모든 엔티티들의 인덱스를 제거합니다. clazz=[{}]", clazz); getFullTextSession().purgeAll(clazz); } @Override public void flushToIndexes() { getFullTextSession().flushToIndexes(); } @Override public <T> void index(T entity) { shouldNotBeNull(entity, "entity"); if (isTraceEnabled) log.trace("수동으로 재 인덱스를 수행합니다. entity=[{}]", entity); getFullTextSession().index(entity); } @Override public void indexAll(Class<?> clazz, int batchSize) { if (isDebugEnabled) log.debug("수형[{}]의 모든 엔티티에 대해 인덱싱을 수행합니다...", clazz); clearIndex(clazz); if (batchSize < DEFAUALT_BATCH_SIZE) batchSize = DEFAUALT_BATCH_SIZE; FullTextSession fts = getFullTextSession(); FlushMode currentFlushMode = fts.getFlushMode(); CacheMode currentCacheMode = fts.getCacheMode(); fts.setFlushMode(FlushMode.MANUAL); fts.setCacheMode(CacheMode.IGNORE); try { Transaction tx = fts.beginTransaction(); ScrollableResults results = fts.createCriteria(clazz).scroll(ScrollMode.FORWARD_ONLY); int index = 0; while (results.next()) { fts.index(results.get(0)); if (++index % batchSize == 0) { fts.flushToIndexes(); fts.clear(); if (isTraceEnabled) log.trace("인덱싱 수행중입니다. index=[{}]", index); } } fts.flushToIndexes(); tx.commit(); log.info("수형[{}]의 모든 엔티티 [{}]개 대해 재 인덱싱을 수행했습니다!!!", clazz, index); } finally { fts.setFlushMode(currentFlushMode); fts.setCacheMode(currentCacheMode); } } @Override public Future<Void> indexAllAsync(final Class<?> clazz, final int batchSize) { if (isDebugEnabled) log.debug("비동기 방식으로 엔티티에 대해 인덱싱을 수행합니다... clazz=[{}], batchSize=[{}]", clazz, batchSize); // TODO: Session이 Thread-safe 하지 않으므로, 새로운 Thread를 만들면 안됩니다. indexAll(clazz, batchSize); return AsyncTool.getTaskHasResult(null); // final FullTextSession fts = getFullTextSession(); // return AsyncTool.startNew(new Callable<Void>() { // @Override // public Void call() { // try { // indexAll(clazz, batchSize); // return null; // } finally { // fts.close(); // } // } // }); } @Override public void clearIndex(Class<?> clazz) { if (isDebugEnabled) log.debug("엔티티에 대한 모든 인덱스 정보를 삭제합니다... clazz=[{}]", clazz); getFullTextSession().purgeAll(clazz); // remove obsolete index getFullTextSession().flushToIndexes(); // apply purge before optimize optimize(clazz); // physically clear space } @Override public void clearIndexAll() { log.info("모든 엔티티에 대해 모든 인덱스 정보를 삭제합니다..."); FullTextSession fts = getFullTextSession(); for (Class clazz : SearchTool.getIndexedClasses(fts.getSessionFactory())) { fts.purgeAll(clazz); fts.flushToIndexes(); } optimizeAll(); } @Override public void optimize(Class<?> clazz) { log.info("지정된 수형의 인덱스를 최적화합니다. clazz=[{}]", clazz); getFullTextSession().getSearchFactory().optimize(clazz); } @Override public void optimizeAll() { log.info("모든 수형의 인덱스를 최적화합니다."); getFullTextSession().getSearchFactory().optimize(); } @Override public void flush() { if (isDebugEnabled) log.debug("세션의 모든 변경 정보를 저장소에 적용합니다..."); getFullTextSession().flush(); } @Override public void flushIndexes() { if (isDebugEnabled) log.debug("세션의 모든 인덱스 변경 정보를 저장합니다..."); getFullTextSession().flushToIndexes(); } }