Spring Data Neo4J 使用

  1. maven引入

    <dependency>    
        <groupId>org.springframework.bootgroupId>    			 
        <artifactId>spring-boot-starter-data-neo4jartifactId>
    dependency>
    
  2. uri配置

    #neo4j
    spring.data.neo4j.uri = bolt://neo4j:7687
    spring.data.neo4j.username= neo4j
    spring.data.neo4j.password= 123456
    
  3. 事务配置

    使用multiTransactionManager这个事务管理器,可以让mysql和neo4j的的修改同时提交或者回滚

    package com.zhibi.malling.aop;
    
    import com.zhibi.malling.annotation.EnableNeo4j;
    import org.neo4j.ogm.session.SessionFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    import org.springframework.context.annotation.Primary;
    import org.springframework.data.neo4j.transaction.Neo4jTransactionManager;
    import org.springframework.data.transaction.ChainedTransactionManager;
    import org.springframework.orm.jpa.JpaTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.TransactionStatus;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.persistence.EntityManagerFactory;
    
    /**
     * @author QinHe at 2019-05-31
     */
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    //@Aspect
    @Configuration
    @DependsOn("sessionFactory")
    @ConditionalOnBean(annotation = EnableNeo4j.class)
    //@EnableTransactionManagement
    public class TransactionAspect {
    
        ThreadLocal<TransactionStatus> transactionStatusThreadLocal = new ThreadLocal<>();
    
        /**
         * 定义mysql事务管理器,必须有transactionManager作为默认事务管理器
         *
         * @param emf
         * @return
         */
        @Bean("transactionManager")
        @Primary
        public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
            return new JpaTransactionManager(emf);
        }
    
        /**
         * 定义neo4j事务管理器
         *
         * @param sessionFactory
         * @return
         */
        @Bean("neo4jTransactionManager")
        public Neo4jTransactionManager neo4jTransactionManager(SessionFactory sessionFactory) {
            return new Neo4jTransactionManager(sessionFactory);
        }
    
    //    @Autowired
    //    @Qualifier("neo4jTransactionManager")
    //    Neo4jTransactionManager neo4jTransactionManager;
    //    @Autowired
    //    @Qualifier("transactionManager")
    //    JpaTransactionManager jpaTransactionManager;
    
        @Autowired
        @Bean(name = "multiTransactionManager")
        public PlatformTransactionManager multiTransactionManager(
                Neo4jTransactionManager neo4jTransactionManager,
                JpaTransactionManager mysqlTransactionManager) {
            return new ChainedTransactionManager(
                    neo4jTransactionManager, mysqlTransactionManager);
        }
    
    //    @Around("@annotation(com.zhibi.malling.annotation.MultiTransaction)")
    //    public Object multiTransaction(ProceedingJoinPoint proceedingJoinPoint) {
    //        TransactionStatus neo4jTransactionStatus = neo4jTransactionManager.getTransaction(new DefaultTransactionDefinition());
    //        TransactionStatus jpaTransactionStatus = jpaTransactionManager.getTransaction(new DefaultTransactionDefinition());
    //        try {
    //            Object obj = proceedingJoinPoint.proceed();
    //            //注意:事务之间必须是包含关系,不能交叉
    //            jpaTransactionManager.commit(jpaTransactionStatus);
    //            neo4jTransactionManager.commit(neo4jTransactionStatus);
    //            return obj;
    //        } catch (Throwable throwable) {
    //            jpaTransactionManager.rollback(jpaTransactionStatus);
    //            neo4jTransactionManager.rollback(neo4jTransactionStatus);
    //            throw new RuntimeException(throwable);
    //        }
    //    }
    }
    
    
  4. 节点实体类orm

    package com.zhibi.malling.user.spi.neo4j;
    
    import com.alibaba.fastjson.JSONObject;
    import com.zhibi.malling.user.spi.enums.BonusConfig;
    import com.zhibi.malling.utils.DateUtil;
    import io.swagger.annotations.ApiModelProperty;
    import org.neo4j.ogm.annotation.*;
    
    import java.io.Serializable;
    import java.math.BigDecimal;
    import java.util.*;
    
    /**
     * @author QinHe at 2019-05-27
     */
    @NodeEntity(label = "User")
    public class UserProfile implements Serializable {
    
        private static final long serialVersionUID = 3870028896024217898L;
    
        @Id
        @GeneratedValue
        private Long id;
    
        @Property(name = "userId")
        @Index(unique = true)
        private Long userId;
    
        @Property(name = "phone")
        private String phone;
    
        @Property(name = "nickname")
        private String nickname;
    
        @Property(name = "mallingBonusRole")
        @ApiModelProperty("mallingBonusRole(1=JP,2=SP,3=EP)")
        private Byte mallingBonusRole;
    
        @Property(name = "marginPaid")
        @ApiModelProperty("marginPaid(1=已交保证金)")
        private Byte marginPaid;
    
        @Property(name = "fastStartBonusRoleLastMonth")
        @ApiModelProperty("上个月FB级别(S/M/L)")
        private Byte fastStartBonusRoleLastMonth;
    
        @Property(name = "fastStartBonusRole")
        @ApiModelProperty("fastStartBonusRole(S/M/L)")
        private Byte fastStartBonusRole;
    
        @Property(name = "fastStartCheckTime")
        @ApiModelProperty("FastStart按钮点击时间,转换成yyyyMMddHHmmss格式,以Long型数据展示")
        private Long fastStartCheckTime;
    
        @Property(name = "unilevelBonusRole")
        @ApiModelProperty("unilevelBonusRole(L1..L7)")
        private Byte unilevelBonusRole;
    
        @Property(name = "depth")
        private Long depth;
    
        @Property(name = "version")
        @Version
        private Long version;
    
        @Transient
        private JSONObject other = new JSONObject();
    
        public UserProfile() {
        }
    
        public enum MallingBonusRoleEnum {
            COMMON((byte) 0),
            JP((byte) 1),
            SP((byte) 2),
            EP((byte) 3);
            private Byte code;
    
            MallingBonusRoleEnum(Byte code) {
                this.code = code;
            }
    
            public Byte getCode() {
                return code;
            }
        }
    
        public enum FastStartBonusRoleEnum {
            COMMON((byte) 0, (byte) 1, BigDecimal.ZERO),
            S((byte) 1, (byte) 2, BonusConfig.S_LEVEL_CONDITION, EarningsLevelForFBEnum.LEVEL_1, EarningsLevelForFBEnum.LEVEL_2),
            M((byte) 2, (byte) 3, BonusConfig.M_LEVEL_CONDITION, EarningsLevelForFBEnum.LEVEL_1, EarningsLevelForFBEnum.LEVEL_2, EarningsLevelForFBEnum.LEVEL_3),
            L((byte) 3, null, BonusConfig.L_LEVEL_CONDITION, EarningsLevelForFBEnum.LEVEL_1, EarningsLevelForFBEnum.LEVEL_2, EarningsLevelForFBEnum.LEVEL_3, EarningsLevelForFBEnum.LEVEL_4);
    
            private static Map<Byte, FastStartBonusRoleEnum> map = new HashMap<>();
    
            static {
                for (FastStartBonusRoleEnum fastStartBonusRoleEnum : FastStartBonusRoleEnum.values()) {
                    map.put(fastStartBonusRoleEnum.getCode(), fastStartBonusRoleEnum);
                }
            }
    
            private Byte code;
            private Byte nextRoleCode;
            private BigDecimal pvNeed;
            private EarningsLevelForFBEnum[] levelEnums;
    
            FastStartBonusRoleEnum(Byte code, Byte nextRoleCode, BigDecimal pvNeed, EarningsLevelForFBEnum... bonusLevels) {
                this.code = code;
                this.nextRoleCode = nextRoleCode;
                this.pvNeed = pvNeed;
                this.levelEnums = bonusLevels;
            }
    
            public static FastStartBonusRoleEnum getEnumByCode(Byte code) {
                return map.get(code);
            }
    
            public Byte getCode() {
                return code;
            }
    
            public Byte getNextRoleCode() {
                return nextRoleCode;
            }
    
            public BigDecimal getPvNeed() {
                return pvNeed;
            }
    
            public EarningsLevelForFBEnum[] getLevelEnums() {
                return levelEnums;
            }
        }
    
        public enum UnilevelBonusRoleEnum {
            COMMON((byte) 0),
            L1((byte) 1, EarningsLevelForUBEnum.LEVEL_1),
            L2((byte) 2, EarningsLevelForUBEnum.LEVEL_2, EarningsLevelForUBEnum.LEVEL_1),
            L3((byte) 3, EarningsLevelForUBEnum.LEVEL_3, EarningsLevelForUBEnum.LEVEL_2, EarningsLevelForUBEnum.LEVEL_1),
            L4((byte) 4, EarningsLevelForUBEnum.LEVEL_4, EarningsLevelForUBEnum.LEVEL_3, EarningsLevelForUBEnum.LEVEL_2, EarningsLevelForUBEnum.LEVEL_1),
            L5((byte) 5, EarningsLevelForUBEnum.LEVEL_5, EarningsLevelForUBEnum.LEVEL_4, EarningsLevelForUBEnum.LEVEL_3, EarningsLevelForUBEnum.LEVEL_2, EarningsLevelForUBEnum.LEVEL_1),
            L6((byte) 6, EarningsLevelForUBEnum.LEVEL_6, EarningsLevelForUBEnum.LEVEL_5, EarningsLevelForUBEnum.LEVEL_4, EarningsLevelForUBEnum.LEVEL_3, EarningsLevelForUBEnum.LEVEL_2, EarningsLevelForUBEnum.LEVEL_1),
            L7((byte) 7, EarningsLevelForUBEnum.LEVEL_7, EarningsLevelForUBEnum.LEVEL_6, EarningsLevelForUBEnum.LEVEL_5, EarningsLevelForUBEnum.LEVEL_4, EarningsLevelForUBEnum.LEVEL_3, EarningsLevelForUBEnum.LEVEL_2, EarningsLevelForUBEnum.LEVEL_1);
    
            private static Map<Byte, UnilevelBonusRoleEnum> map = new HashMap<>();
    
            static {
                for (UnilevelBonusRoleEnum unilevelBonusRoleEnum : UnilevelBonusRoleEnum.values()) {
                    map.put(unilevelBonusRoleEnum.getCode(), unilevelBonusRoleEnum);
                }
            }
    
            private Byte code;
            private EarningsLevelForUBEnum[] levelEnums;
    
            UnilevelBonusRoleEnum(Byte code, EarningsLevelForUBEnum... levelEnums) {
                this.code = code;
                this.levelEnums = levelEnums;
            }
    
            public static UnilevelBonusRoleEnum getEnumByCode(Byte code) {
                return map.get(code);
            }
    
            public Byte getCode() {
                return code;
            }
    
            public EarningsLevelForUBEnum[] getLevelEnums() {
                return levelEnums;
            }
        }
    
        public enum EarningsLevelForFBEnum {
            LEVEL_1(1, BonusConfig.DIRECTLY_LEVEL1_INCOME),
            LEVEL_2(2, BonusConfig.DIRECTLY_LEVEL2_INCOME),
            LEVEL_3(3, BonusConfig.DIRECTLY_LEVEL3_INCOME),
            LEVEL_4(4, BonusConfig.DIRECTLY_LEVEL4_INCOME),
            ;
    
            private static Map<Integer, EarningsLevelForFBEnum> map = new HashMap<>();
    
            static {
                for (EarningsLevelForFBEnum earningsLevelForFBEnum : EarningsLevelForFBEnum.values()) {
                    map.put(earningsLevelForFBEnum.getCode(), earningsLevelForFBEnum);
                }
            }
    
            private Integer code;
            private BigDecimal bonusPercentage;
    
            EarningsLevelForFBEnum(Integer code, BigDecimal bonusPercentage) {
                this.code = code;
                this.bonusPercentage = bonusPercentage;
            }
    
            public static EarningsLevelForFBEnum getEnumByCode(Integer code) {
                return map.get(code);
            }
    
            public Integer getCode() {
                return code;
            }
    
            public BigDecimal getBonusPercentage() {
                return bonusPercentage;
            }
        }
    
        public enum EarningsLevelForUBEnum {
            LEVEL_1(1, BonusConfig.UNILEVEL_L1, BonusConfig.UNILEVEL_LEVEL1_LEVEL4_INCOME),
            LEVEL_2(2, BonusConfig.UNILEVEL_L2, BonusConfig.UNILEVEL_LEVEL1_LEVEL4_INCOME),
            LEVEL_3(3, BonusConfig.UNILEVEL_L3_L7, BonusConfig.UNILEVEL_LEVEL1_LEVEL4_INCOME),
            LEVEL_4(4, BonusConfig.UNILEVEL_L3_L7, BonusConfig.UNILEVEL_LEVEL1_LEVEL4_INCOME),
            LEVEL_5(5, BonusConfig.UNILEVEL_L3_L7, BonusConfig.UNILEVEL_LEVEL5_INCOME),
            LEVEL_6(6, BonusConfig.UNILEVEL_L3_L7, BonusConfig.UNILEVEL_LEVEL6_INCOME),
            LEVEL_7(7, BonusConfig.UNILEVEL_L3_L7, BonusConfig.UNILEVEL_LEVEL7_INCOME);
    
            private static Map<Integer, EarningsLevelForUBEnum> map = new HashMap<>();
    
            static {
                for (EarningsLevelForUBEnum earningsLevelForUBEnum : EarningsLevelForUBEnum.values()) {
                    map.put(earningsLevelForUBEnum.getCode(), earningsLevelForUBEnum);
                }
            }
    
            private Integer code;
            private BigDecimal pvCondition;
            private BigDecimal bonusPercentage;
    
            EarningsLevelForUBEnum(Integer code, BigDecimal pvCondition, BigDecimal bonusPercentage) {
                this.code = code;
                this.pvCondition = pvCondition;
                this.bonusPercentage = bonusPercentage;
            }
    
            public static EarningsLevelForUBEnum getEnumByCode(Integer code) {
                return map.get(code);
            }
    
            public Integer getCode() {
                return code;
            }
    
            public BigDecimal getPvCondition() {
                return pvCondition;
            }
    
            public BigDecimal getBonusPercentage() {
                return bonusPercentage;
            }
        }
    }
    
    
  5. 关系实体类

    package com.zhibi.malling.user.spi.neo4j;
    
    import com.alibaba.fastjson.JSONObject;
    import org.neo4j.ogm.annotation.*;
    
    import java.io.Serializable;
    import java.util.Date;
    
    /**
     * @author QinHe at 2019-05-27
     */
    
    @RelationshipEntity(type = "Invite")
    public class InviteRelation implements Serializable {
        private static final long serialVersionUID = 3870028896024217898L;
    
        @Id
        @GeneratedValue
        private Long id;
    
        /**
         * 定义关系的起始节点 == StartNode
         */
        @StartNode
        private UserProfile startNode;
    
        /**
         * 定义关系的终止节点 == EndNode
         */
        @EndNode
        private UserProfile endNode;
    
        private Date createTime;
    
        private Long inviterId;
    
        private Long inviteeId;
    
        @Transient
        private JSONObject other = new JSONObject();
    
        @Property(name = "version")
        @Version
        private Long version;
    
        public InviteRelation() {
        }
    
        public JSONObject getOther() {
            return other;
        }
    
        public void setOther(JSONObject other) {
            this.other = other;
        }
    
        public UserProfile getStartNode() {
            return startNode;
        }
    
        public void setStartNode(UserProfile startNode) {
            this.startNode = startNode;
        }
    
        public UserProfile getEndNode() {
            return endNode;
        }
    
        public void setEndNode(UserProfile endNode) {
            this.endNode = endNode;
        }
    
        public Date getCreateTime() {
            return createTime;
        }
    
        public void setCreateTime(Date createTime) {
            this.createTime = createTime;
        }
    
        public Long getInviterId() {
            return inviterId;
        }
    
        public void setInviterId(Long inviterId) {
            this.inviterId = inviterId;
        }
    
        public Long getInviteeId() {
            return inviteeId;
        }
    
        public void setInviteeId(Long inviteeId) {
            this.inviteeId = inviteeId;
        }
    
        public Long getVersion() {
            return version;
        }
    
        public void setVersion(Long version) {
            this.version = version;
        }
    
    }
    
    
  6. Jpa的Repository

    package com.zhibi.malling.repository.neo4j;
    
    import com.zhibi.malling.user.spi.neo4j.UserProfile;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.neo4j.annotation.Query;
    import org.springframework.data.neo4j.repository.Neo4jRepository;
    import org.springframework.data.repository.query.Param;
    
    import java.util.List;
    
    /**
     * @author QinHe at 2019-05-27
     */
    public interface UserNeo4jRepository extends Neo4jRepository<UserProfile, Long> {
        UserProfile findByUserId(Long userId);
    
        @Query(" match(inviter:User)-[r:Invite*1..]->(invitee:User) where inviter.userId = {inviterId} return  invitee")
        List<UserProfile> getAllInvitees(@Param("inviterId") Long inviterId);
    
        @Query(" match(invitee:User)<-[r:Invite*1..]-(inviter:User) where invitee.userId = {inviteeId} return  inviter")
        List<UserProfile> getAllInviters(@Param("inviteeId") Long inviteeId);
    
        @Query(" match(invitee:User)<-[r:Invite]-(inviter:User) where invitee.userId = {inviteeId} return  inviter")
        UserProfile getInviter(@Param("inviteeId") Long inviteeId);
    
        @Query(" match(inviter:User)-[r:Invite]->(invitee:User) where inviter.userId = {inviterId} return  count(invitee.userId)")
        long countDirectByInviterId(@Param("inviterId") Long inviterId);
    
        @Query(" match(inviter:User)-[r:Invite]->(invitee:User) where inviter.userId = {inviterId} and invitee.mallingBonusRole in {inviteeRoleList} return  count(invitee.userId)")
        long countDirectByInviterIdAndInviteeRole(@Param("inviterId") Long inviterId, @Param("inviteeRoleList") List<Byte> inviteeRoleList);
    
        @Query(" match(inviter:User)-[r:Invite]->(invitee:User) where inviter.userId = {inviterId} and invitee.mallingBonusRole={inviteeRole} return  count(invitee.userId)")
        long countDirectByInviterIdAndInviteeRole(@Param("inviterId") Long inviterId, @Param("inviteeRole") Byte inviteeRole);
    
        @Query(" match(inviter:User)-[r:Invite]->(invitee:User) where inviter.userId = {inviterId} return  count(invitee.userId)")
        UserProfile getNearestInviter(@Param("inviterId") Long inviterId);
    
        @Query(value = " match(inviter:User)-[r:Invite*0..]->(invitee:User) where inviter.userId = {inviterId} and invitee.userId in {userIdList}" +
                "  and invitee.totalDirectInviteJpCount<{mallingBonusJpUpgradeSpCondition} return invitee order by totalDirectInviteJpCount desc ",
                countQuery = " match(inviter:User)-[r:Invite*0..]->(invitee:User) where inviter.userId = {inviterId} and invitee.userId in {userIdList}" +
                        "  and invitee.totalDirectInviteJpCount<{mallingBonusJpUpgradeSpCondition} return count(invitee) ")
        Page<UserProfile> getInviteesForUpgradeEp(@Param("legId") Long legId, @Param("userIdList") List<Long> userIdList, Integer mallingBonusJpUpgradeSpCondition, Pageable pageable);
    
        @Query(" match(inviter:User)-[r1:Invite]->(leg:User)-[r2:Invite*0..]->(invitee:User) where inviter.userId = {inviterId} and invitee.userId={userId} return leg ")
        UserProfile findLeg(@Param("inviterId") Long inviterId, @Param("userId") Long userId);
    
        @Query("match(inviter:User)-[r:Invite*]->(invitee:User) where inviter.userId = {userId} and not (invitee)-->() RETURN count(invitee)")
        Long getFarthestInviteeCount(@Param("userId") Long userId);
    
        @Query("match(inviter:User)-[r:Invite*]->(invitee:User) where inviter.userId = {userId} and not (invitee)-->() RETURN invitee order by invitee.id skip {skip} limit {limit}")
        List<UserProfile> getFarthestInvitee(@Param("userId") Long userId, @Param("skip") Long skip, @Param("limit") Long limit);
    
        @Query("match(head:User{userId:{headUserId}}), (farthest:User{userId:{farthestUserId}}), p=shortestPath((head)-[Invite*]-(farthest)) RETURN p")
        List<UserProfile> getInviteePath(@Param("headUserId") Long headUserId, @Param("farthestUserId") Long farthestUserId);
    
        @Query(value = "match(inviter:User)-[r:Invite]->(invitee:User) where inviter.userId = {userId} return invitee order by invitee.userId desc",
                countQuery = "match(inviter:User)-[r:Invite]->(invitee:User) where inviter.userId = {userId} return count(invitee.userId)")
        Page<UserProfile> getDirectInviteePage(@Param("userId") Long userId, Pageable pageable);
    
        @Query("match(inviter:User)-[r:Invite*]->(invitee:User) where inviter.userId = {userId} return count(invitee.userId)")
        long countAllInvitee(@Param("userId") Long userId);
    
        @Query("match(:User{userId:{topUserId}})-[:Invite*1]->(leg:User)-[:Invite*0..]-> (:User{userId:{childUserId}}) return leg")
        UserProfile getLegUser(@Param("topUserId") Long headUserId, @Param("childUserId") Long childUserId);
    }
    
    
  7. 事务的使用:

    @Transactional(rollbackFor = Exception.class, value = "multiTransactionManager")
    
  8. 使用原生的session进行复杂查询

    @Override
        public List<UserProfile> getAllInvitees(Long inviterId, Long depth, List<Byte> mallingBonusRoleList,
                                                List<Byte> fastStartBonusRoleList, List<Byte> unilevelBonusRoleList, Integer limit) {
            if (inviterId == null) {
                throw new CommonException("inviteeId is null");
            }
            String cql = "match(inviter:User)-[r:Invite*1..{depth}]->(invitee:User) where inviter.userId = {inviterId} ";
            if (depth != null) {
                cql = cql.replace("{depth}", depth.toString());
            } else {
                cql = cql.replace("{depth}", "");
            }
            cql = cql.replace("{inviterId}", inviterId.toString());
    
            if (mallingBonusRoleList != null && mallingBonusRoleList.size() > 0) {
                cql += " and invitee.mallingBonusRole in " + createInCypher(mallingBonusRoleList);
            }
            if (fastStartBonusRoleList != null && fastStartBonusRoleList.size() > 0) {
                cql += " and invitee.fastStartBonusRole in " + createInCypher(fastStartBonusRoleList);
            }
            if (unilevelBonusRoleList != null && unilevelBonusRoleList.size() > 0) {
                cql += " and invitee.unilevelBonusRole in " + createInCypher(unilevelBonusRoleList);
            }
            cql += " return  invitee ";
            if (limit != null) {
                cql += " limit " + limit;
            }
            Session session = SessionFactoryUtils.getSession(sessionFactory);
            Iterable<UserProfile> query = session.query(UserProfile.class, cql, Maps.newHashMap());
            return Lists.newArrayList(query);
        }
    
        @Override
        public String createInCypher(List<?> list) {
            if (list != null && list.size() > 0) {
                StringBuilder stringBuilder = new StringBuilder("[");
                list.forEach(o -> {
                    if (o != null) {
                        if (o instanceof String) {
                            stringBuilder.append("\"").append(o).append("\",");
                        } else {
                            stringBuilder.append(o).append(",");
                        }
                    }
                });
                stringBuilder.deleteCharAt(stringBuilder.length() - 1);
                stringBuilder.append("]");
                return stringBuilder.toString();
            }
            return null;
        }
    
  9. 异常信息解决

    在使用@Transactional(rollbackFor = Exception.class, value = “multiTransactionManager”)作为nei4j的事务管理的时候,commit时报异常:

    org.neo4j.ogm.exception.TransactionManagerException: "Transaction is not current for this thread"
    

    原因:因为springdataneo4j支持的cypher语句里面不支持depth,所以需要手动用原生的session来执行cql,在开启session时,使用了新的session而不是当前方法中当前线程的事务中的session,所以解决办法:

    Session session = sessionFactory.openSession();
    
    

    替换为:

    Session session = SessionFactoryUtils.getSession(sessionFactory);
    
    

你可能感兴趣的:(javaweb技术,互联网技术)