Hibernate最佳实践

1         概述

很多的Java工程师,可能都知道Hibernate是当前优秀的、广泛使用的OR mapping 框架。框架清晰、操作方便、性能优良。

但是,很少知道该框架内部如何实现。同时当前对于应用开发,很多也将涉及到很多技术性的需求,比如:

l         服务启动时,缓存表数据;

l         命名艺术;

l         配置文件加载和解析;

l         唯一键生成;

l         优秀库的使用

l         异常架构设计等等。

我们该如何实现,或者如何更清晰的实现,或者如何更快速的实现?我们从头造车,还是借鉴成功经验。这是摆在我们面前的一个公共的问题。

Hibernate是为我们实现一些技术性需求,提供了很多到技术解决方案。而这些技术方案都蕴涵在它的代码之中。涉及到公共库、设计模式、代码结构、命名艺术。

       通过我的开发经验,分析开发框架源码的经验,我个人认为不应该从头造车,即使是比照设计模式,来写代码,而不查找一些成功案例去设计相对技术性的需求,也是从头造车。我推荐对于技术性需求,查找成功实现方案,可能包括:其他部门的设计方式、开源框架、同事经验等。结合自己系统,借鉴成功案例、框架等。,才能更好,更快的满足业务上快速开发的需求。

       本文就针对Hibernate的框架,通过我该框架代码的阅读和分析,就下面一些内容进行了整理。包括:

l         设计细节和命名艺术

l         最佳实现

l         实现机制

l         优美代码

l         Hql实例

l         参考资料

       对于优秀开源的框架代码或内部或其他团队的优秀代码,进行阅读、分析、应用、总结、发布文档,是成为一名优秀工程师或架构师的必经之路。

引用java大师一句话。Question:是否有一种益智的训练或有趣的行为让你觉得能使你成为一名更好的开发者?Josh Bloch:我认为数学和写作能使你成为更好的开发者。数学与编程一样,要求严谨的思维。而写作会强迫你去组织你的想法。数学和写作都训练了相同的审美机能,而这对于写出好的程序也是必需的。

 

2         设计细节和命名艺术

2.1         用户提供方式-UserSuppliedConnectionProvider

/**

 * An implementation of the <literal>ConnectionProvider</literal> interface that

 * simply throws an exception when a connection is requested. This implementation

 * indicates that the user is expected to supply a JDBC connection.

 * @see ConnectionProvider

 * @author Gavin King

 */

public class UserSuppliedConnectionProvider implements ConnectionProvider {

2.2         连接提供者工厂类-Factory

ConnectionProviderFactory

创建方法: new开头

/**

 * Instantiates a connection provider given either <tt>System</tt> properties or

 * a <tt>java.util.Properties</tt> instance. The <tt>ConnectionProviderFactory</tt>

 * first attempts to find a name of a <tt>ConnectionProvider</tt> subclass in the

 * property <tt>hibernate.connection.provider_class</tt>. If missing, heuristics are used

 * to choose either <tt>DriverManagerConnectionProvider</tt>,

 * <tt>DatasourceConnectionProvider</tt>, <tt>C3P0ConnectionProvider</tt> or

 * <tt>DBCPConnectionProvider</tt>.

 * @see ConnectionProvider

 * @author Gavin King

 */

 

2.3         验证-verify

Environment

verifyProperties(GLOBAL_PROPERTIES);

l          

2.4         验证类是否支持-supports

boolean linkedHashSupport;

       try {

           Class.forName("java.util.LinkedHashSet");

           linkedHashSupport = true;

       }

       catch (ClassNotFoundException cnfe) {

           linkedHashSupport = false;

       }

2.5         委托-Delegate

目的:使得功能类代码更清晰、更容易维护和扩展。

方法:可以一些功能内部实现委托给帮助类实现。

/**

     * The helper instance which contains much of the code which we

     * delegate to.

     */

    protected NestableDelegate delegate = new NestableDelegate( this );

l          

2.6         打开、关闭和创建-open/close/create/build/new

 

2.7         日志提示-processing

对于关键处理,根据具体情况,打印一定日志信息。

log.debug( "processing collection mappings" );

2.8         是否能使用-is*Enable

 

/**

        * Are statistics logged

        */

       public boolean isStatisticsEnabled();

2.9         加载文件方式-ClassLoader

 

// 接受外部传入的加载器

public Configuration addResource(String path, ClassLoader classLoader) throws MappingException {

// 根据当前线程上下文加载器

/**

     * Read mappings from an application resource trying different classloaders.

     * This method will try to load the resource first from the thread context

     * classloader and then from the classloader that loaded Hibernate.

     */

public Configuration addResource(String path) throws MappingException {

              log.info( "Reading mappings from resource: " + path );

              ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

              InputStream rsrc = null;

              if (contextClassLoader!=null) {

                     rsrc = contextClassLoader.getResourceAsStream( path );

              }

              if ( rsrc == null ) {

                     rsrc = Environment.class.getClassLoader().getResourceAsStream( path );

              }

              if ( rsrc == null ) {

                     throw new MappingException( "Resource: " + path + " not found" );

              }

              try {

                     return addInputStream( rsrc );

              }

              catch (MappingException me) {

                     throw new MappingException( "Could not read mappings from resource: " + path, me );

              }

       }

2.10     加载文件和处理字符流分离

 

2.11     文件映射-Mapping

获取映射文件,通过程序加载外围的配置文件,getMapping();

protected String[] getMappings() {

              return new String[] {

                            "cid/Customer.hbm.xml",

                            "cid/Order.hbm.xml",

                            "cid/LineItem.hbm.xml",

                            "cid/Product.hbm.xml"

              };

       }

2.12     Xml解析应用

XMLHelper

public SAXReader createSAXReader(String file, List errorsList, EntityResolver entityResolver) {

public DOMReader createDOMReader() {

2.13     Finally中异常处理

非关键异常,可以通过警告级别进行展示。

public Configuration addInputStream(InputStream xmlInputStream) throws MappingException {

              try {

                     List errors = new ArrayList();

                     org.dom4j.Document doc = xmlHelper.createSAXReader( "XML InputStream", errors, entityResolver )

                                   .read( new InputSource( xmlInputStream ) );

                     if ( errors.size() != 0 ) {

                            throw new MappingException( "invalid mapping", (Throwable) errors.get( 0 ) );

                     }

                     add( doc );

                     return this;

              }

              catch (DocumentException e) {

                     throw new MappingException( "Could not parse mapping document in input stream", e );

              }s

              finally {

                     try {

                            xmlInputStream.close();

                     }

                     catch (IOException ioe) {

                            log.warn( "Could not close input stream", ioe );

                     }

              }

       }

2.14     绑定-bind&binding

private static void bindNamedQuery(Element queryElem, String path, Mappings mappings) {

2.15     客户化-Custom

private static void handleCustomSQL(Element node, Join model) throws MappingException {

 

 

2.16     时间:在。。之时 on

public interface Interceptor {

public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) throws CallbackException;

public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) throws CallbackException;

2.17     功能名称命名ListMap

public class Configuration implements Serializable {

protected Map sqlResultSetMappings;

    protected Map filterDefinitions;

    protected List secondPasses;

    protected List propertyReferences;

2.18     非法的- IllegalArgument

Illegal

2.19     元数据-Metadata

DatabaseMetadata

 

2.20     转换工具-hbm2ddl

2 表示 to,始终常用的表述方式。

2.21     辅助-AuxiliaryDatabaseObject

auxiliary  辅助的, 补充的; 备用的

2.22     修正-qualify

含义:使)具有资格, (使)合格

意图:根据缺省方式,纠正为标准方式。

Table

public static String qualify(String catalog, String schema, String table) {

2.23     表示boolean值方式

isUpdateEnablesupportsUniquehasNext

2.24     在某处取值-in||on

public interface Cache {

public long getSizeInMemory();

public long getElementCountOnDisk();

2.25     唯一值生成器-Timestamper

/**

 * Generates increasing identifiers (in a single VM only).

 * Not valid across multiple VMs. Identifiers are not necessarily

 * strictly increasing, but usually are.

 */

public final class Timestamper {

2.26     命名策-NamingStrategy

* A set of rules for determining the physical column

 * and table names given the information in the mapping

 * document. May be used to implement project-scoped

 * naming standards for database objects.

 

2.27     属性文件中命名方式

采用小写字母

不同单词之间使用下划线方式

hibernate.transaction.factory_class

2.28     自动状态检测update/saveOrUpdate/merge

Usually update() or saveOrUpdate() are used in the following scenario:

·                     the application loads an object in the first session

·                     the object is passed up to the UI tier

·                     some modifications are made to the object

·                     the object is passed back down to the business logic tier

·                     the application persists these modifications by calling update() in a second session

saveOrUpdate() does the following:

·                     if the object is already persistent in this session, do nothing

·                     if another object associated with the session has the same identifier, throw an exception

·                     if the object has no identifier property, save() it

·                     if the object's identifier has the value assigned to a newly instantiated object, save() it

·                     if the object is versioned (by a <version> or <timestamp>), and the version property value is the same value assigned to a newly instantiated object, save() it

·                     otherwise update() the object

and merge() is very different:

·                                 if there is a persistent instance with the same identifier currently associated with the session, copy the state of the given object onto the persistent instance

·                                 if there is no persistent instance currently associated with the session, try to load it from the database, or create a new persistent instance

·                                 the persistent instance is returned

·                                 the given instance does not become associated with the session, it remains detached

 

2.29     -后操作-pre/post

/**

     * Called before a flush

     */

    public void preFlush(Iterator entities) throws CallbackException;

    /**

     * Called after a flush that actually ends in execution of the SQL statements required to synchronize

     * in-memory state with the database.

     */

    public void postFlush(Iterator entities) throws CallbackException;

2.30     实现器-Implementor

public interface SessionImplementor extends Serializable {

2.31     抽象实现-abstract

提供一些公共功能。在接口和具体实现类之间建立一个桥梁。

AbstractSessionImpl

2.32     判断-if

errorIfClosed

doIfPossible

2.33     通过标志位检测Session

l         每一个方法调用前,首先验证session有效性。

public abstract class AbstractSessionImpl implements SessionImplementor {

 

    protected transient SessionFactoryImpl factory;

    private boolean closed = false;

}

 

protected void errorIfClosed() {

       if ( closed ) {

           throw new SessionException( "Session is closed!" );

       }

    }

2.34     调用-invoking

protected boolean invokeDeleteLifecycle(EventSource session, Object entity, EntityPersister persister) {

2.35     清除- evict-clear

public void evict(Class persistentClass) throws HibernateException {

p.getCache().clear();

2.36     验证-verify

 

3         最佳实现

3.1         面向接口的编程-Session/SessionFactory

描述和实现一个较大的功能,通过定义一个对外接口。流行框架:springhibernate都是采用这种方式。

l         结构清晰

l         易于扩展,使用多态性。

3.2         支持多种Db连接提供者-Provider

通常以接口的方式提供通用功能。

public interface ConnectionProvider { // A strategy for obtaining JDBC connections.

包括不同的实现:

l         DriverManagerConnectionProvider

/**

 * A connection provider that uses <tt>java.sql.DriverManager</tt>. This provider

 * also implements a very rudimentary connection pool.

 * @see ConnectionProvider

 * @author Gavin King

 */

l         DatasourceConnectionProvider

/**

 * A connection provider that uses a <tt>DataSource</tt> registered with JNDI.

 * Hibernate will use this <tt>ConnectionProvider</tt> by default if the

 * property <tt>hibernate.connection.datasource</tt> is set.

 * @see ConnectionProvider

 * @author Gavin King

 */

3.3         接口和实现之间的多层架构

多个接口、抽象实现类等。

意图:

l         事件注册

l         公共功能

结构:

l         独立功能采用独立的接口格式

实现:

public final class SessionImpl extends AbstractSessionImpl

       implements EventSource, org.hibernate.classic.Session, JDBCContext.Context {

3.4         禁止实例化类模式

包括:常量类、参数配置类等等。

private Environment() { throw new UnsupportedOperationException(); }

// cannot be instantiated

       private ConnectionProviderFactory() { throw new UnsupportedOperationException(); }

3.5         异常类架构

l         HibernateException

意图:为持久化层定义的基类。

例如:public class SessionException extends HibernateException {

命名启示:1、根据软件品牌命名 2spring按照应用层次命名:DataAccessExcepton

/**

 * Any exception that occurs inside the persistence layer

 * or JDBC driver. <tt>SQLException</tt>s are always wrapped

 * by instances of <tt>JDBCException</tt>.

 *

 * @see JDBCException

 * @author Gavin King

 */

 

public class HibernateException extends NestableRuntimeException {

 

l         JDBCException

意图:打包sql异常。

/**

 * Wraps an <tt>SQLException</tt>. Indicates that an exception

 * occurred during a JDBC call.

 *

 * @see java.sql.SQLException

 * @author Gavin King

 */

public class JDBCException extends HibernateException {

/**

 * An interface to be implemented by {@link java.lang.Throwable}

 * extensions which would like to be able to nest root exceptions

 * inside themselves.

 *

 * @author <a href="mailto:dlr@collab.net">Daniel Rall</a>

 * @author <a href="mailto:knielsen@apache.org">Kasper Nielsen</a>

 * @author <a href="mailto:steven@caswell.name">Steven Caswell</a>

 * @author Pete Gieser

 * @version $Id: Nestable.java 4782 2004-11-21 00:11:27Z pgmjsd $

 * @since 1.0

 */

public interface Nestable {

 

/**

 * The base class of all runtime exceptions which can contain other

 * exceptions.

 *

 * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>

 * @author <a href="mailto:dlr@collab.net">Daniel Rall</a>

 * @author <a href="mailto:knielsen@apache.org">Kasper Nielsen</a>

 * @author <a href="mailto:steven@caswell.name">Steven Caswell</a>

 * @version $Id: NestableRuntimeException.java 8137 2005-09-09 15:21:10Z epbernard $

 * @see org.apache.commons.lang.exception.NestableException

 * @since 1.0

 */

public class NestableRuntimeException extends RuntimeException implements Nestable {

 

3.6         异常工具类

public final class JDBCExceptionHelper {

public final class ExceptionUtils {

3.7         Cache架构

采用Adapter设计模式。

采用多种缓冲实现。

 

A Hibernate Session is a transaction-level cache of persistent data. It is possible to configure a cluster or JVM-level (SessionFactory-level) cache on a class-by-class and collection-by-collection basis. You may even plug in a clustered cache. Be careful. Caches are never aware of changes made to the persistent store by another application (though they may be configured to regularly expire cached data).

By default, Hibernate uses EHCache for JVM-level caching. (JCS support is now deprecated and will be removed in a future version of Hibernate.) You may choose a different implementation by specifying the name of a class that implements org.hibernate.cache.CacheProvider using the property hibernate.cache.provider_class.

Table 19.1. Cache Providers

Cache

Provider class

Type

Cluster Safe

Query Cache Supported

Hashtable (not intended for production use)

org.hibernate.cache.HashtableCacheProvider

memory

 

yes

EHCache

org.hibernate.cache.EhCacheProvider

memory, disk

 

yes

OSCache

org.hibernate.cache.OSCacheProvider

memory, disk

 

yes

SwarmCache

org.hibernate.cache.SwarmCacheProvider

clustered (ip multicast)

yes (clustered invalidation)

 

JBoss TreeCache

org.hibernate.cache.TreeCacheProvider

clustered (ip multicast), transactional

yes (replication)

yes (clock sync req.)

 

3.8         尽量使用jdk已有异常类

Environment

private Environment() { throw new UnsupportedOperationException(); }

 

Configuration

if ( x == chars.length - 1 ) {

                         throw new IllegalArgumentException( "unmatched placeholder start [" + property + "]" );

                     }

3.9         版本向后兼容

Hibernate3 hibernate2兼容时,

l         定义单独包:org.hibernate.classic

/**

 * An extension of the <tt>Session</tt> API, including all

 * deprecated methods from Hibernate2. This interface is

 * provided to allow easier migration of existing applications.

 * New code should use <tt>org.hibernate.Session</tt>.

 * @author Gavin King

 */

public interface Session extends org.hibernate.Session {

 

3.10     同时提供简单和提高实现-default/improved

这是一种策略,为不同应用需求,提供不同的选择。

3.11     每一个细粒度的功能对应独立的类

例如: Table

3.12     TestCase架构

对于开源框架,通常都将提供一个抽象的测试基类,可以应用到自己项目中。好的实践方式包括:

l         程序设计方式

l         文件加载方式

l          

public abstract class TestCase extends junit.framework.TestCase {

 

3.13     event architecture

定义各种的不同事件。

l         对所有Session事件都配置监听器。

l         在系统初始化configuration.configure时,把所有监听器都注册进入。

 

意图:

l         动作和实现分离,当某个动作发生时,触发一个事件,对应的监听器,获得该事件后完成一定的活动。

 

结构:

实现:

l         need a configuration entry telling Hibernate to use the listener in addition to the default listener

l         may register it programmatically

实现:

l         每一个事件,都有一个对应的监听器。

l         监听器,需要预先注册。

l         当某个动作发生时,主动的触发事件。

 

代码示例:

l         public class EventListeners

n         A convience holder for all defined session event listeners

l         AbstractEvent

n         private final EventSource session;

n         Defines a base class for Session generated events.

l         DeleteEvent

l           public class DefaultDeleteEventListener implements DeleteEventListener {

l         SessionImpl

    public void delete(Object object) throws HibernateException {

       fireDelete( new DeleteEvent(object, this) );

  }

l          

3.14     Interceptors and events-callback

It is often useful for the application to react to certain events that occur inside Hibernate. This allows implementation of certain kinds of generic functionality, and extension of Hibernate functionality

3.15     Fetching strategies

A fetching strategy is the strategy Hibernate will use for retrieving associated objects if the application needs to navigate the association. Fetch strategies may be declared in the O/R mapping metadata, or over-ridden by a particular HQL or Criteria query.

Hibernate3 defines the following fetching strategies:

·                                 Join fetching - Hibernate retrieves the associated instance or collection in the same SELECT, using an OUTER JOIN.

·                                 Select fetching - a second SELECT is used to retrieve the associated entity or collection. Unless you explicitly disable lazy fetching by specifying lazy="false", this second select will only be executed when you actually access the association.

·                                 Subselect fetching - a second SELECT is used to retrieve the associated collections for all entities retrieved in a previous query or fetch. Unless you explicitly disable lazy fetching by specifying lazy="false", this second select will only be executed when you actually access the association.

·                                 Batch fetching - an optimization strategy for select fetching - Hibernate retrieves a batch of entity instances or collections in a single SELECT, by specifying a list of primary keys or foreign keys.

 

3.16     配置管理架构-Configuration

3.16.1    基本配置和访问-Environment

l         常量参数配置:

n         软件版本、db访问属性、软件属性、系统属性等

n         配置文件参数定义。采用小写字母、连接词用“_”分割。例如:"hibernate.use_sql_comments"

l         解析方法、存储方式

 

/**

 * Provides access to configuration info passed in <tt>Properties</tt> objects.

 * <br><br>

 * Hibernate has two property scopes:

 * <ul>

 * <li><b>Factory-level</b> properties may be passed to the <tt>SessionFactory</tt> when it

 * instantiated. Each instance might have different property values. If no

 * properties are specified, the factory calls <tt>Environment.getProperties()</tt>.

 * <li><b>System-level</b> properties are shared by all factory instances and are always

 * determined by the <tt>Environment</tt> properties.

 * </ul>

 * The only system-level properties are

 * <ul>

 * <li><tt>hibernate.jdbc.use_streams_for_binary</tt>

 * <li><tt>hibernate.cglib.use_reflection_optimizer</tt>

 * </ul>

 * <tt>Environment</tt> properties are populated by calling <tt>System.getProperties()</tt>

 * and then from a resource named <tt>/hibernate.properties</tt> if it exists. System

 * properties override properties specified in <tt>hibernate.properties</tt>.<br>

 * <br>

 * The <tt>SessionFactory</tt> is controlled by the following properties.

 * Properties may be either be <tt>System</tt> properties, properties

 * defined in a resource named <tt>/hibernate.properties</tt> or an instance of

 * <tt>java.util.Properties</tt> passed to

 * <tt>Configuration.buildSessionFactory()</tt><br>

3.16.2    配置控制类Configuration

l         提供多种加载文件方式, 包括:hbm.xml、属性文件并提供默认方式。

public Configuration configure() throws HibernateException {

           configure( "/hibernate.cfg.xml" );

           return this;

    }

 

public Configuration addResource(String path) throws MappingException {

 

public Configuration addResource(String path, ClassLoader classLoader) throws MappingException {

 

public Configuration addInputStream(InputStream xmlInputStream) throws MappingException {

l         创建SessionFactory,初始化:db基本设置等。

l         生成:创建、删除、更新Schema Script

 

/**

 * Provides access to configuration info passed in <tt>Properties</tt> objects.

 * <br><br>

 * Hibernate has two property scopes:

 * <ul>

 * <li><b>Factory-level</b> properties may be passed to the <tt>SessionFactory</tt> when it

 * instantiated. Each instance might have different property values. If no

 * properties are specified, the factory calls <tt>Environment.getProperties()</tt>.

 * <li><b>System-level</b> properties are shared by all factory instances and are always

 * determined by the <tt>Environment</tt> properties.

 * </ul>

 * The only system-level properties are

 * <ul>

 * <li><tt>hibernate.jdbc.use_streams_for_binary</tt>

 * <li><tt>hibernate.cglib.use_reflection_optimizer</tt>

 * </ul>

 * <tt>Environment</tt> properties are populated by calling <tt>System.getProperties()</tt>

 * and then from a resource named <tt>/hibernate.properties</tt> if it exists. System

 * properties override properties specified in <tt>hibernate.properties</tt>.<br>

 * <br>

 * The <tt>SessionFactory</tt> is controlled by the following properties.

 * Properties may be either be <tt>System</tt> properties, properties

 * defined in a resource named <tt>/hibernate.properties</tt> or an instance of

 * <tt>java.util.Properties</tt> passed to

 * <tt>Configuration.buildSessionFactory()</tt><br>

3.16.3    实例配置Settings和配置工厂

l         Settings

Settings that affect the behaviour of Hibernate at runtime

l         SettingsFactory

n         Reads configuration properties and configures a <tt>Settings</tt> instance.

n         SessionFactory name

n         JDBC and connection settings

n         Interrogate JDBC metadata 获得DB的基本信息

n         SQL Dialect

n         use dialect default properties

n         Transaction settings

n         JDBC and connection settings

n         SQL Generation settings

n         Query parser settings

n         Second-level / query cache

n         SQL Exception converter

n         Statistics and logging

n         Schema export

n          

3.17     函数类化

 

3.18     表字段类化

3.19     Java反射技术应用

3.19.1    反射帮助类

// 根据类名获得类

public static Class classForName(String name, Class caller) throws ClassNotFoundException {

public static Constructor getDefaultConstructor(Class clazz) throws PropertyNotFoundException {

3.19.2    类属性处理

public void initializeListeners(Configuration cfg) {

       Field[] fields = getClass().getDeclaredFields();

       for ( int i = 0; i < fields.length; i++ ) {

           Object[] listeners;

           try {

              Object listener = fields[i].get(this);

              if (listener instanceof Object[]) {

                  listeners = (Object[]) listener;

              }

              else {

                  continue;

              }

 

           }

           catch (Exception e) {

              throw new AssertionFailure("could not init listeners");

           }

 

public void setListeners(String type, String[] listenerClasses) {

       Object[] listeners = (Object[]) Array.newInstance( eventListeners.getListenerClassFor(type), listenerClasses.length );

       for ( int i = 0; i < listeners.length ; i++ ) {

           try {

              listeners[i] = ReflectHelper.classForName( listenerClasses[i] ).newInstance();

           }

           catch (Exception e) {

              throw new MappingException(

                     "Unable to instantiate specified event (" + type + ") listener class: " + listenerClasses[i],

                     e

                  );

           }

       }

       setListeners( type, listeners );

    }

3.20     缺省设计模式

l         为接口提供缺省实现。

l         为属性值设置初始值。

n         EventListeners

l          

3.21     Visitor设计模式

意图:

 

结构:

实现:

l         public abstract class AbstractVisitor {

n          Abstract superclass of algorithms that walk a tree of property values of an entity, and perform specific functionality for collections, components and associated entities.

l         new OnUpdateVisitor( source, id ).process( entity, persister );

3.22     行动(Action)统一管理

意图:

对各种不同行动,统一管理。

描述:

/**

 * Responsible for maintaining the queue of actions related to events.

 * </p>

 * The ActionQueue holds the DML operations queued as part of a session's

 * transactional-write-behind semantics.  DML operations are queued here

 * until a flush forces them to be executed against the database.

 *

 * @author Steve Ebersole

 */

public class ActionQueue {

/**

 * An operation which may be scheduled for later execution.

 * Usually, the operation is a database insert/update/delete,

 * together with required second-level cache management.

 *

 * @author Gavin King

 */

 

结构:

public interface Executable {

3.23     独立复杂日志信息工具类

/**

 * MessageHelper methods for rendering log messages relating to managed

 * entities and collections typically used in log statements and exception

 * messages.

 *

 * @author Max Andersen, Gavin King

 */

public final class MessageHelper {

 

    private MessageHelper() {

    }

public static String infoString(String entityName, Serializable id) {

3.24     异常管理工具

public final class JDBCExceptionReporter {

3.25     版本控制version.properties

l         记录相关组件jar的详细信息。包括:版本、名称、依赖关系

l         依赖关系:

runtime,

required

optional,

runtime,

buildtime

required for standalone operation (outside application server)

l         通过ant脚本,提取信息

3.26     应用工具库

版本检查

versioncheck.lib=versioncheck.jar

versioncheck.version=1.0

versioncheck.name=version checker

versioncheck.when=buildtime

 

风格检查

checkstyle.lib=checkstyle-all.jar

checkstyle.name=Checkstyle

checkstyle.when=buildtime

 

 

3.27     Build设计风格

主要Target

l         Documentation

l         Run a single unit test

l          

 

提供一些好的设计方法:

l         格式化时间

<tstamp>

           <format property="subversion" pattern="yyyy-MM-dd hh:mm:ss"/>

       </tstamp>

l         使用cvs命令

<target name="patch" depends="checkstyle"

           description="Create a patch">

       <cvs command="-q diff -u -N" output="patch.txt"/>

    </target>

l          

l          

3.28     Batch processing

到达一定的行数后,进行提交,并刷新session

Session session = sessionFactory.openSession();
      
      
Transaction tx = session.beginTransaction();
      
      
   
      
      
for ( int i=0; i<100000; i++ ) {
      
      
    Customer customer = new Customer(.....);
      
      
    session.save(customer);
      
      
    if ( i % 20 == 0 ) { //20, same as the JDBC batch size
      
      
        //flush a batch of inserts and release memory:
      
      
        session.flush();
      
      
        session.clear();
      
      
    }
      
      
}
      
      
   
      
      
tx.commit();
      
      
session.close();
      
      

 

4         实现机制

4.1         HQL语言

4.1.1        注意事项

1.在使用字段时,要添加表(即po)的别名。

4.1.2        时间比较

原来 hibernate 支持各数据库函数,只要之前定义了hibernate.dialect=org.hibernate.dialect.SQLServerDialect
使用哪个数据库方言。

4.1.2.1       > <使用

1StringBuffer hql = new StringBuffer(512)

                            .append("from CfgProduction prod where prod.prodStatus='").append(CfgProductionConstants.ProdStatus.PRE_CANCEL)

                            .append("' and sysdate>prod.invalidTime");

2.对时间的直接操作"and (sysdate - c.insertTime <= 31) and c.status =:status");

4.1.2.2       To_char使用

where to_char(field1,'yyyy-MM')='2006-11'

where to_char(field1,'yyyy-MM-dd')=' 2006-11-03 '

4.1.2.3       Between的使用

取某一天的所有数据。

 1 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
 2 Date ld = df.parse(" 2006-01-01 ");
 3 Calendar rd = Calendar.getInstance();
 4 rd.setTime(ld);
 5 rd.set(Calendar.HOUR_OF_DAY, 23);
 6 rd.set(Calendar.MINUTE, 59);
 7 rd.set(Calendar.SECOND, 59);
 8 Query query = sessoin.createQuery("from Order where date between :l and :r");
 9 query.setParameter("l", ld);
10 query.setParameter("r", rd.getTime());
11 return query.list();

4.1.3        Where

where to_char(field1,'yyyy-MM')='2006-11'

4.2         异常处理机制

sessionImpl

       }

              catch (HibernateException he) {

                     log.error("Could not synchronize database state with session");

                     throw he;

              }

4.3         常量类的设计

1.Hibernate的类:MatchMode FlushMode设计的很好。

Usboss中,ProdOperateType ,对操作类型和操作描述成对进行定义。

4.4         批量操作

1. 根据hibernate的实现机制,默认是:操作了20条数据后,进行session.flush();session.clear(),然后开始新的循环。

4.5         StringHelper

       public static final String EMPTY_STRING="";

       public static final char DOT='.';

       public static final char UNDERSCORE='_';

       public static final String COMMA_SPACE=", ";

       public static final String COMMA = ",";

       public static final String OPEN_PAREN = "(";

       public static final String CLOSE_PAREN = ")";

    public static final char SINGLE_QUOTE = '/'';

4.6         MessageHelper

Helper methods for rendering log messages and exception

4.7         Insert类的设计

Insert  使用了SequencedHashMap

4.8         SequencedHashMap

来自commons-collections- 2.1.1

4.9         SringBuffer的使用

Insert 

StringBuffer buf = new StringBuffer( columns.size()*15 + tableName.length() + 10 );

4.10     IdentifierGeneratorFactory

 

4.11     Transient变量的使用

SessionImpl的大量的transient

 

Implementors must be threadsafe (preferrably immutable) and must provide a constructor of type (net.sf.hibernate.map.PersistentClass, net.sf.hibernate.impl.SessionFactoryImplementor).

4.12     Accessor类的设计

BasicPropertyAccessor 内部类的实现方式

public static final class BasicSetter implements Setter {

4.13     工具类的设计

1. 操作配置文件的类 ConfigHelper  ClassLoader的使用机制。

2. 操作XML文件的类 XMLHelper

4.14      一个持久化对象对应多session的情况

错误情况:使用update有时候会出现Illegal attempt to associate a collection with two open sessions的异常,但是在取得对象后使用session.close()也不行,会出现另外的一个异常session is not open or is closed,原来就是getHibernateTemplate().update(object),修改为getHibernateTemplate().merge(object)问题解决。

 

解决办法:

1.    hibernate2.1中,没有merge函数,在3.*版本中已经有了该方法。但是可以采用saveOrUpdateCopy方法,它的实现代码和merge是一样的。源代码是一样的。

2.    代码注释是:

       /**

        * Copy the state of the given object onto the persistent object with the same

        * identifier. If there is no persistent instance currently associated with

        * the session, it will be loaded. Return the persistent instance. If the

        * given instance is unsaved or does not exist in the database, save it and

        * return it as a newly persistent instance. Otherwise, the given instance

        * does not become associated with the session.

        *

        * @param object a transient instance with state to be copied

        * @return an updated persistent instance

        */

       public Object saveOrUpdateCopy(Object object) throws HibernateException;

 

       /**

        * Copy the state of the given object onto the persistent object with the same

        * identifier. If there is no persistent instance currently associated with

        * the session, it will be loaded. Return the persistent instance. If the

        * given instance is unsaved, save a copy of and return it as a newly persistent

        * instance. The given instance does not become associated with the session.

        * This operation cascades to associated instances if the association is mapped

        * with <tt>cascade="merge"</tt>.<br>

        * <br>

        * The semantics of this method are defined by JSR-220.

        *

        * @param object a detached instance with state to be copied

        * @return an updated persistent instance

        */

       public Object merge(Object object) throws HibernateException;

 

merge(Object entity)
          Copy the state of the given object onto the persistent object with the same identifier.
update(Object entity)
          Update the given persistent instance, associating it with the current Hibernate Session.

参考文档:如果你确定当前session没有包含与之具有相同持久化标识的持久实例,使用update() 如果想随时合并你的的改动而不考虑session的状态,使用merge()

我的理解:update只能在保证在session中只有一个持久化标示时,可以使用update,如果你在从数据库或者其他地方得到了另一个持久化对象,标示跟以前的某个对象一样,这个时候,只能用merge

4.15     延期关闭策略

如果在一个事务中读取一个object,而在同一个请求中的另外一个事务中保存这个object,这个集合仍然关联到原始load他们的那个session上(仍然处于open状态)-这样就会返回以下错误:

 

org.springframework.orm.hibernate.HibernateSystemException: Illegal attempt to associate a collection with two open sessions;
nested exception is net.sf.hibernate.HibernateException: Illegal attempt to associate a collection with two open sessions
net.sf.hibernate.HibernateException: Illegal attempt to associate a collection with two open

 

所以要在同一个请求的同一个事务中执行加载-修改-保存这个周期,以保证他们是被同一个Hibernate session操作

 

这个问题在Hibernate删除对象的时候表现的最严重,因为Hibernate删除一个对象要生成二条sql语句,二次于数据库交互,如果在Delete中又使用了load来获得对象的话,那就需要生成三条语句,这时候事务就成了大问题,如果不能在同一个Session中来处理这个问题就非常的麻烦

 

所以在Spring中的HibernateTemplateSpring写了一个closeSessionIfNecessary(Session session)方法来判断事务是否结束,而在自己写的DAO中当多个如果想运行这样的操作,就不能在单个方法中closeSession,而应该进行判断,事务是否结束,可在DAO中包含下列方法:然后在每个CRUD方法后面吧closeSession替换成closeSessionIfNecessary

       public static void closeSessionIfNecessary(Session session, SessionFactory sessionFactory) {

              if (session == null) {

                     return;

              }

              SessionHolder sessionHolder =

                  (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

              if (sessionHolder != null && sessionHolder.containsSession(session)) {

                     return;

              }

              closeSessionOrRegisterDeferredClose(session, sessionFactory);

       }

 

4.16      FlushMode模式

1.    read-only mode (i.e. FlushMode.NEVER)

2.     

4.17     保证数据库表数据和hbm对应

1、 字段是否为空对应

2、 字段类型要正确,例如:数据库是varchar,导入数据时,就不能是number型的。

:exception setting property value with CGLIB (set hibernate.cglib.use_reflection_optimizer=false for more info) getter of com.qnuse.zj114boss.po.CfgEnterpriseOrderRelationPK.?; nested exception is net.sf.hibernate.PropertyAccessException:

3、  

 

4.18     RowCallbackHandler

回调方法的实现机制。类似与C/C++的函数的参数中传递函数。

可以实现RowCallbackHandler接口的一个类,完成对行的数据进行逻辑处理,例如:对一定行数的记录进行入库操作等,不同的逻辑可以实现不同子类。

    public void query(String sql, RowCallbackHandler handler) throws DaoException{

       if (logger.isDebugEnabled()) {

           logger.debug("执行查询SQL语句,并逐条处理结果数据,SQL=[" + sql + "]");

       }

       try {

           getJdbcTemplate().query(sql, handler);

       } catch (DataAccessException e) {

           logger.warn(e.getMessage());

           throw new DaoException(e.getMessage(), e);

       }

    }

 

类似的回调机制:

JdbcTemplate

public Object query(final String sql, final ResultSetExtractor rse) throws DataAccessException {

       Assert.notNull(sql, "SQL must not be null");

       Assert.notNull(rse, "ResultSetExtractor must not be null");

       if (logger.isDebugEnabled()) {

           logger.debug("Executing SQL query [" + sql + "]");

       }

 

       class QueryStatementCallback implements StatementCallback, SqlProvider {

           public Object doInStatement(Statement stmt) throws SQLException {

              ResultSet rs = null;

              try {

                  rs = stmt.executeQuery(sql);

                  ResultSet rsToUse = rs;

                  if (nativeJdbcExtractor != null) {

                     rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);

                  }

                  return rse.extractData(rsToUse);

              }

              finally {

                  JdbcUtils.closeResultSet(rs);

              }

           }

           public String getSql() {

              return sql;

           }

       }

       return execute(new QueryStatementCallback());

    }

4.19     jdbcTemplate

4.19.1    queryForInt

sql.append("select SEQ_HAND_CHARGE_HISTORY.nextval from dual");

       long sn = queryForInt(sql.toString());

4.19.2    queryForObject

//Execute a query for a result object, given static SQL.

String sql = "select execute_time from cfg_group_send_plan where oid="+planId;

       Date groupSendTime = (Date)getBaseJdbcDao().queryForObject(sql, Date.class);

4.19.3    query RowMapper

     * Execute a query given static SQL, mapping each row to a Java object

     * via a RowMapper.

 

4.19.4    query RowCallbackHandler

getBaseJdbcDao().query(sql.toString(),

              new ExportToFileRowCountCallbackHandler(BUFFER_NUMBER,

                     getAbsoluteExportFile(date), DATA_SEPARATOR));

4.19.5    其他

//  每一个参数对应符号

// return a List that contains a Map per row

List queryForList(String sql, Object[] args) throws DataAccessException;

// 实例

StringBuilder contentSQL = new StringBuilder(100);

       contentSQL.append("SELECT CONTENT_CODE,CP_ID,FILE_PATH,FEE,PROGRAM_ID,CONTENT_TYPE_ID FROM ").append(tableName);

       contentSQL.append(" WHERE UPPER_CONTENT_NAME = ?");

       List<Map> contentResult = queryForList(contentSQL.toString(), new Object[]{StringUtils.upperCase(contentName)}); //this.queryForList(contentSQL.toString());

       if(contentResult.isEmpty())

       {

           throw new AuthorizationException("没有找到内容文件");

       }

       ContentTO content = new ContentTO();

       content.contentCode = (String) contentResult.get(0).get("CONTENT_CODE");

       content.filePath = (String) contentResult.get(0).get("FILE_PATH");

4.20     session的使用

注意: spring中,可以直接的用getSession直接的获得session

/**

        * 处理一行数据,完成数据校验、入库、内存数据库同步

        * @param batchImportParameter 批量操作的参数

        * @param line                批量文件中的一行数据

        * @param lineNum           数据行号

        * @param batchId            批次号

        * @return badRow            错误行数据

        */

       private BadRow processImportLine(BatchImportParameter batchImportParameter,String line,int lineNum, String batchId) throws Exception{

              if (logger.isDebugEnabled()) {

                     StringBuffer content = new StringBuffer();

                     content.append("line=").append(line);

                     content.append("lineNum=").append(lineNum);

                     content.append("batchId=").append(batchId);

                     logger.debug("start of the processImportLine:"+content.toString());

              }

             

              if (StringUtils.isEmpty(line)) {

                     BadRow badRow =new BadRow();

                     badRow.setRowNum(lineNum);

                     badRow.setErrMessage("空行");

                     return badRow;

              }

              String[] elements = line.split(","); // 英文的分割符号

               

              BadRow badRow = validate(elements);

              if(badRow != null && badRow.isExistErrMessage()){

                     badRow.setRow(line);

                     badRow.setRowNum(lineNum);

                     return badRow;

              }

             

              Transaction transaction = null;

              SessionFactory sessionFactory = getSessionFactory();

              Session session = sessionFactory.openSession();

              try {

                     BaseHibernateObject po = getPo(elements, batchId, batchImportParameter);

                     checkDuplicate(po);

 

                     transaction = session.beginTransaction();

                     create(po);

                     transaction.commit();

                    

                     mdbSynchronize(po, MdbRefreshUtil.ACTION_INSERT);          

              } catch (DaoException e) { 

                     badRow = new BadRow();

                     badRow.setRowNum(lineNum);

                     badRow.setRow(line);

                     badRow.setErrMessage(e.getMessage());

              } catch(Exception e) {

                     e.printStackTrace();

                     badRow = new BadRow();

                     badRow.setRowNum(lineNum);

                     badRow.setRow(line);

                     badRow.setErrMessage(e.getMessage());

                     if(transaction != null){

                            transaction.rollback();

                     }

                     throw e;

              } finally {

                     session.close();

              }

              return badRow;

       }

 

5         优美代码

5.1         解析类名称

public static String unqualify(String qualifiedName) {

       int loc = qualifiedName.lastIndexOf(".");

       return ( loc < 0 ) ? qualifiedName : qualifiedName.substring( qualifiedName.lastIndexOf(".") + 1 );

    }

 

5.2         转换类名为表名称

protected static String addUnderscores(String name) {

       StringBuffer buf = new StringBuffer( name.replace('.', '_') );

       for (int i=1; i<buf.length()-1; i++) {

           if (

              Character.isLowerCase( buf.charAt(i-1) ) &&

              Character.isUpperCase( buf.charAt(i) ) &&

              Character.isLowerCase( buf.charAt(i+1) )

           ) {

              buf.insert(i++, '_');

           }

       }

       return buf.toString().toLowerCase();

    }

6         Hql实例

6.1         Fetch

In addition, a "fetch" join allows associations or collections of values to be initialized along with their parent objects, using a single select. This is particularly useful in the case of a collection. It effectively overrides the outer join and lazy declarations of the mapping file for associations and collections

 

6.2         Set方式设置参数值

queryStringBuffer.append("select distinct d from ").append(getClassName(getPoClass())).append(" d, " +

                            "CfgOrderRelation c where ((not exists (select c.comp_id.accountId from CfgOrderRelation c " +

                                   "where d.accNumber = c.comp_id.accountId)) or (d.accNumber = c.comp_id.accountId " +

                                   "and c.comp_id.serviceId not like :notLikeServiceId)) " +

                                   "and d.smsSentStatus =:smsSentStatus order by d.accNumber");

             

              logger.debug("/n== the queryString is: " + queryStringBuffer.toString());

              try {

                     Query queryObject = null;

                     queryObject = getHibernateTemplate().createQuery(getSession(), queryStringBuffer.toString());

                     queryObject.setString("notLikeServiceId", "YYHB%");

                     queryObject.setString("smsSentStatus", DatYyhbAccountConsts.SmsSentStatus.NO_SENT_SMS);

                    

                     logger.debug("end of the buildQueryCondition()");

7         Hibernate Using Best Practice

Write fine-grained classes and map them using <component>.

Use an Address class to encapsulate street, suburb, state, postcode. This encourages code reuse and simplifies refactoring.

Declare identifier properties on persistent classes.

Hibernate makes identifier properties optional. There are all sorts of reasons why you should use them. We recommend that identifiers be 'synthetic' (generated, with no business meaning).

Identify natural keys.

Identify natural keys for all entities, and map them using <natural-id>. Implement equals() and hashCode() to compare the properties that make up the natural key.

Place each class mapping in its own file.

Don't use a single monolithic mapping document. Map com.eg.Foo in the file com/eg/Foo.hbm.xml. This makes particularly good sense in a team environment.

Load mappings as resources.

Deploy the mappings along with the classes they map.

Consider externalising query strings.

This is a good practice if your queries call non-ANSI-standard SQL functions. Externalising the query strings to mapping files will make the application more portable.

Use bind variables.

As in JDBC, always replace non-constant values by "?". Never use string manipulation to bind a non-constant value in a query! Even better, consider using named parameters in queries.

Don't manage your own JDBC connections.

Hibernate lets the application manage JDBC connections. This approach should be considered a last-resort. If you can't use the built-in connections providers, consider providing your own implementation of org.hibernate.connection.ConnectionProvider.

Consider using a custom type.

Suppose you have a Java type, say from some library, that needs to be persisted but doesn't provide the accessors needed to map it as a component. You should consider implementing org.hibernate.UserType. This approach frees the application code from implementing transformations to / from a Hibernate type.

Use hand-coded JDBC in bottlenecks.

In performance-critical areas of the system, some kinds of operations might benefit from direct JDBC. But please, wait until you know something is a bottleneck. And don't assume that direct JDBC is necessarily faster. If you need to use direct JDBC, it might be worth opening a Hibernate Session and using that JDBC connection. That way you can still use the same transaction strategy and underlying connection provider.

Understand Session flushing.

From time to time the Session synchronizes its persistent state with the database. Performance will be affected if this process occurs too often. You may sometimes minimize unnecessary flushing by disabling automatic flushing or even by changing the order of queries and other operations within a particular transaction.

In a three tiered architecture, consider using detached objects.

When using a servlet / session bean architecture, you could pass persistent objects loaded in the session bean to and from the servlet / JSP layer. Use a new session to service each request. Use Session.merge() or Session.saveOrUpdate() to synchronize objects with the database.

In a two tiered architecture, consider using long persistence contexts.

Database Transactions have to be as short as possible for best scalability. However, it is often neccessary to implement long running application transactions, a single unit-of-work from the point of view of a user. An application transaction might span several client request/response cycles. It is common to use detached objects to implement application transactions. An alternative, extremely appropriate in two tiered architecture, is to maintain a single open persistence contact (session) for the whole lifecycle of the application transaction and simply disconnect from the JDBC connection at the end of each request and reconnect at the beginning of the subsequent request. Never share a single session across more than one application transaction, or you will be working with stale data.

Don't treat exceptions as recoverable.

This is more of a necessary practice than a "best" practice. When an exception occurs, roll back the Transaction and close the Session. If you don't, Hibernate can't guarantee that in-memory state accurately represents persistent state. As a special case of this, do not use Session.load() to determine if an instance with the given identifier exists on the database; use Session.get() or a query instead.

Prefer lazy fetching for associations.

Use eager fetching sparingly. Use proxies and lazy collections for most associations to classes that are not likely to be completely held in the second-level cache. For associations to cached classes, where there is an a extremely high probability of a cache hit, explicitly disable eager fetching using lazy="false". When an join fetching is appropriate to a particular use case, use a query with a left join fetch.

Use the open session in view pattern, or a disciplined assembly phase to avoid problems with unfetched data.

Hibernate frees the developer from writing tedious Data Transfer Objects (DTO). In a traditional EJB architecture, DTOs serve dual purposes: first, they work around the problem that entity beans are not serializable; second, they implicitly define an assembly phase where all data to be used by the view is fetched and marshalled into the DTOs before returning control to the presentation tier. Hibernate eliminates the first purpose. However, you will still need an assembly phase (think of your business methods as having a strict contract with the presentation tier about what data is available in the detached objects) unless you are prepared to hold the persistence context (the session) open across the view rendering process. This is not a limitation of Hibernate! It is a fundamental requirement of safe transactional data access.

Consider abstracting your business logic from Hibernate.

Hide (Hibernate) data-access code behind an interface. Combine the DAO and Thread Local Session patterns. You can even have some classes persisted by handcoded JDBC, associated to Hibernate via a UserType. (This advice is intended for "sufficiently large" applications; it is not appropriate for an application with five tables!)

Don't use exotic association mappings.

Good usecases for a real many-to-many associations are rare. Most of the time you need additional information stored in the "link table". In this case, it is much better to use two one-to-many associations to an intermediate link class. In fact, we think that most associations are one-to-many and many-to-one, you should be careful when using any other association style and ask yourself if it is really neccessary.

Prefer bidirectional associations.

Unidirectional associations are more difficult to query. In a large application, almost all associations must be navigable in both directions in queries.

 

8         参考资料

1. www.hibernate.org

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Hibernate 是一个开源的O/R mappimg的框架,基于JDBC提供了一种持久性数据管理的方案,相对于EntityBean来说是相当轻量级的。由于Hibernate是基于 JDBC的,所以它的数据库查寻的能力相对于CMP来说也是异常强大的,Hibernate自身也提供了HQL查寻语句。 一个最简单的Hibernate project(不涉及Struts, Tomcat, XDoclet,JBoss等东东)必须的几个东东: 1. Hibernate工具包。 2. JDBC数据库连接驱动。以mysql为例,mysql-connector-java-3.1.×-bin.jar。 3. 配置文件。 1) Hibernate全局配置文件,hibernate.properties或者hibernate.cfg.xml.。一般使用XML文件。 2) 数据O/R mapping 配置文件,也就是数据库中每一条记录的详细说明,包括field, PrimaryKey等。*.hbm.xml,*一般用映射到该类记录的Class的名称表示。 ------------------------ 开发一个Hibernate时有几个工具还是挺好用的 1. Middlegen-Hibernate,用来自动生成对象映射的配置文件。感觉配置起来也挺麻烦的,不过对于有一大坨的mapping对象的cfg文件来说倒是很省事的,关键是避免出错了。 2. Hibernate Extention,用来自动生成与那些*.hbm.xml对应的POJO,也就是根据那些对象关系映射的配置文件生成相应的class文件。 HibernateEx里面有一个hbm2java工具,就是用来根据些配置文件生成相应的POJO class。另外还有两个东东,一个是class2hbm,与第一个相反,是根据class来导出映射文件的。还有一个ddl2hbm,是根据数据库来导出表结构,并生成映射文件和POJO class。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值