Solon整合hibernate、Solon使用hibernate

Solon整合hibernate、Solon使用hibernate

环境

jdk17+ ,solon 2.5.4(当前最新:2023年9月13日09:42:12)、hibernate 6.3(当前最新版本:2023年9月13日09:42:31)

依赖

        <dependency>
            <groupId>org.noear</groupId>
            <artifactId>solon-web</artifactId>
            <version>2.5.4</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-core -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>${hutool.version}</version>
        </dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core -->
        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.3.0.Final</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>5.0.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.1.0</version>
        </dependency>

定义一个hibernate配置

import cn.hutool.core.util.ClassUtil;
import org.hibernate.HibernateException;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.context.internal.ThreadLocalSessionContext;

import javax.sql.DataSource;
import java.lang.reflect.Proxy;
import java.util.Properties;
import java.util.Set;

/**
 * @author lingkang
 * created by 2023/9/11
 */
public class HibernateConfiguration extends Configuration {
    public HibernateConfiguration() {
    }

    /**
     * 添加实体包扫描,有hibernate的@Table、@Entity
     */
    public HibernateConfiguration addScanPackage(String... packagePath) {
        if (packagePath != null)
            for (String pack : packagePath) {
                Set<Class<?>> classes = ClassUtil.scanPackage(pack);
                for (Class<?> clazz : classes)
                    addAnnotatedClass(clazz);
            }
        return this;
    }

    public HibernateConfiguration setDataSource(DataSource dataSource) {
        if (dataSource != null) {
            this.getProperties().put(AvailableSettings.DATASOURCE, dataSource);
        }
        return this;
    }

    public HibernateConfiguration setProperties(Properties properties) {
        if (properties != null) {
            properties.entrySet().forEach(obj -> {
                getProperties().put(obj.getKey(), obj.getValue());
            });
        }
        return this;
    }

    @Override
    public SessionFactory buildSessionFactory() throws HibernateException {
        getProperties().put(AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY, "jdbc");
        getProperties().put(AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, ThreadLocalSessionContext.class.getName());
        SessionFactory sessionFactory = super.buildSessionFactory();
        return sessionFactory;
    }
}

一定要开启当前线程回话上下文,因为没有实现JTA事务

配置

import com.zaxxer.hikari.HikariDataSource;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AvailableSettings;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import top.lingkang.ciyuanwaf.config.h.HibernateConfiguration;

import javax.sql.DataSource;
import java.util.Properties;

/**
 * @author lingkang
 * Created by 2023/9/9
 */
@Configuration
public class DbConfig {
    @Bean
    public DataSource db1(@Inject("${db1}") HikariDataSource ds) {
        ds.setPoolName("db1");
        ds.setMaximumPoolSize(10);
        return ds;
    }

    @Bean
    public SessionFactory sessionFactory(@Inject DataSource dataSource) {
        Properties properties = new Properties();
        // properties.put(AvailableSettings.DIALECT, MySQLDialect.class.getName());
        // properties.put(AvailableSettings.JAKARTA_JTA_DATASOURCE, dataSource);
        properties.put(AvailableSettings.DATASOURCE, dataSource);
        /*properties.put(AvailableSettings.DRIVER,"com.mysql.cj.jdbc.Driver");
        properties.put(AvailableSettings.URL,"jdbc:mysql://localhost:3306/cy?useUnicode=true&nullCatalogMeansCurrent=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai");
        properties.put(AvailableSettings.USER,"root");
        properties.put(AvailableSettings.PASS,"123456");*/

        HibernateConfiguration configuration = new HibernateConfiguration();
        configuration.addScanPackage("top.lingkang.ciyuanwaf.entity");
        configuration.setProperties(properties);
        return configuration.buildSessionFactory();
    }

}

app.yml配置

db1:
  jdbcUrl: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true
  driverClassName: com.mysql.cj.jdbc.Driver
  username: root
  password: 123456

配置事务

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.noear.solon.Solon;
import org.noear.solon.core.aspect.Invocation;
import org.noear.solon.data.around.TranInterceptor;

/**
 * @author lingkang
 * created by 2023/9/12
 */
public class HibernateTranInterceptor extends TranInterceptor {

    private SessionFactory sessionFactory;

    private static final ThreadLocal<Integer> tranNumber = new ThreadLocal<>();

    @Override
    public Object doIntercept(Invocation inv) throws Throwable {
        if (sessionFactory == null)
            sessionFactory = Solon.context().getBean(SessionFactory.class);

        Session session = sessionFactory.getCurrentSession();
        if (tranNumber.get() == null)
            tranNumber.set(0);
        else
            tranNumber.set(tranNumber.get() + 1);

        Object result = null;
        try {
            if (!session.getTransaction().isActive())
                session.beginTransaction();
            result = super.doIntercept(inv);
            if (tranNumber.get() == 0) {
                session.getTransaction().commit();
            } else tranNumber.set(tranNumber.get() - 1);
        } catch (Throwable e) {
            session.getTransaction().rollback();
            throw e;
        }

        return result;
    }
}

注意:事务提交/回滚后,会自动关闭当前回话的

插件类

import org.noear.solon.core.AppContext;
import org.noear.solon.core.Plugin;
import org.noear.solon.data.annotation.Tran;
import top.lingkang.ciyuanwaf.config.h.a.DaoMapperBeanBuilder;
import top.lingkang.ciyuanwaf.config.h.a.Mapper;

/**
 * @author lingkang
 * created by 2023/9/11
 */
public class HibernatePlugin implements Plugin {

    @Override
    public void start(AppContext context) throws Throwable {
        context.beanInterceptorAdd(Tran.class, new HibernateTranInterceptor(), 1);
    }

    @Override
    public void prestop() throws Throwable {
        Plugin.super.prestop();
    }

    @Override
    public void stop() throws Throwable {
        Plugin.super.stop();
    }
}

插件top.lingkang.ciyuanwaf.config.properties文件如下
src/main/resources/META-INF/solon/top.lingkang.ciyuanwaf.config.properties

内容如下:

solon.plugin=top.lingkang.ciyuanwaf.config.h.HibernatePlugin

实体类

import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import lombok.Data;

import java.util.Date;

/**
 * @author lingkang
 * Created by 2023/9/9
 */
@Data
@MappedSuperclass
public class BaseTime {
    @Column(name = "createTime")
    private Date createTime;
    @Column(name = "updateTime")
    private Date updateTime;

    @PrePersist// 新增时自动修改时间
    public void prePersist() {
        if (createTime == null)
            createTime = new Date();
        if (updateTime == null)
            updateTime = createTime;
    }
}

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;

import java.util.Date;

/**
 * @author lingkang
 * created by 2023/9/8
 */
@Data
@Entity
@Table(name = "http_entity")
public class HttpEntity extends BaseTime{
    @Id
    private String id;
    @Column(name = "description")
    private String description;
    @Column(name = "type")
    private String type;
}

调用

注意,solon未适配JTA事务处理,所有操作均在事务中执行(查询也要在事务中执行,hibernate底层即使是查询也会检查事务是否开启)

package top.lingkang.ciyuanwaf.controller;

import com.alibaba.fastjson2.JSONObject;
import com.zaxxer.hikari.HikariDataSource;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Inject;
import org.noear.solon.annotation.Mapping;
import org.noear.solon.core.handle.Context;
import org.noear.solon.data.annotation.Tran;
import top.lingkang.ciyuanwaf.dao.HttpEntityDao;
import top.lingkang.ciyuanwaf.entity.HttpEntity;

import java.util.List;

/**
 * @author lingkang
 * created by 2023/9/8
 */
@Controller
public class WebController {
    @Inject
    private SessionFactory sessionFactory;

    private Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    @Tran// 任何数据库操作都要开启事务
    @Mapping("/t")
    public void t1() {
        HttpEntity entity = new HttpEntity();
        entity.setId(System.currentTimeMillis() + "");
        httpEntityDao.saveOrUpdate(entity);
        System.out.println(JSONObject.toJSONString(entity));
        /*int update = getSession().createQuery(
                "update HttpEntity set updateTime=?1 where id='current'"
        ).setParameter(1, new Date()).executeUpdate();
        System.out.println(update);*/
    }

    @Tran// 任何数据库操作都要开启事务
    @Mapping("/t2")
    public Object t2() {
        HttpEntity entity = new HttpEntity();
        entity.setId(System.currentTimeMillis() + "");
        httpEntityDao.saveOrUpdate(entity);
        List list = getSession()
                .createQuery("select e from HttpEntity e where e.id=?1")
                .setParameter(1,entity.getId())
                .setMaxResults(1).list();
        return list;
    }

    @Tran// 任何数据库操作都要开启事务
    @Mapping("/t3")
    public Object t3() {
        List list = getSession()
                .createQuery("select e from HttpEntity e")
                .setMaxResults(1).list();
        return list;
    }
}

实现JPA的JpaRepository效果

JPA可以通过继承JpaRepository接口来快速savefindAll等操作,底层原理是使用代理这个接口实现。

1、定义@Mapper,对标jpa的@Repository注解、定义Dao,对标实现JPA的JpaRepository接口

import java.lang.annotation.*;

/**
 * @author lingkang
 * created by 2023/9/12
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Mapper {
}
import java.io.Serializable;

/**
 * @author lingkang
 * created by 2023/9/12
 */
public interface Dao<E> {
    void saveOrUpdate(E entity);

    void deleteById(Serializable id);
}

2、定义@Mapper接口代理处理

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.noear.solon.Solon;
import top.lingkang.ciyuanwaf.config.h.a.Dao;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @author lingkang
 * created by 2023/9/12
 */
public class HibernateDaoInvocationHandler implements InvocationHandler {
    private SessionFactory sessionFactory;
    private Class<?> entityClass;
    private Class<?> daoInterface;

    public HibernateDaoInvocationHandler(Class<?> entityClass, Class<?> daoInterface) {
        this.entityClass = entityClass;
        this.daoInterface = daoInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass() == Dao.class) {
            if ("saveOrUpdate".equals(method.getName())) {
                JpaUtils.checkNotNull(args, "保存对象不能为空");
                JpaUtils.checkNotNull(args[0], "保存对象不能为空");
                Session session = session();
                session.persist(args[0]);// 保存或更新
            }
        }
        return null;
    }


    private Session session() {
        if (sessionFactory == null)
            sessionFactory = Solon.context().getBean(SessionFactory.class);
        return sessionFactory.getCurrentSession();
    }
}

上面我只做了一个saveOrUpdate做例子,还有findAll、根据字段find也要自己解析,也没有做类的检查等,可自行实现

3、编写solon的BeanBuilder应用我们的接口代理

import lombok.extern.slf4j.Slf4j;
import org.noear.solon.core.BeanBuilder;
import org.noear.solon.core.BeanWrap;
import top.lingkang.ciyuanwaf.config.h.HibernateDaoInvocationHandler;
import top.lingkang.ciyuanwaf.config.h.JpaUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;

/**
 * @author lingkang
 * created by 2023/9/12
 */
@Slf4j
public class DaoMapperBeanBuilder implements BeanBuilder<Mapper> {

    @Override
    public void doBuild(Class<?> clz, BeanWrap bw, Mapper anno) throws Throwable {
        if (clz.isInterface()) {
            Type[] interfaces = clz.getGenericInterfaces();
            Type type = interfaces[0];
            String className = getEntityClassName(type.getTypeName());
            Class<?> forName = Class.forName(className);
            // log.info(getEntityClassName(type.getTypeName()));

            Object proxyInstance = Proxy.newProxyInstance(
                    this.getClass().getClassLoader(),
                    new Class[]{clz}, new HibernateDaoInvocationHandler(forName, clz)
            );
            Field field = bw.getClass().getDeclaredField("raw");
            field.setAccessible(true);
            field.set(bw, proxyInstance);
        } else
            throw new IllegalArgumentException("@Mapper 只能作用在接口上,当前:" + clz.getName());
    }

    // top.lingkang.ciyuanwaf.config.Dao<top.lingkang.ciyuanwaf.entity.ItemEntity>
    // 将返回实体类:top.lingkang.ciyuanwaf.entity.ItemEntity
    private String getEntityClassName(String name) {
        String substring = name.substring(38);
        return substring.substring(0, substring.length() - 1);
    }
}

DaoMapperBeanBuilder配置到solon,修改之前的HibernatePlugin
加入下面的内容

context.beanBuilderAdd(Mapper.class, new DaoMapperBeanBuilder());
import org.noear.solon.core.AppContext;
import org.noear.solon.core.Plugin;
import org.noear.solon.data.annotation.Tran;
import top.lingkang.ciyuanwaf.config.h.a.DaoMapperBeanBuilder;
import top.lingkang.ciyuanwaf.config.h.a.Mapper;

/**
 * @author lingkang
 * created by 2023/9/11
 */
public class HibernatePlugin implements Plugin {

    @Override
    public void start(AppContext context) throws Throwable {
        context.beanBuilderAdd(Mapper.class, new DaoMapperBeanBuilder());

        context.beanInterceptorAdd(Tran.class, new HibernateTranInterceptor(), 1);
    }

    @Override
    public void prestop() throws Throwable {
        Plugin.super.prestop();
    }

    @Override
    public void stop() throws Throwable {
        Plugin.super.stop();
    }
}

4、编写dao(mapper)

import top.lingkang.ciyuanwaf.config.h.a.Mapper;
import top.lingkang.ciyuanwaf.config.h.a.Dao;
import top.lingkang.ciyuanwaf.entity.HttpEntity;

/**
 * @author lingkang
 * created by 2023/9/12
 */
@Mapper
public interface HttpEntityDao extends Dao<HttpEntity> {
}

5、调用Dao

@Inject
    HttpEntityDao httpEntityDao;

    @Tran// 任何数据库操作都要开启事务
    @Mapping("/t")
    public void t1() {
        HttpEntity entity = new HttpEntity();
        entity.setId(System.currentTimeMillis() + "");
        httpEntityDao.saveOrUpdate(entity);
        System.out.println(JSONObject.toJSONString(entity));
    }