`

hibernate dao

阅读更多
[size=large]有分页功能的 HibernateDao


package org.mysterylab.utopiaframework.core.orm.hibernate3;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Disjunction;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.transform.ResultTransformer;
import org.mysterylab.utopiaframework.core.orm.Page;
import org.mysterylab.utopiaframework.core.orm.PropertyFilter;
import org.mysterylab.utopiaframework.core.orm.PropertyFilter.MatchType;
import org.mysterylab.utopiaframework.util.reflect.ReflectionUtil;
import org.springframework.util.Assert;

/**
 * 在 {@link BaseHibernateDao} 的基础上加入了如下特性:
 * <ul>
 * 	<li>1) 分页查询.</li>
 * 	<li>2) 按属性过滤条件列表查询.</li>
 * </ul>
 *
 * @param <T>
 *            DAO 操作的对象类型
 * @param <PK>
 *            主键类型
 *            
 * @author 
 * @see PropertyFilter
 */
public class HibernateDao<T, PK extends Serializable> extends BaseHibernateDao<T, PK> {

	/**
	 * 通过子类的泛型定义获得对象的类型.<br>
	 * 比如: 
	 * <pre>
	 * {@code
	 *  public class UserDao extends HibernateDao<User, Long>
	 * }
	 * </pre>
	 */
	public HibernateDao() {
		super();
	}
	
	/**
	 * 跳过 DAO 层, 直接在 Service 层使用 BaseHibernateDao 的构造函数,
	 * 在构造函数中定义对象类型.
	 * <pre>
	 * {@code
	 *   HibernateDao<User, Long> userDao = new HibernateDao<User, Long>(sessionFactory, User.class);
	 * }
	 * </pre>
	 * @param sessionFactory
	 * @param entityClass
	 */
	public HibernateDao(final SessionFactory sessionFactory,
			final Class<T> entityClass) {
		super(sessionFactory, entityClass);
	}
	
	/**
	 * 分页获取全部对象.
	 * 
	 * @param page
	 * @return
	 */
	public Page<T> getAll(final Page<T> page) {
		return findPage(page);
	}

	/**
	 * 按 HQL 分页查询.
	 * 
	 * @param page
	 *            分页参数. 注意不支持其中的 orderBy 参数
	 * @param hql
	 *            hql 语句
	 * @param values
	 *            数量可变的查询参数,按顺序绑定
	 * 
	 * @return 分页查询结果, 附带结果列表及所有查询输入参数
	 */
	@SuppressWarnings("unchecked")
	public Page<T> findPage(final Page<T> page, final String hql,
			final Object... values) {
		
		Assert.notNull(page, "page can not be null");
		Query query = createQuery(hql, values);
		if (page.isAutoCount()) {
			long totalCount = countHqlResult(hql, values);
			page.setTotalCount(totalCount);
		}
		setPageParameterToQuery(query, page);
		
		List result = query.list();
		page.setResult(result);
		return page;
	}
	
	/**
	 * 按 HQL 分页查询.
	 * 
	 * @param page
	 *            分页参数. 注意不支持其中的 orderBy 参数
	 * @param hql
	 *            hql 语句
	 * @param values
	 *            命名参数, 按名称绑定
	 * 
	 * @return 分页查询结果, 附带结果列表及所有查询输入参数
	 */
	@SuppressWarnings("unchecked")
	public Page<T> findPage(final Page<T> page, final String hql,
			final Map<String, ?> values) {
		
		Assert.notNull(page, "page can not be null");
		Query query = createQuery(hql, values);
		if (page.isAutoCount()) {
			long totalCount = countHqlResult(hql, values);
			page.setTotalCount(totalCount);
		}
		setPageParameterToQuery(query, page);

		List result = query.list();
		page.setResult(result);
		return page;
	}
	
	/**
	 * 按 Criteria 分页查询.
	 * 
	 * @param page
	 *            分页参数.
	 * @param criterions
	 *            数量可变的 Criterion
	 * 
	 * @return 分页查询结果.附带结果列表及所有查询输入参数.
	 */
	@SuppressWarnings("unchecked")
	public Page<T> findPage(final Page<T> page, final Criterion... criterions) {
		Assert.notNull(page, "page can not be null");

		Criteria criteria = createCriteria(criterions);
		if (page.isAutoCount()) {
			long totalCount = countCriteriaResult(criteria);
			page.setTotalCount(totalCount);
		}
		setPageParameterToCriteria(criteria, page);

		List result = criteria.list();
		page.setResult(result);
		return page;
	}
	
	/**
	 * 设置分页参数到 Query 对象, 辅助函数.
	 * 
	 * @param query
	 * @param page
	 * @return
	 */
	protected Query setPageParameterToQuery(final Query query, final Page<T> page) {

		Assert.isTrue(page.getPageSize() > 0, "Page#pageSize must larger than 0");

		// 注意 Hibernate 的 firstResult 的序号从 0 开始
		query.setFirstResult(page.getFirst() - 1);
		query.setMaxResults(page.getPageSize());
		return query;
	}
	
	/**
	 * 设置分页参数到 Criteria 对象, 辅助函数.
	 * 
	 * @param criteria
	 * @param page
	 * @return
	 */
	protected Criteria setPageParameterToCriteria(final Criteria criteria,
			final Page<T> page) {

		Assert.isTrue(page.getPageSize() > 0, "Page Size must larger than zero");

		// 注意 Hibernate 的 firstResult 的序号从 0 开始
		criteria.setFirstResult(page.getFirst() - 1);
		criteria.setMaxResults(page.getPageSize());

		if (page.isOrderBySetted()) {
			String[] orderByArray = StringUtils.split(page.getOrderBy(), ',');
			String[] orderArray = StringUtils.split(page.getOrder(), ',');

			Assert.isTrue(orderByArray.length == orderArray.length,
					"分页多重排序参数中, 排序字段与排序方向的个数不相等");

			for (int i = 0; i < orderByArray.length; i++) {
				if (Page.ASC.equals(orderArray[i])) {
					criteria.addOrder(Order.asc(orderByArray[i]));
				} else {
					criteria.addOrder(Order.desc(orderByArray[i]));
				}
			}
		}
		return criteria;
	}
	
	/**
	 * 执行 count 查询获得本次 Hql 查询所能获得的对象总数.<br>
	 * 注意本函数只能自动处理简单的 hql 语句, 复杂的 hql 查询请另行编写 count 语句查询.
	 * 
	 * @param hql
	 * @param values
	 * @return
	 */
	protected long countHqlResult(final String hql, final Object... values) {
		String countHql = prepareCountHql(hql);

		try {
			Long count = findUnique(countHql, values);
			return count;
		} catch (Exception e) {
			throw new RuntimeException("hql can't be auto count, hql is:"
					+ countHql, e);
		}
	}
	
	/**
	 * 执行 count 查询获得本次 Hql 查询所能获得的对象总数.<br>
	 * 注意本函数只能自动处理简单的 hql 语句, 复杂的 hql 查询请另行编写 count 语句查询.
	 * 
	 * @param hql
	 * @param values
	 * @return
	 */
	protected long countHqlResult(final String hql, final Map<String, ?> values) {
		String countHql = prepareCountHql(hql);

		try {
			Long count = findUnique(countHql, values);
			return count;
		} catch (Exception e) {
			throw new RuntimeException("hql can't be auto count, hql is:"
					+ countHql, e);
		}
	}
	
	/**
	 * 为 count 查询获得 Hql 查询获得的对象总数做预处理.
	 * 
	 * @param orgHql
	 * @return
	 */
	private String prepareCountHql(String orgHql) {
		String fromHql = orgHql;
		/*
		 * select 子句与 order by 子句会影响 count 查询, 进行简单的排除. 比如:
		 * select name from order as o order by o.number asc -> from order as o
		 */
		fromHql = "from " + StringUtils.substringAfter(fromHql, "from");
		fromHql = StringUtils.substringBefore(fromHql, "order by");

		String countHql = "select count(*) " + fromHql;
		return countHql;
	}
	
	/**
	 * 执行 count 查询获得本次 Criteria 查询所能获得的对象总数.
	 * 
	 * @param criteria
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected long countCriteriaResult(final Criteria criteria) {
		CriteriaImpl impl = (CriteriaImpl) criteria;

		// 先把 Projection、ResultTransformer、OrderBy 取出来, 清空三者后再执行 Count 操作
		Projection projection = impl.getProjection();
		ResultTransformer transformer = impl.getResultTransformer();

		List<CriteriaImpl.OrderEntry> orderEntries = null;
		try {
			orderEntries = (List) ReflectionUtil.getFieldValue(impl, "orderEntries");
			ReflectionUtil.setFieldValue(impl, "orderEntries", new ArrayList());
		} catch (Exception e) {
			logger.error("exception could not be happened", e.getMessage());
		}

		// 执行 Count 查询
		Long totalCountObject = (Long) criteria.setProjection(Projections.rowCount())
				.setMaxResults(1).uniqueResult();
		long totalCount = (totalCountObject != null) ? totalCountObject : 0;

		// 将之前的 Projection, ResultTransformer 和 OrderBy 条件重新设回去
		criteria.setProjection(projection);

		if (projection == null) {
			criteria.setResultTransformer(CriteriaSpecification.ROOT_ENTITY);
		}
		if (transformer != null) {
			criteria.setResultTransformer(transformer);
		}
		try {
			ReflectionUtil.setFieldValue(impl, "orderEntries", orderEntries);
		} catch (Exception e) {
			logger.error("exception could not be happened", e.getMessage());
		}

		return totalCount;
	}
	
	/**
	 * 按属性查找对象列表, 支持多种匹配方式.
	 * 
	 * @param matchType
	 *            匹配方式, 目前支持的取值见 {@link PropertyFilter} 的 MatcheType enum
	 */
	public List<T> findBy(final String propertyName, final Object value,
			final MatchType matchType) {
		Criterion criterion = buildCriterion(propertyName, value, matchType);
		return find(criterion);
	}
	
	/**
	 * 按属性过滤条件列表查找对象列表.
	 * 
	 * @param filters
	 * @return
	 */
	public List<T> find(List<PropertyFilter> filters) {
		Criterion[] criterions = buildCriterionByPropertyFilter(filters);
		return find(criterions);
	}
	
	/**
	 * 按属性过滤条件列表分页查找对象.
	 * 
	 * @param page
	 * @param filters
	 * @return
	 */
	public Page<T> findPage(final Page<T> page, final List<PropertyFilter> filters) {
		Criterion[] criterions = buildCriterionByPropertyFilter(filters);
		return findPage(page, criterions);
	}
	
	/**
	 * 按属性条件参数创建 Criterion, 辅助函数.
	 * 
	 * @param propertyName
	 * @param propertyValue
	 * @param matchType
	 * @return
	 */
	protected Criterion buildCriterion(final String propertyName,
			final Object propertyValue, final MatchType matchType) {
		Assert.hasText(propertyName, "propertyName can not be null");
		Criterion criterion = null;
		// 根据 MatchType 构造 criterion
		switch (matchType) {
		
			case EQ:
				criterion = Restrictions.eq(propertyName, propertyValue);
				break;
				
			case LIKE:
				criterion = Restrictions.like(propertyName, (String) propertyValue,
						MatchMode.ANYWHERE);
				break;

			case LE:
				criterion = Restrictions.le(propertyName, propertyValue);
				break;
				
			case LT:
				criterion = Restrictions.lt(propertyName, propertyValue);
				break;
				
			case GE:
				criterion = Restrictions.ge(propertyName, propertyValue);
				break;
				
			case GT:
				criterion = Restrictions.gt(propertyName, propertyValue);
		}
		return criterion;
	}
	
	/**
	 * 按属性条件列表创建 Criterion 数组, 辅助函数.
	 * 
	 * @param filters
	 * @return
	 */
	protected Criterion[] buildCriterionByPropertyFilter(
			final List<PropertyFilter> filters) {
		List<Criterion> criterionList = new ArrayList<Criterion>();
		for (PropertyFilter filter : filters) {
			if (!filter.hasMultiProperties()) {
				// 只有一个属性需要比较的情况
				Criterion criterion = buildCriterion(filter.getPropertyName(),
						filter.getMatchValue(), filter.getMatchType());
				criterionList.add(criterion);
			} else {
				// 包含多个属性需要比较的情况,进行or处理
				Disjunction disjunction = Restrictions.disjunction();
				for (String param : filter.getPropertyNames()) {
					Criterion criterion = buildCriterion(param, filter
							.getMatchValue(), filter.getMatchType());
					disjunction.add(criterion);
				}
				criterionList.add(disjunction);
			}
		}
		return criterionList.toArray(new Criterion[criterionList.size()]);
	}
}


BaseHibernateDao

package org.mysterylab.utopiaframework.core.orm.hibernate3;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.persistence.NonUniqueResultException;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.mysterylab.utopiaframework.util.reflect.ReflectionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;

/**
 * 封装了 Hibernate3 原生 API 的 DAO 泛型基类. 通过 Hibernate 来操纵对象, 主要是 {@link Session}
 * 的一些方法的二次封装和 HQL 与 QBC 的一些简单检索.
 * 
 * @author
 * @param <T>
 *            DAO 操作的对象类型
 * @param <PK>
 *            主键类型
 */
public class BaseHibernateDao<T, PK extends Serializable> {

	protected Logger logger = LoggerFactory.getLogger(getClass());
	
	protected SessionFactory sessionFactory;
	
	protected Class<T> entityClass;
	
	/**
	 * 通过子类的泛型定义获得对象的类型. 比如:
	 * <pre>
	 * {@code
	 *  public class UserDao extends BaseHibernateDao<User, Long>
	 * }
	 * </pre>
	 */
	public BaseHibernateDao() {
		this.entityClass = ReflectionUtil.getSuperClassGenericType(getClass());
	}
	
	/**
	 * 跳过 DAO 层, 直接在 Service 层使用 BaseHibernateDao 的构造函数,
	 * 在构造函数中定义对象类型.
	 * <pre>
	 * {@code
	 * 	BaseHibernateDao<User, Long> userDao = new BaseHibernateDao<User, Long>(sessionFactory, User.class);
	 * }
	 * </pre>
	 * @param sessionFactory
	 * @param entityClass
	 */
	public BaseHibernateDao(final SessionFactory sessionFactory,
			final Class<T> entityClass) {
		this.sessionFactory = sessionFactory;
		this.entityClass = entityClass;
	}

	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	/**
	 * 当配置有 sessionFactory 的时候采用 Spring 的自动注入.
	 * 
	 * @param sessionFactory
	 */
	@Autowired
	public void setSessionFactory(final SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}
	
	/**
	 * 获取 Hibernate 的 Session. 注意在使用 {@link SessionFactory#getCurrentSession()}
	 * 方法的时候, Hibernate 的操作默认必须包含在一个 Transaction 中. 如果这些操作 CRUD
	 * 操作不被包含在一个具体的事务中, 则会抛出如下异常:
	 * <pre>
	 * org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here
	 * </pre>
	 * 这里顺便说下  {@link SessionFactory#openSession()} 方法与其的区别: opensSession()
	 * 得到一个新的 session, 而 getCurrentSession() 则是从当前线程中得到事务开始时创建的
	 * Transaction 的那个 session, 如果事务没有正确启动的话, 就没有一个 session 被绑定到当前线程
	 * 
	 * @return
	 */
	public Session getSession() {
		return sessionFactory.getCurrentSession();
	}
	
	/**
	 * 取得对象的主键名.
	 * 
	 * @return
	 */
	public String getIdName() {
		ClassMetadata meta = getSessionFactory().getClassMetadata(entityClass);
		return meta.getIdentifierPropertyName();
	}

	/**
	 * 按 id 获取对象. 采用 {@link Session#load(Class, Serializable)} 方法在数据库中不存在与
	 * OID 对应的记录的时候, 会抛出 {@link ObjectNotFoundException} 的异常. 采用 get()
	 * 还是采用 load() 方法的区别在于前者是立即加载, 而后者是延迟加载, 使用的场合有:
	 * <ul>
	 * 	<li>1) 如果加载一个对象的目的是为了访问它的各个属性, 用 get() 方法.</li>
	 * 	<li>2) 如果加载一个对象的目的是为了删除它, 或者是为了建立与别的对象的关联关系, 用 load() 方法.</li>
	 * </ul>
	 * 比如以下代码:
	 * <pre>
	 *  Transaction tx = session.beginTransaction();
	 *  // 立即检索策略
	 *  Order order = (Order) session.get(Order.class, new Long(1));
	 *  // 延迟检索策略
	 *  Customer customer = (Customer) session.load(Customer.class, new Long(1));
	 *  // 建立 Ordre 与 Customer 的多对一单向关联关系
	 *  order.setCustomer(customer);
	 *  tx.commit();
	 *  // Session 不需要知道 Customer 的各个属性的值, 而只要知道 Customer 对象的 OID 即可生成如下 sql 语句:
	 *  // update ORDER set CUSTOMER_ID=1, ORDER_NUMBER=... where ID=1;
	 * </pre>
	 * 
	 * @param id
	 *            主键
	 * @return
	 * @see #get(Serializable)
	 * @see #delete(Serializable)
	 */
	@SuppressWarnings("unchecked")
	public T load(final PK id) {
		Assert.notNull(id, "id can not be null");
		return (T) getSession().load(entityClass, id);
	}
	
	/**
	 * 按 id 获取对象. 采用 {@link Session#get(Class, Serializable)} 方法在数据库不存在与
	 * OID 对应的记录的时候, 会返回 null.
	 * 
	 * @param id
	 *            主键
	 * @return
	 * @see #load(Serializable)
	 */
	@SuppressWarnings("unchecked")
	public T get(final PK id) {
		Assert.notNull(id, "id can not be null");
		return (T) getSession().get(entityClass, id);
	}
	
	/**
	 * 按 id 列表获取对象列表.
	 * 
	 * @param ids
	 * @return
	 */
	public List<T> get(final Collection<PK> ids) {
		return find(Restrictions.in(getIdName(), ids));
	}
	
	/**
	 * 获取全部对象.
	 * 
	 * @return
	 */
	public List<T> getAll() {
		return find();
	}
	
	/**
	 * 获取全部对象, 支持按属性排序. 采用 QBC 的检索方式.
	 * 
	 * @param orderByProperty
	 * @param isAsc
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getAll(String orderByProperty, boolean isAsc) {
		Criteria c = createCriteria();
		if (isAsc) {
			c.addOrder(Order.asc(orderByProperty));
		} else {
			c.addOrder(Order.desc(orderByProperty));
		}
		return c.list();
	}
	
	/**
	 * 按属性查找对象列表, 匹配方式为相等. 采用 QBC 的检索方式.
	 * 
	 * @param propertyName
	 * @param value
	 * @return
	 */
	public List<T> findBy(final String propertyName, final Object value) {
		Assert.hasText(propertyName, "propertyName can not be null");
		Criterion criterion = Restrictions.eq(propertyName, value);
		return find(criterion);
	}
	
	/**
	 * 采用 QBC 的检索方式查询对象列表.
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	@SuppressWarnings("unchecked")
	public List<T> find(final Criterion... criterions) {
		return createCriteria(criterions).list();
	}
	
	/**
	 * 采用 QBC 的检索方式查询唯一对象. 可能有以下几种情况:
	 * <ul>
	 * 	<li>1) 如果有多个值抛 {@link NonUniqueResultException} 异常, 需要用 setMaxResults(1) 方法来限制.</li>
	 * 	<li>2) 如果有值且只有一个, 返回一个 Object.</li>
	 * 	<li>3) 如果没值, 返回 null.</li>
	 * </ul>
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	@SuppressWarnings("unchecked")
	public T findUnique(final Criterion... criterions) {
		return (T) createCriteria(criterions).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 按属性查找唯一对象, 匹配方式为相等. 采用 QBC 的检索方式.
	 * 
	 * @param propertyName
	 * @param value
	 * @return
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public T findUniqueBy(final String propertyName, final Object value) {
		Assert.hasText(propertyName, "propertyName can not be null");
		Criterion criterion = Restrictions.eq(propertyName, value);
		return (T) createCriteria(criterion).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 采用 HQL 的检索方式查询对象列表.
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 */
	@SuppressWarnings("unchecked")
	public <X> List<X> find(final String hql, final Object... values) {
		return createQuery(hql, values).list();
	}
	
	/**
	 * 采用 HQL 的检索方式查询对象列表.
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 */
	@SuppressWarnings("unchecked")
	public <X> List<X> find(final String hql, final Map<String, ?> values) {
		return createQuery(hql, values).list();
	}
	
	/**
	 * 采用 HQL 的检索方式查询唯一对象.
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public <X> X findUnique(final String hql, final Object... values) {
		return (X) createQuery(hql, values).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 采用 HQL 的检索方式查询唯一对象.
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public <X> X findUnique(final String hql, final Map<String, ?> values) {
		return (X) createQuery(hql, values).setMaxResults(1).uniqueResult();
	}

	/**
	 * 保存新增或修改的对象.
	 * 
	 * @param entity
	 *            对象
	 */
	public void save(final T entity) {
		Assert.notNull(entity, "entity can not be null");
		getSession().saveOrUpdate(entity);
		logger.debug("save entity: {}", entity);
	}
	
	/**
	 * 删除对象.
	 * 
	 * @param entity
	 *            对象必须是 session 中的对象或含 id 属性的 transient 对象
	 */
	public void delete(final T entity) {
		Assert.notNull(entity, "entity can not be null");
		getSession().delete(entity);
		logger.debug("delete entity: {}", entity);
	}

	/**
	 * 按 id 删除对象.
	 * 
	 * @param id
	 *            主键
	 * @see #load(Serializable)
	 */
	public void delete(final PK id) {
		Assert.notNull(id, "id can not be null");
		delete(load(id));
		logger.debug("delete entity {},id is {}", entityClass.getSimpleName(), id);
	}
	
	/**
	 * 采用 HQL 的检索方式进行批量修改/删除操作.
	 * 
	 * @param values
	 *            数量可变的参数,按顺序绑定
	 * @return 更新记录数.
	 */
	public int batchExecute(final String hql, final Object... values) {
		return createQuery(hql, values).executeUpdate();
	}

	/**
	 * 采用 HQL 的检索方式进行批量修改/删除操作.
	 * 
	 * @param values
	 *            命名参数,按名称绑定
	 * @return 更新记录数.
	 */
	public int batchExecute(final String hql, final Map<String, ?> values) {
		return createQuery(hql, values).executeUpdate();
	}
	
	/**
	 * 根据查询 HQL 与参数列表创建 Query 对象. 与 find() 函数可进行更加灵活的操作. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Order o where o.customer=":customer and "
	 *      + "o.orderNumber like :orderNumber");
	 *  query.setParameter(1, customer);
	 *  query.setParameter(2, orderNumber);
	 * </pre>
	 * 上面的程序默认是使用了 Hibernate 的自动根据参数值的 Java 类型来进行对应的映射类型,
	 * 这样可以减少在第三个参数中指定 Java 类型的麻烦. 但是对于日期的 java.util.Date 类型,
	 * 会对应多种 Hibernate 映射类型, 如 Hibernate.DATE 或 Hibernate.TIMESTAMP,
	 * 因此必须在 setParameter() 方法中显式地指定到底对应那种 Hibernate 映射类型. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Customer c where c.birthday=:birthday");
	 *  query.setParameter("birthday", birthday, Hibernate.DATE);
	 * </pre>
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 */
	public Query createQuery(final String queryString, final Object... values) {
		Assert.hasText(queryString, "queryString can not be null");
		Query query = getSession().createQuery(queryString);
		if (values != null) {
			for (int i = 0; i < values.length; i++) {
				// 按顺序绑定参数
				query.setParameter(i, values[i]);
			}
		}
		return query;
	}
	
	/**
	 * 根据查询 HQL 与参数列表创建 Query 对象. 与 find() 函数可进行更加灵活的操作. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Customer as c where c.name=:name "
	 *      + "and c.age=:age";
	 *  Customer customer = new Customer();
	 *  customer.setName("Tom");
	 *  customer.setAge(21);
	 *  // 命名参数中的 "name" 和 "age" 分别对应 Customer 类的 name 属性和 age 属性, 否则会抛异常
	 *  query.setProperties(customer);
	 * </pre>
	 * 在本方法中采用的是传递一个 {@link Map} 作为参数, 同样会根据 Map 的名来进行自动绑定. 另外,
	 * setProperties() 方法调用 setParameter() 方法, setParameter() 方法再根据 Customer
	 * 对象的属性的 Java 类型来判断 Hibernate 映射类型. 如果命名参数为日期类型, 则不能通过
	 * setProperties() 方法来绑定. 另外, 参数绑定对 null 是安全的, 比如如下代码不会抛异常:
	 * <pre>
	 *  String name = null;
	 *  session.createQuery("from Customer c where c.name=:name").setString("name", name).list();
	 * </pre>
	 * 上面的查询语句对应的 SQL 语句是: 
	 * <pre>
	 *  select * from CUSTOMERS where NMAE=null;
	 * </pre>
	 * 这条查询语句的查询结果永远为空. 如果要查询名字为 null 的客户, 应该使用 "is null"
	 * 比较运算符, 比如:
	 * <pre>
	 *  // HQL 检索方式
	 *  session.createQuery("from Customer c where c.name is null");
	 *  // QBC 检索方式
	 *  session.createCriteria(Customer.class).add(Restrictions.isNull("name"));
	 * </pre>
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 * @see #find(String, Object...)
	 */
	public Query createQuery(final String queryString, final Map<String, ?> values) {
		Assert.hasText(queryString, "queryString can not be null");
		Query query = getSession().createQuery(queryString);
		if (values != null) {
			// 把命名参数与对象的属性值进行绑定
			query.setProperties(values);
		}
		return query;
	}
	
	/**
	 * 根据 Criterion 条件创建 Criteria. 与 find() 函数可进行更加灵活的操作.
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	public Criteria createCriteria(final Criterion... criterions) {
		Criteria criteria = getSession().createCriteria(entityClass);
		for (Criterion c : criterions) {
			criteria.add(c);
		}
		return criteria;
	}
	
	/**
	 * 初始化对象. 使用 load() 方法得到的仅是对象 Proxy, 在传到 View 层前需要进行初始化.
	 * 如果传入 entity, 则只初始化 entity 的直接属性, 但不会初始化延迟加载的关联集合和属性.
	 * 如需初始化关联属性,需执行:
	 * <pre>
	 * Hibernate.initialize(user.getRoles())        // 初始化User的直接属性和关联集合
	 * Hibernate.initialize(user.getDescription())  // 初始化User的直接属性和延迟加载的Description属性
	 * </pre>
	 */
	public void initProxyObject(Object proxy) {
		Hibernate.initialize(proxy);
	}
	
	/**
	 * 为 Query 添加 distinct transformer. 预加载关联对象的 HQL 会引起主对象重复,
	 * 需要进行 distinct 处理.
	 * 
	 * @param query
	 * @return
	 */
	public Query distinct(Query query) {
		query.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
		return query;
	}
	
	/**
	 * 为 Criteria 添加 distinct transformer. 预加载关联对象的 HQL 会引起主对象重复,
	 * 需要进行 distinct 处理.
	 * 
	 * @param criteria
	 * @return
	 */
	public Criteria distinct(Criteria criteria) {
		criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
		return criteria;
	}
	
	/**
	 * 判断对象的属性值在数据库内是否唯一.<br>
	 * 在修改对象的情景下, 如果属性新修改的值 (value) 等于属性原来的值 (orgValue) 则不作比较.
	 * 
	 * @param propertyName
	 * @param newValue
	 * @param oldValue
	 * @return
	 */
	public boolean isPropertyUnique(final String propertyName,
			final Object newValue, final Object oldValue) {
		if (newValue == null || newValue.equals(oldValue)) {
			return true;
		}
		Object object = findUniqueBy(propertyName, newValue);
		return (object == null);
	}
	
	/**
	 * 清除当前 Session.
	 */
	public void flush() {
		getSession().flush();
	}
}


Dao使用说明

package org.mysterylab.utopiaframework.core.orm.hibernate3;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.persistence.NonUniqueResultException;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.mysterylab.utopiaframework.util.reflect.ReflectionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;

/**
 * 封装了 Hibernate3 原生 API 的 DAO 泛型基类. 通过 Hibernate 来操纵对象, 主要是 {@link Session}
 * 的一些方法的二次封装和 HQL 与 QBC 的一些简单检索.
 * 
 * @author 
 * @param <T>
 *            DAO 操作的对象类型
 * @param <PK>
 *            主键类型
 */
public class BaseHibernateDao<T, PK extends Serializable> {

	protected Logger logger = LoggerFactory.getLogger(getClass());
	
	protected SessionFactory sessionFactory;
	
	protected Class<T> entityClass;
	
	/**
	 * 通过子类的泛型定义获得对象的类型. 比如:
	 * <pre>
	 * {@code
	 *  public class UserDao extends BaseHibernateDao<User, Long>
	 * }
	 * </pre>
	 */
	public BaseHibernateDao() {
		this.entityClass = ReflectionUtil.getSuperClassGenericType(getClass());
	}
	
	/**
	 * 跳过 DAO 层, 直接在 Service 层使用 BaseHibernateDao 的构造函数,
	 * 在构造函数中定义对象类型.
	 * <pre>
	 * {@code
	 * 	BaseHibernateDao<User, Long> userDao = new BaseHibernateDao<User, Long>(sessionFactory, User.class);
	 * }
	 * </pre>
	 * @param sessionFactory
	 * @param entityClass
	 */
	public BaseHibernateDao(final SessionFactory sessionFactory,
			final Class<T> entityClass) {
		this.sessionFactory = sessionFactory;
		this.entityClass = entityClass;
	}

	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	/**
	 * 当配置有 sessionFactory 的时候采用 Spring 的自动注入.
	 * 
	 * @param sessionFactory
	 */
	@Autowired
	public void setSessionFactory(final SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}
	
	/**
	 * 获取 Hibernate 的 Session. 注意在使用 {@link SessionFactory#getCurrentSession()}
	 * 方法的时候, Hibernate 的操作默认必须包含在一个 Transaction 中. 如果这些操作 CRUD
	 * 操作不被包含在一个具体的事务中, 则会抛出如下异常:
	 * <pre>
	 * org.hibernate.HibernateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here
	 * </pre>
	 * 这里顺便说下  {@link SessionFactory#openSession()} 方法与其的区别: opensSession()
	 * 得到一个新的 session, 而 getCurrentSession() 则是从当前线程中得到事务开始时创建的
	 * Transaction 的那个 session, 如果事务没有正确启动的话, 就没有一个 session 被绑定到当前线程
	 * 
	 * @return
	 */
	public Session getSession() {
		return sessionFactory.getCurrentSession();
	}
	
	/**
	 * 取得对象的主键名.
	 * 
	 * @return
	 */
	public String getIdName() {
		ClassMetadata meta = getSessionFactory().getClassMetadata(entityClass);
		return meta.getIdentifierPropertyName();
	}

	/**
	 * 按 id 获取对象. 采用 {@link Session#load(Class, Serializable)} 方法在数据库中不存在与
	 * OID 对应的记录的时候, 会抛出 {@link ObjectNotFoundException} 的异常. 采用 get()
	 * 还是采用 load() 方法的区别在于前者是立即加载, 而后者是延迟加载, 使用的场合有:
	 * <ul>
	 * 	<li>1) 如果加载一个对象的目的是为了访问它的各个属性, 用 get() 方法.</li>
	 * 	<li>2) 如果加载一个对象的目的是为了删除它, 或者是为了建立与别的对象的关联关系, 用 load() 方法.</li>
	 * </ul>
	 * 比如以下代码:
	 * <pre>
	 *  Transaction tx = session.beginTransaction();
	 *  // 立即检索策略
	 *  Order order = (Order) session.get(Order.class, new Long(1));
	 *  // 延迟检索策略
	 *  Customer customer = (Customer) session.load(Customer.class, new Long(1));
	 *  // 建立 Ordre 与 Customer 的多对一单向关联关系
	 *  order.setCustomer(customer);
	 *  tx.commit();
	 *  // Session 不需要知道 Customer 的各个属性的值, 而只要知道 Customer 对象的 OID 即可生成如下 sql 语句:
	 *  // update ORDER set CUSTOMER_ID=1, ORDER_NUMBER=... where ID=1;
	 * </pre>
	 * 
	 * @param id
	 *            主键
	 * @return
	 * @see #get(Serializable)
	 * @see #delete(Serializable)
	 */
	@SuppressWarnings("unchecked")
	public T load(final PK id) {
		Assert.notNull(id, "id can not be null");
		return (T) getSession().load(entityClass, id);
	}
	
	/**
	 * 按 id 获取对象. 采用 {@link Session#get(Class, Serializable)} 方法在数据库不存在与
	 * OID 对应的记录的时候, 会返回 null.
	 * 
	 * @param id
	 *            主键
	 * @return
	 * @see #load(Serializable)
	 */
	@SuppressWarnings("unchecked")
	public T get(final PK id) {
		Assert.notNull(id, "id can not be null");
		return (T) getSession().get(entityClass, id);
	}
	
	/**
	 * 按 id 列表获取对象列表.
	 * 
	 * @param ids
	 * @return
	 */
	public List<T> get(final Collection<PK> ids) {
		return find(Restrictions.in(getIdName(), ids));
	}
	
	/**
	 * 获取全部对象.
	 * 
	 * @return
	 */
	public List<T> getAll() {
		return find();
	}
	
	/**
	 * 获取全部对象, 支持按属性排序. 采用 QBC 的检索方式.
	 * 
	 * @param orderByProperty
	 * @param isAsc
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getAll(String orderByProperty, boolean isAsc) {
		Criteria c = createCriteria();
		if (isAsc) {
			c.addOrder(Order.asc(orderByProperty));
		} else {
			c.addOrder(Order.desc(orderByProperty));
		}
		return c.list();
	}
	
	/**
	 * 按属性查找对象列表, 匹配方式为相等. 采用 QBC 的检索方式.
	 * 
	 * @param propertyName
	 * @param value
	 * @return
	 */
	public List<T> findBy(final String propertyName, final Object value) {
		Assert.hasText(propertyName, "propertyName can not be null");
		Criterion criterion = Restrictions.eq(propertyName, value);
		return find(criterion);
	}
	
	/**
	 * 采用 QBC 的检索方式查询对象列表.
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	@SuppressWarnings("unchecked")
	public List<T> find(final Criterion... criterions) {
		return createCriteria(criterions).list();
	}
	
	/**
	 * 采用 QBC 的检索方式查询唯一对象. 可能有以下几种情况:
	 * <ul>
	 * 	<li>1) 如果有多个值抛 {@link NonUniqueResultException} 异常, 需要用 setMaxResults(1) 方法来限制.</li>
	 * 	<li>2) 如果有值且只有一个, 返回一个 Object.</li>
	 * 	<li>3) 如果没值, 返回 null.</li>
	 * </ul>
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	@SuppressWarnings("unchecked")
	public T findUnique(final Criterion... criterions) {
		return (T) createCriteria(criterions).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 按属性查找唯一对象, 匹配方式为相等. 采用 QBC 的检索方式.
	 * 
	 * @param propertyName
	 * @param value
	 * @return
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public T findUniqueBy(final String propertyName, final Object value) {
		Assert.hasText(propertyName, "propertyName can not be null");
		Criterion criterion = Restrictions.eq(propertyName, value);
		return (T) createCriteria(criterion).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 采用 HQL 的检索方式查询对象列表.
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 */
	@SuppressWarnings("unchecked")
	public <X> List<X> find(final String hql, final Object... values) {
		return createQuery(hql, values).list();
	}
	
	/**
	 * 采用 HQL 的检索方式查询对象列表.
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 */
	@SuppressWarnings("unchecked")
	public <X> List<X> find(final String hql, final Map<String, ?> values) {
		return createQuery(hql, values).list();
	}
	
	/**
	 * 采用 HQL 的检索方式查询唯一对象.
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public <X> X findUnique(final String hql, final Object... values) {
		return (X) createQuery(hql, values).setMaxResults(1).uniqueResult();
	}
	
	/**
	 * 采用 HQL 的检索方式查询唯一对象.
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 * @see #findUnique(Criterion...)
	 */
	@SuppressWarnings("unchecked")
	public <X> X findUnique(final String hql, final Map<String, ?> values) {
		return (X) createQuery(hql, values).setMaxResults(1).uniqueResult();
	}

	/**
	 * 保存新增或修改的对象.
	 * 
	 * @param entity
	 *            对象
	 */
	public void save(final T entity) {
		Assert.notNull(entity, "entity can not be null");
		getSession().saveOrUpdate(entity);
		logger.debug("save entity: {}", entity);
	}
	
	/**
	 * 删除对象.
	 * 
	 * @param entity
	 *            对象必须是 session 中的对象或含 id 属性的 transient 对象
	 */
	public void delete(final T entity) {
		Assert.notNull(entity, "entity can not be null");
		getSession().delete(entity);
		logger.debug("delete entity: {}", entity);
	}

	/**
	 * 按 id 删除对象.
	 * 
	 * @param id
	 *            主键
	 * @see #load(Serializable)
	 */
	public void delete(final PK id) {
		Assert.notNull(id, "id can not be null");
		delete(load(id));
		logger.debug("delete entity {},id is {}", entityClass.getSimpleName(), id);
	}
	
	/**
	 * 采用 HQL 的检索方式进行批量修改/删除操作.
	 * 
	 * @param values
	 *            数量可变的参数,按顺序绑定
	 * @return 更新记录数.
	 */
	public int batchExecute(final String hql, final Object... values) {
		return createQuery(hql, values).executeUpdate();
	}

	/**
	 * 采用 HQL 的检索方式进行批量修改/删除操作.
	 * 
	 * @param values
	 *            命名参数,按名称绑定
	 * @return 更新记录数.
	 */
	public int batchExecute(final String hql, final Map<String, ?> values) {
		return createQuery(hql, values).executeUpdate();
	}
	
	/**
	 * 根据查询 HQL 与参数列表创建 Query 对象. 与 find() 函数可进行更加灵活的操作. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Order o where o.customer=":customer and "
	 *      + "o.orderNumber like :orderNumber");
	 *  query.setParameter(1, customer);
	 *  query.setParameter(2, orderNumber);
	 * </pre>
	 * 上面的程序默认是使用了 Hibernate 的自动根据参数值的 Java 类型来进行对应的映射类型,
	 * 这样可以减少在第三个参数中指定 Java 类型的麻烦. 但是对于日期的 java.util.Date 类型,
	 * 会对应多种 Hibernate 映射类型, 如 Hibernate.DATE 或 Hibernate.TIMESTAMP,
	 * 因此必须在 setParameter() 方法中显式地指定到底对应那种 Hibernate 映射类型. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Customer c where c.birthday=:birthday");
	 *  query.setParameter("birthday", birthday, Hibernate.DATE);
	 * </pre>
	 * 
	 * @param values
	 *            数量可变的参数, 按顺序绑定
	 */
	public Query createQuery(final String queryString, final Object... values) {
		Assert.hasText(queryString, "queryString can not be null");
		Query query = getSession().createQuery(queryString);
		if (values != null) {
			for (int i = 0; i < values.length; i++) {
				// 按顺序绑定参数
				query.setParameter(i, values[i]);
			}
		}
		return query;
	}
	
	/**
	 * 根据查询 HQL 与参数列表创建 Query 对象. 与 find() 函数可进行更加灵活的操作. 比如:
	 * <pre>
	 *  Query query = session.createQuery("from Customer as c where c.name=:name "
	 *      + "and c.age=:age";
	 *  Customer customer = new Customer();
	 *  customer.setName("Tom");
	 *  customer.setAge(21);
	 *  // 命名参数中的 "name" 和 "age" 分别对应 Customer 类的 name 属性和 age 属性, 否则会抛异常
	 *  query.setProperties(customer);
	 * </pre>
	 * 在本方法中采用的是传递一个 {@link Map} 作为参数, 同样会根据 Map 的名来进行自动绑定. 另外,
	 * setProperties() 方法调用 setParameter() 方法, setParameter() 方法再根据 Customer
	 * 对象的属性的 Java 类型来判断 Hibernate 映射类型. 如果命名参数为日期类型, 则不能通过
	 * setProperties() 方法来绑定. 另外, 参数绑定对 null 是安全的, 比如如下代码不会抛异常:
	 * <pre>
	 *  String name = null;
	 *  session.createQuery("from Customer c where c.name=:name").setString("name", name).list();
	 * </pre>
	 * 上面的查询语句对应的 SQL 语句是: 
	 * <pre>
	 *  select * from CUSTOMERS where NMAE=null;
	 * </pre>
	 * 这条查询语句的查询结果永远为空. 如果要查询名字为 null 的客户, 应该使用 "is null"
	 * 比较运算符, 比如:
	 * <pre>
	 *  // HQL 检索方式
	 *  session.createQuery("from Customer c where c.name is null");
	 *  // QBC 检索方式
	 *  session.createCriteria(Customer.class).add(Restrictions.isNull("name"));
	 * </pre>
	 * 
	 * @param values
	 *            命名参数, 按名称绑定
	 * @see #find(String, Object...)
	 */
	public Query createQuery(final String queryString, final Map<String, ?> values) {
		Assert.hasText(queryString, "queryString can not be null");
		Query query = getSession().createQuery(queryString);
		if (values != null) {
			// 把命名参数与对象的属性值进行绑定
			query.setProperties(values);
		}
		return query;
	}
	
	/**
	 * 根据 Criterion 条件创建 Criteria. 与 find() 函数可进行更加灵活的操作.
	 * 
	 * @param criterions
	 *            数量可变的 Criterion
	 */
	public Criteria createCriteria(final Criterion... criterions) {
		Criteria criteria = getSession().createCriteria(entityClass);
		for (Criterion c : criterions) {
			criteria.add(c);
		}
		return criteria;
	}
	
	/**
	 * 初始化对象. 使用 load() 方法得到的仅是对象 Proxy, 在传到 View 层前需要进行初始化.
	 * 如果传入 entity, 则只初始化 entity 的直接属性, 但不会初始化延迟加载的关联集合和属性.
	 * 如需初始化关联属性,需执行:
	 * <pre>
	 * Hibernate.initialize(user.getRoles())        // 初始化User的直接属性和关联集合
	 * Hibernate.initialize(user.getDescription())  // 初始化User的直接属性和延迟加载的Description属性
	 * </pre>
	 */
	public void initProxyObject(Object proxy) {
		Hibernate.initialize(proxy);
	}
	
	/**
	 * 为 Query 添加 distinct transformer. 预加载关联对象的 HQL 会引起主对象重复,
	 * 需要进行 distinct 处理.
	 * 
	 * @param query
	 * @return
	 */
	public Query distinct(Query query) {
		query.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
		return query;
	}
	
	/**
	 * 为 Criteria 添加 distinct transformer. 预加载关联对象的 HQL 会引起主对象重复,
	 * 需要进行 distinct 处理.
	 * 
	 * @param criteria
	 * @return
	 */
	public Criteria distinct(Criteria criteria) {
		criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
		return criteria;
	}
	
	/**
	 * 判断对象的属性值在数据库内是否唯一.<br>
	 * 在修改对象的情景下, 如果属性新修改的值 (value) 等于属性原来的值 (orgValue) 则不作比较.
	 * 
	 * @param propertyName
	 * @param newValue
	 * @param oldValue
	 * @return
	 */
	public boolean isPropertyUnique(final String propertyName,
			final Object newValue, final Object oldValue) {
		if (newValue == null || newValue.equals(oldValue)) {
			return true;
		}
		Object object = findUniqueBy(propertyName, newValue);
		return (object == null);
	}
	
	/**
	 * 清除当前 Session.
	 */
	public void flush() {
		getSession().flush();
	}
}

[/size]
分享到:
评论
2 楼 davinsam 2012-08-31  
import org.mysterylab.utopiaframework.core.orm.Page; 
import org.mysterylab.utopiaframework.core.orm.PropertyFilter; 
import org.mysterylab.utopiaframework.core.orm.PropertyFilter.MatchType; 
import org.mysterylab.utopiaframework.util.reflect.ReflectionUtil;
这些类怎么实现???
1 楼 davinsam 2012-08-31  
dao类使用说明弄错了,怎么跟basehibernatedao一样啦?能重新写一下吗?

相关推荐

Global site tag (gtag.js) - Google Analytics