`
atianchen
  • 浏览: 25293 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
文章分类
社区版块
存档分类
最新评论

Hibernate自定义表单完全解决方案(无需重置SessionFactory)

阅读更多
最近开发的一个系统,需要在不更改代码和重启系统的情况下提供对用户自动建表的支持,由于系统应用了hibernate,所以在建表同时也要建立持久化对象以及对这些对象注册,人渣我首先想倒的是 baidu和google,哪知一番搜索下来,发现都不尽入人意,于是乎,造轮子之路开始了
数据库我是采用的oracle9i,目前在比如数据库类型支持,还有对象关系支持上都很简单,不过在现有基础上进行扩展,都是可以实现的
实现步骤如下
建立class->生成hbm.xml->在Hibernate'config里面注册持久化类->通知SessionFactory持久化类的新增
1 准备
首先准备基础数据,我建立了几个类来对生成的表和属性做描述 这些描述都将作为传输传递给class生成方法和hbm.xml的生成方法
RenderClass 描述要生成的实体类 属性如下
  • className 类名
  • tableName 对应表名
  • properties 属性集合

RenderProperty 就是properties集合的内容 属性如下
  • name 属性名称
  • type java类型
  • field 字段名
  • primary 是否主键
  • sequence 对应ID生成的sequence
  • length 对应长度

2 生成class
采用ASM 生成
	/**
	 * 根据传入参数创建CLASS文件
	 *
	 * @param 类名
	 * @param 保存路径
	 * @param 属性描述
	 * @return Class
	 */
	public  Class build(String clsname,String savepath,Collection properties)
	{
		Class cls = null;
		try
		{
			String classname = BuildUtil.transferClassName(clsname);
			ClassWriter cw = new ClassWriter(false);   
			//建立构造函数
			cw.visit(V1_1, ACC_PUBLIC, classname, null, "java/lang/Object", null);   
			MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);   
			mw.visitVarInsn(ALOAD, 0);   
			mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");   
			mw.visitInsn(RETURN);   
			mw.visitMaxs(1, 1);   
			mw.visitEnd();
			
			BuildProperty property = null;
			String propertytype = null;
			String propertyname = null;;
			//建立属性
			Iterator iterator = properties.iterator();
			while (iterator.hasNext())
			{
				//建立属性对应的类变量
				property = (BuildProperty)iterator.next();
				propertytype = BuildUtil.transferClassName(property.getType());
				propertyname = WordUtils.capitalize(property.getName());
				cw.visitField(ACC_PRIVATE, property.getName(), "L"+propertytype+";", null, null).visitEnd();
				//建立get方法
			    mw = cw.visitMethod(ACC_PUBLIC, "get"+propertyname, "()L"+propertytype+";", null, 
	    		null); 
			    mw.visitCode(); 
			    mw.visitVarInsn(ALOAD, 0); 
			    mw 
	    		.visitFieldInsn(GETFIELD, classname, property.getName(), 
	    		"L"+propertytype+";"); 
			    mw.visitInsn(ARETURN); 
			    mw.visitMaxs(1, 1); 
			    mw.visitEnd(); 
			    //建立set方法
			    mw = cw.visitMethod(ACC_PUBLIC, "set"+propertyname, "(L"+propertytype+";)V", 
			    		null, null); 
			    mw.visitCode(); 
			    mw.visitVarInsn(ALOAD, 0); 
			    mw.visitVarInsn(ALOAD, 1); 
			    mw 
	    		.visitFieldInsn(PUTFIELD, classname, property.getName(), 
	    		"L"+propertytype+";"); 
			    mw.visitMaxs(2, 2); 
			    mw.visitInsn(RETURN); 
			    mw.visitEnd(); 
			}
			
			cw.visitEnd();

		    byte[] code = cw.toByteArray(); 
			if (savepath!=null)
			{  
				Assistant.createNewFile(savepath);
			    FileOutputStream fos = new FileOutputStream(savepath);   
			    fos.write(code);   
			    fos.close();   
			}
			cls = this.defineClass(clsname, code, 0, code.length);
			return cls;
		}
		catch (Throwable e)
		{
			e.printStackTrace();
		}
		return cls;
	}

3 生成hbm.xml
生成这种事情,当然是交给模板了,我这里用的是Freemarker,在这里,我要感谢freemarker的开发组,感谢webwork 感谢XXCMS系统对freemarker以及其他模板语言的大力推广,,感谢。。。。(省略100字)
模板嘛,肯定不仅仅是应用在web环境的,看看源代码下的freemarker.cache包,恩,不错,freemarker团队确实是有一定“野心”的 首先是渲染的实现
package com.mit.cooperate.core.asm.render;

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;

import javax.servlet.ServletContext;

import com.mit.cooperate.core.util.Assistant;

import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleHash;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
/**
 * freeMarker模板渲染
 * @author courser.tijichen
 */
public class FreemarkerRender implements Render{
	
	private Configuration templateconfig;
	
	public  FreemarkerRender()
	{
		this.initialize();
	}
	/**
	 * 初始化freemarker配置
	 *
	 */
	public void initialize()
	{
		try
		{
			if (templateconfig==null)
			{
				templateconfig = new Configuration();
				templateconfig.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
				templateconfig.setObjectWrapper(ObjectWrapper.DEFAULT_WRAPPER);
				templateconfig.setTemplateLoader(new ClassTemplateLoader(this.getClass(),"/"));
				templateconfig.setTemplateUpdateDelay(1200);
				templateconfig.setDefaultEncoding("gb2312");
				templateconfig.setLocale(new java.util.Locale("zh_CN"));
				templateconfig.setNumberFormat("0.##########");
			}
		}
		catch (Exception e)
		{}
	}
	/**
	 * 渲染
	 *
	 */
	public void render(RenderClass target,String template,String outpath)
	{
		try
		{
			StringWriter writer = new StringWriter();
			Template tl = templateconfig.getTemplate(
					template,
					templateconfig.getLocale());
			SimpleHash root = new SimpleHash();
			root.put("entity", target);
			tl.process(root, writer);
			Assistant.createNewFile(outpath);
			FileOutputStream fos = new FileOutputStream(outpath);
			PrintWriter pwriter = new PrintWriter(fos);
			pwriter.write(writer.toString());
			pwriter.close();
			fos.close();   
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

然后就是准备模板了,首先声明,我这个模板写得简单粗陋之极,不过粗陋简单不正是人渣偶的终极处世之道么,何况只实现几个简单功能,仅仅是针对测试而已嘛,何必搞得那么正规严肃了,嘿嘿
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="${entity.className}" table="${entity.tableName}">
    	<#if entity.properties?exists>
			<#list entity.properties as property>
				<#if property.primary>
					<id name="${property.name}" type="${property.type}">
				<#else>
					<property name="${property.name}" type="${property.type}">
				</#if>
				<#if property.type=="java.lang.String">
					<column name="${property.field?upper_case}" <#if property.length?exists>length="${property.length}"</#if>></column>
				<#elseif property.type=="java.util.Date">
						<column name="${property.field?upper_case}" length=7></column>
				<#elseif property.type=="java.lang.Long" || property.type=="java.lang.Integer"
					|| property.type=="java.lang.Short">
						<column name="${property.field?upper_case}" <#if property.length?exists>precision="${property.length}"</#if> scale="0"></column>
				</#if>
				<#if property.primary==true>
					<#if property.sequence?exists>
						<generator class="sequence">
							<param name="sequence">${property.sequence}</param>
						</generator>
					</#if>
					</id>
				<#else>
					</property>
				</#if>
			</#list>
		</#if>
    </class>
</hibernate-mapping>

4 注册
首先是对生成的hbm.xml的注册,比如,我获取倒Hibernate的一个config以后
URL  url = this.getClass().getResource("/com/mit/test/person.hbm.xml");
config.addURL(url);

然后就是要通知sessionFactory,新增了持久类,目前很多方法都是重启sessionfactory,就是关闭当前 sessionFactory ,然后根据config build一个新的sessionFactory出来,但是,这种情况感觉总不那么完美,虽然这个过程持续不到多长时间,但用户每增一个表就close然后build一个,单说用户体验,人家正在提交数据了,你把这个给close了....
但目前hibernate包的 sessionFactory 确实没提供这种对持久类的add支持,XX伟人说过:没有条件 创造条件也要上,于是乎,拿起你的键盘,启动 editplus,向hibernate3的源码砍去。。

其实,改动地方也不大。
一是,改动Configuration,三句话
	public Mapping getMapping()
	{
		return this.mapping;
	}

然后是SessionFactoryImpl 我们要让他知道,这个世界上还存在很多未知的来客,需要你去主动了解。。。。
增加代码如下
	public void addPersistentClass(PersistentClass model,Mapping mapping)
	{
		if ( !model.isInherited() ) {
			IdentifierGenerator generator = model.getIdentifier().createIdentifierGenerator(
					settings.getDialect(),
			        settings.getDefaultCatalogName(),
			        settings.getDefaultSchemaName(),
			        (RootClass) model
			);
			if (!identifierGenerators.containsKey(model.getEntityName()))
				identifierGenerators.put( model.getEntityName(), generator );
		}
		
		model.prepareTemporaryTables( mapping, settings.getDialect() );
		String cacheRegion = model.getRootClass().getCacheRegionName();
	
		CacheConcurrencyStrategy cache = CacheFactory.createCache(
					model.getCacheConcurrencyStrategy(),
			        cacheRegion,
			        model.isMutable(),
			        settings,
			        properties
				);
		if (cache!=null)
			allCacheRegions.put( cache.getRegionName(), cache.getCache() );
			
		
		EntityPersister cp = PersisterFactory.createClassPersister(model, cache, this, mapping);
		if ( cache != null && cache.getCache() instanceof OptimisticCache ) {
			( ( OptimisticCache ) cache.getCache() ).setSource( cp );
		}
		entityPersisters.put( model.getEntityName(), cp );
	}

最后遗留的是hbm.xml在cfg.xml的注册,以保证系统重启后可以顺利加载新增的持久化配置
	/**
	 * 把hbm.xml的路径加入到cfg.xml的mapping结点
	 *
	 * @param cfg.xml的路径
	 * @param hbm.xml的路径
	 */
	public static void updateHbmCfg(URL url,String hbm)
	{
		try
		{
			SAXReader reader = new SAXReader();
			Document doc = reader.read(url);
			Element element = (Element)doc.getRootElement()
			.selectSingleNode("session-factory");
			
			Element hbmnode = element.addElement("mapping");
			hbmnode.addAttribute("resource", hbm);
			String filepath = url.getFile();
			if (filepath.charAt(0)=='/')
				filepath = filepath.substring(1);
			FileOutputStream outputstream = new FileOutputStream(filepath);
			XMLWriter writer = new XMLWriter(outputstream);
			writer.write(doc);
			outputstream.close();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

大功告成 ,先运行一个小周天

最后是总结测试,写个junit 搞定 代码如下:
package com.mit.cooperate.core.hibernate;

import junit.framework.TestCase;

import java.net.URL;
import java.util.ArrayList;

import org.apache.commons.beanutils.PropertyUtils;

import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
import org.hibernate.SessionFactory;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.impl.SessionFactoryImpl;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.Transaction;

import com.mit.cooperate.core.asm.*;
import com.mit.cooperate.core.asm.render.*;

public class HibernateTest extends TestCase {
	
	private Configuration config;
	private SessionFactory factory;
	
	public void setUp()
	{
		URL  url = this.getClass().getResource("/com/mit/cooperate/core/hibernate/hibernate.cfg.xml");
		config = new Configuration().configure(url);
		factory = config.buildSessionFactory();
	}
	
	public void testBuild() throws Exception
	{
		//持久类对象描述
		RenderClass rc = new RenderClass();
		ArrayList list = new ArrayList();
		
		RenderProperty property = new RenderProperty();
		//添加主键
		property.setName("oid");
		property.setField("oid");
		property.setLength(new Integer(15));
		property.setPrimary(true);
		property.setType(Long.class.getName());
		property.setSequence("SEQ_PERSON");
		
		list.add(property);
		//添加一个name字段
		property = new RenderProperty();
		property.setName("name");
		property.setType(String.class.getName());
		property.setField("name");
		property.setLength(new Integer(20));
		
		list.add(property);
		
		rc.setProperties(list);
		//类名
		rc.setClassName("com.mit.test.Person");
		rc.setTableName("person");
		//开始生成class
		POBuildUtil util = new POBuildUtil();
		util.build(rc.getClassName(),"E:\\cpc\\source\\cooperateCore\\com\\mit\\test\\Person.class",list);
		//实例化一个person
		Object person = Class.forName("com.mit.test.Person").newInstance();//hbmcls.newInstance();
		
		//开始生成hbm.xml
		FreemarkerRender render = new FreemarkerRender();
		render.render(rc, Templates.TEMPLATE_HIBERNATE3, "E:\\cpc\\source\\cooperateCore\\com\\mit\\test\\person.hbm.xml");
		URL  url = this.getClass().getResource("/com/mit/test/person.hbm.xml");
		config.addURL(url);
		//更新hibernate.cfg.xml
		HibernateUtil.updateHbmCfg( this.getClass().getResource("/com/mit/cooperate/core/hibernate/hibernate.cfg.xml"), "com/mit/test/person.hbm.xml");
		
		PersistentClass model = config.getClassMapping("com.mit.test.Person");
		//sessionFactory哪下子,快接纳person爷爷进去
		((SessionFactoryImpl)factory).addPersistentClass(model, config.getMapping());
		//生成数据库
		SchemaExport export = new SchemaExport(config,((SessionFactoryImpl)factory).getSettings());
		export.execute(true, true,false,true);
		//测试一下,随便给个名字什么的
		PropertyUtils.setProperty(person, "name", "chenzhi");
		Session session = factory.openSession();
		Transaction tran = session.beginTransaction();
		try
		{
			//保存
			session.save(person);
			tran.commit();
		}
		catch (Exception e)
		{
			e.printStackTrace();
			tran.rollback();
		}
		finally
		{
			session.close();
		}
	}
	
	public void tearDown()
	{
		factory.close();
	}
	
	
}
分享到:
评论
40 楼 eastlift 2008-07-17  
这个需求很有必要,我也研究了下
http://www.infoq.com/cn/articles/hibernate-custom-fields
这也是一种实现方式。

二年前也曾试写过另一种将数据库表结构横加一系统附加属性表的写法。
总之两种方案:

一、动态建表,动态扩充
二、附加系统属性表

LZ的属于第一种吧

我还真想高手们帮忙权衡一下
39 楼 orpheus 2008-07-08  
抛去实用性不说,楼主很强的技术功底,收了
38 楼 wangwenpinghello 2008-07-07  
我们的项目实现了动态sql的查询,是我们项目经理实现的,牛人,呵呵
37 楼 ioucn 2008-07-07  
atianchen 写道
		POBuildUtil util = new POBuildUtil();
		util.build(rc.getClassName(),"E:\\cpc\\source\\cooperateCore\\com\\mit\\test\\Person.class",list);
		//实例化一个person
		Object person = Class.forName("com.mit.test.Person").newInstance();//hbmcls.newInstance();
		


楼主你确定这里不会报错?明显是两个不同的ClassLoader啊,
自定义POBuildUtil创建和装载的类,在当前ClassLoader怎么可能用forName装载得到
36 楼 liutianhao 2008-07-04  
liutianhao 写道
LZ,包是有冲突的,后来换成ASM3.0.jar包,把ClassWriter cw = new ClassWriter(false)中的‘false’换成‘0’了
到最后运行到((SessionFactoryImpl)factory).addPersistentClass(model, mapping)这步的时候过不去了
你说问题出在哪里呢?


问题发现了,原来model.getCacheConcurrencyStrategy()得到的值是null,该怎么觉解啊?
35 楼 liutianhao 2008-07-04  
LZ,包是有冲突的,后来换成ASM3.0.jar包,把ClassWriter cw = new ClassWriter(false)中的‘false’换成‘0’了
到最后运行到((SessionFactoryImpl)factory).addPersistentClass(model, mapping)这步的时候过不去了
你说问题出在哪里呢?
34 楼 gaogb 2008-07-03  
凡是有兴趣讨论这个问题的请加我的qq80810917
33 楼 xingzhiyun 2008-06-28  
看看我的自定义字段功能:
http://218.26.9.84/ajax/manage/紫色stand_main.aspx
能够增加字段,减少字段,全字段检索,排序,字段筛选等功能
32 楼 yinhj 2008-06-22  
这个功能很实用哦.呵呵
31 楼 panlw 2008-06-21  
Spring的JDBC封装即简单又灵活,还有S2Dao也不错的。
30 楼 panlw 2008-06-21  
即笨又重的东西,估计只有作者自己用得好!
29 楼 panlw 2008-06-21  
中国的程序员被Hibernate还得好惨!!!
28 楼 taupo 2008-06-17  
还没有读完,,,,,,,,,,不过你的头像,让我爆炸了

和我用的一样
27 楼 laiseeme 2008-06-17  
像使用了spring结合hibernate的时候这种情况怎么弄?
26 楼 fuwang 2008-06-16  
bitnewheart 写道
canca 写道
在建表的过程序。突间断电,怎么办?

这个问题太汗了,插入数据时,断电了,怎么办?
断了就断了呗,我们不能在程序上来应对这种情况吧!
回复备份状态,或者自己准备备用电源啥的。

数据库事务可以保证断电时不出问题。确定?
25 楼 bitnewheart 2008-06-16  
canca 写道
在建表的过程序。突间断电,怎么办?

这个问题太汗了,插入数据时,断电了,怎么办?
断了就断了呗,我们不能在程序上来应对这种情况吧!
回复备份状态,或者自己准备备用电源啥的。
24 楼 canca 2008-06-12  
在建表的过程序。突间断电,怎么办?
23 楼 atianchen 2008-06-05  
关于动态修改的问题,我这里有个变通方案,在只能说能用 ,远谈不上好 。
因为我们知道 ,要实现动态加载类,必须用不同的classloader
而hibernate会首先从线程上下文的loader加载,然后才是Class.forName
如果基于hibernate这种现成加载机制,确实很难实现动态修改class然后立即生效。

详细情况我会开帖描述
22 楼 starfeng 2008-06-05  
这个方法不错。有2个不错的地方:
一是用ASM动态生成class这一出发点。
二是无需重置SessionFactory的实现上。

关动态实体的修改如何做,不知道楼主是如何在你这里面集成进来的。网上常用的方法是基于dynamic-component的实现方式,那样的实现实在是很让人不爽,所有的model都得extends一个CustomizableEntity。

结合你的这个思路,我想是否可以这样处理:
1. 用ASM或cglib实现动态的创建,修改。 - 这里会带来一个新的问题,扩展classloader以实现对修改类的重加载
2. XML语法解析器,用于新增及修改hbm文件。 - 这会不同于你的模版机制的生成方式,我想在hibernate中会有相关处理类,我们要做的也许是对这个已有类做相关扩展
3. 你现有的这个SessionFactoryImpl.addPersistentClass,他的功能要做相关扩展了,不光是能新增persisstent class,还应包括修改了。
4. serialize问题,系统停机再重启,将修改再次运用上去。

另,代为回答,关于为什么有这样的应用:
在业务系统中,动态新增单据,及修改单据字段是最常见的。
21 楼 tang_huaiyu 2008-06-02  
十分感谢搂主的好文章!

相关推荐

    hibernate自定义查询

    Hibernate是一款强大的对象关系映射(ORM)框架,它允许开发者使用Java对象来操作数据库,而无需直接编写SQL语句。在Hibernate中,自定义查询是扩展其功能的重要方式,能够帮助我们实现更复杂的数据库交互需求。本文...

    用Hibernate实现领域对象的自定义字段

    4. **配置SessionFactory**:在Hibernate配置文件(hibernate.cfg.xml)中,添加自定义类型的相关配置。 ```xml &lt;hibernate-configuration&gt; ... &lt;!-- 添加自定义类型 --&gt; ... &lt;/hibernate-configuration...

    HibernateSessionFactory.java

    HibernateSessionFactory.java

    JPA + Hibernate 3 CRUD操作历史审计日志的解决方案

    这篇博客"JPA + Hibernate 3 CRUD操作历史审计日志的解决方案"将深入探讨如何实现这一需求。 首先,我们需要理解JPA和Hibernate的基础概念。JPA定义了一组用于持久化Java对象的API,它允许开发者通过面向对象的方式...

    使用 HibernateSessionFactory 类

    在Java的持久化框架Hibernate中,`SessionFactory`是核心组件之一,它负责管理数据库会话并提供对象-关系映射(ORM)的功能。本篇文章将深入探讨`SessionFactory`的使用,以及如何创建和操作它。 `SessionFactory`...

    spring配置sessionFactory(spring3.2.3+hibernate4.2.2)

    Spring会自动将SessionFactory注入到Repository中,无需手动创建Session。在上述代码中,`getCurrentSession()`返回了一个与当前线程绑定的Session,简化了事务管理。 此外,为了处理事务,还需要在Spring配置文件...

    HibernateSessionFactory 代码

    本篇文章将深入探讨`HibernateSessionFactory`及其在Hibernate中的作用,以及如何使用它来实现增、删、查、改(CRUD)操作。 `SessionFactory`是Hibernate的核心组件,它是线程安全的,负责管理数据库连接和会话。`...

    很不错的Hibernate解决方案.pdf

    在此背景下,《很不错的Hibernate解决方案》一文介绍了一种基于Hibernate框架的物流系统持久层解决方案,旨在解决当前物流系统中存在的问题,并提升整体性能。 #### Hibernate框架简介 Hibernate是一种开放源代码的...

    HibernateSpring多数据库解决方案.doc

    总结一下,实现Spring与Hibernate的多数据库解决方案,主要涉及以下几个步骤: 1. 配置多个数据源,分别用于读和写操作。 2. 创建与数据源相对应的Hibernate SessionFactory,确保数据流向正确的数据库。 3. 可选地...

    HibernateSessionFactory.java Hibernate使用的整合的工具文件

    Session s= HibernateSessionFactory.getSession(); 就是Hibernate的工具java类

    HibernateSessionFactory

    用于获得Session会话及关闭Session会话

    Spring+Hibernate实现增删改查的两种方法

    在Java Web开发中,Spring和Hibernate是两个非常重要的框架,它们常常被联合使用来处理数据库操作,如增、删、改、查(CRUD)。本文将深入探讨如何利用Spring与Hibernate整合,通过两种不同的方式来实现这些基本操作...

    新Hibernate SessionFactory().getCurrentSession()猫腻

    标题提到的"新Hibernate SessionFactory().getCurrentSession()猫腻"揭示了一个常见的使用误区或者说是陷阱,即不正确地使用SessionFactory的getCurrentSession()方法。这篇文章可能探讨了这个方法在实际应用中的...

    Hibernate下数据批量处理解决方案

    然而,实际上,通过适当的技术手段,我们可以有效地解决Hibernate在批量处理时可能出现的性能问题。以下是一些关于如何在Hibernate环境下优化批量数据处理的策略。 首先,了解问题的根源。在上述例子中,当尝试向...

    springboot集成hibernate

    在本文中,我们将深入探讨如何将Spring Boot框架与Hibernate ORM集成,特别是在不使用JPA(Java Persistence API)的情况下。Spring Boot以其便捷的自动配置和简化Java应用开发而广受欢迎,而Hibernate作为Java领域...

    hibernate实现动态表查询的多种解决方案

    ### Hibernate 实现动态表查询的多种解决方案 #### 一、前言 在使用Hibernate进行数据库操作时,我们经常需要根据不同的需求对不同的表进行查询。特别是在处理多表、多库等复杂场景时,如何实现动态表查询就显得尤...

Global site tag (gtag.js) - Google Analytics