前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >天哪!手动编写mybatis雏形竟然这么简单

天哪!手动编写mybatis雏形竟然这么简单

作者头像
程序员爱酸奶
发布2020-07-23 10:54:34
3070
发布2020-07-23 10:54:34
举报
文章被收录于专栏:程序员爱酸奶程序员爱酸奶

前言

mybaits 在ORM 框架中,可算是半壁江山了,由于它是轻量级,半自动加载,灵活性和易拓展性。深受广大公司的喜爱,所以我们程序开发也离不开mybatis 。但是我们有对mabtis 源码进行研究吗?或者想看但是不知道怎么看的苦恼吗?

归根结底,我们还是需要知道为什么会有mybatis ,mybatis 解决了什么问题?想要知道mybatis 解决了什么问题,就要知道传统的JDBC 操作存在哪些痛点才促使mybatis 的诞生。我们带着这些疑问,再来一步步学习吧。

原始JDBC 存在的问题

所以我们先来来看下原始JDBC 的操作: 我们知道最原始的数据库操作。分为以下几步:1、获取connection 连接 2、获取preparedStatement 3、参数替代占位符 4、获取执行结果resultSet 5、解析封装resultSet 到对象中返回。

如下是原始JDBC 的查询代码,存在哪些问题?

public static void main(String[] args) { String dirver="com.mysql.jdbc.Driver"; String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"; String userName="root"; String password="123456"; Connection connection=null; ListuserList=new ArrayList<>(); try { Class.forName(dirver); connection= DriverManager.getConnection(url,userName,password); String sql="select * from user where username=?"; PreparedStatement preparedStatement=connection.prepareStatement(sql); preparedStatement.setString(1,"张三"); System.out.println(sql); ResultSet resultSet=preparedStatement.executeQuery(); User user=null; while(resultSet.next()){ user=new User(); user.setId(resultSet.getInt("id")); user.setUsername(resultSet.getString("username")); user.setPassword(resultSet.getString("password")); userList.add(user); } } catch (Exception e) { e.printStackTrace(); }finally { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } if (!userList.isEmpty()) { for (User user : userList) { System.out.println(user.toString()); } } }

小伙伴们发现了上面有哪些不友好的地方?我这里总结了以下几点: 1、数据库的连接信息存在硬编码,即是写死在代码中的。2、每次操作都会建立和释放connection 连接,操作资源的不必要的浪费。3、sql 和参数存在硬编码。4、将返回结果集封装成实体类麻烦,要创建不同的实体类,并通过set方法一个个的注入。

存在上面的问题,所以mybatis 就对上述问题进行了改进。对于硬编码,我们很容易就想到配置文件来解决。mybatis 也是这么解决的。对于资源浪费,我们想到使用连接池,mybatis 也是这个解决的。 对于封装结果集麻烦,我们想到是用JDK的反射机制,好巧,mybatis 也是这么解决的。

设计思路

既然如此,我们就来写一个自定义持久层框架,来解决上述问题,当然是参照mybatis 的设计思路,这样我们在写完之后,再来看mybatis 的源码就恍然大悟,这个地方这样配置原来是因为这样啊。 我们分为使用端和框架端两部分。

使用端

我们在使用mybatis 的时候是不是需要使用SqlMapConfig.xml 配置文件,用来存放数据库的连接信息,以及mapper.xml 的指向信息。mapper.xml 配置文件用来存放sql 信息。所以我们在使用端来创建两个文件SqlMapConfig.xml 和mapper.xml。

框架端

框架端要做哪些事情呢?如下:1、获取配置文件。也就是获取到使用端的SqlMapConfig.xml 以及mapper.xml的 文件 2、解析配置文件。对获取到的文件进行解析,获取到连接信息,sql,参数,返回类型等等。这些信息都会保存在configuration 这个对象中。3、创建SqlSessionFactory,目的是创建SqlSession的一个实例。4、创建SqlSession ,用来完成上面原始JDBC 的那些操作。

那在SqlSession 中 进行了哪些操作呢?1、获取数据库连接 2、获取sql,并对sql 进行解析 3、通过内省,将参数注入到preparedStatement 中 4、执行sql 5、通过反射将结果集封装成对象

使用端实现

好了,上面说了一下,大概的设计思路,主要也是仿照mybatis 主要的类实现的,保证类名一致,方便我们后面阅读源码。我们先来配置好使用端吧,我们创建一个maven 项目。在项目中,我们创建一个User实体类

public class User { private Integer id; private String username; private String password; private String birthday; //getter()和setter()方法 }

创建SqlMapConfig.xml 和Mapper.xml SqlMapConfig.xml

可以看到我们xml 中就配置了数据库的连接信息,以及mapper 一个索引。mybatis中的SqlMapConfig.xml 中还包含其他的标签,只是丰富了功能而已,所以我们只用最主要的。

mapper.xml 是每个类的sql 都会生成一个对应的mapper.xml 。我们这里就用User 类来说吧,所以我们就创建一个UserMapper.xml

select * from user select * from user where username=#{username}

可以看到有点mybatis 里面文件的味道,有namespace表示命名空间,id 唯一标识,resultType 返回结果集的类型,paramType 参数的类型。我们使用端先创建到这,主要是两个配置文件,我们接下来看看框架端是怎么实现的。

加油哈哈。

框架端实现

框架端,我们按照上面的设计思路一步一步来。

获取配置

怎么样获取配置文件呢?我们可以使用JDK自带自带的类Resources加载器来获取文件。我们创建一个自定义Resource类来封装一下:

import java.io.InputStream; public class Resources { public static InputStream getResources(String path){ //使用系统自带的类Resources加载器来获取文件。 return Resources.class.getClassLoader().getResourceAsStream(path); } }

这样通过传入路径,就可以获取到对应的文件流啦。

解析配置文件

上面获取到了SqlMapConfig.xml 配置文件,我们现在来解析它。不过在此之前,我们需要做一点准备工作,就是解析的内存放到什么地方?所以我们来创建两个实体类Mapper 和Configuration。

Mapper Mapper 实体类用来存放使用端写的mapper.xml 文件的内容,我们前面说了里面有.id、sql、resultType 和paramType .所以我们创建的Mapper实体如下:

public class Mapper { private String id; private Class resultType; private Class parmType; private String sql; //getter()和setter()方法 }

这里我们为什么不添加namespace 的值呢?聪明的你肯定发现了,因为mapper里面这些属性表明每个sql 都对应一个mapper,而namespace 是一个命名空间,算是sql 的上一层,所以在mapper中暂时使用不到,就没有添加了。

Configuration Configuration 实体用来保存SqlMapConfig 中的信息。所以需要保存数据库连接,我们这里直接用JDK提供的 DataSource。还有一个就是mapper 的信息。每个mapper 有自己的标识,所以这里采用hashMap来存储。如下:

public class Configuration { private DataSource dataSource; HashMapmapperMap=new HashMap<>(); //getter()和setter方法 } XmlMapperBuilder

做好了上面的准备工作,我们先来解析mapper 吧。我们创建一个XmlMapperBuilder 类来解析。通过dom4j 的工具类来解析XML 文件。我这里用的dom4j 依赖为:

org.dom4j dom4j 2.1.3

思路:1、获取文件流,转成document。2、获取根节点,也就是mapper。获取根节点的namespace属性值 3、获取select 节点,获取其id,sql,resultType,paramType 4、将select 节点的属性封装到Mapper 实体类中。5、同理获取update/insert/delete 节点的属性值封装到Mapper 中 6、通过namespace.id 生成key 值将mapper对象保存到Configuration实体中的HashMap 中。7、返回 Configuration实体 代码如下:

public class XmlMapperBuilder { private Configuration configuration; public XmlMapperBuilder(Configuration configuration){ this.configuration=configuration; } public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException { Document document=new SAXReader().read(in); Element rootElement=document.getRootElement(); String namespace=rootElement.attributeValue("namespace"); Listlist=rootElement.selectNodes("//select"); for (int i = 0; i < list.size(); i++) { Mapper mapper=new Mapper(); Element element= (Element) list.get(i); String id=element.attributeValue("id"); mapper.setId(id); String paramType = element.attributeValue("paramType"); if(paramType!=null && !paramType.isEmpty()){ mapper.setParmType(Class.forName(paramType)); } String resultType = element.attributeValue("resultType"); if (resultType != null && !resultType.isEmpty()) { mapper.setResultType(Class.forName(resultType)); } mapper.setSql(element.getTextTrim()); String key=namespace+"."+id; configuration.getMapperMap().put(key,mapper); } return configuration; } }

上面我只解析了select 标签。大家可以解析对应insert/delete/uupdate 标签,操作都是一样的。

XmlConfigBuilder

我们再来解析一下SqlMapConfig.xml 配置信息思路是一样的, 1、获取文件流,转成document。2、获取根节点,也就是configuration。3、获取根节点中所有的property 节点,并获取值,也就是获取数据库连接信息 4、创建一个dataSource 连接池 5、将连接池信息保存到Configuration实体中 6、获取根节点的所有mapper 节点 7、调用XmlMapperBuilder 类解析对应mapper 并封装到Configuration实体中 8、完整代码如下:

public class XmlConfigBuilder { private Configuration configuration; public XmlConfigBuilder(Configuration configuration){ this.configuration=configuration; } public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { Document document=new SAXReader().read(in); Element rootElement=document.getRootElement(); //获取连接信息 ListpropertyList=rootElement.selectNodes("//property"); Properties properties=new Properties(); for (int i = 0; i < propertyList.size(); i++) { Element element = (Element) propertyList.get(i); properties.setProperty(element.attributeValue("name"),element.attributeValue("value")); } //使用连接池 ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setDriverClass(properties.getProperty("driverClass")); dataSource.setJdbcUrl(properties.getProperty("jdbcUrl")); dataSource.setUser(properties.getProperty("userName")); dataSource.setPassword(properties.getProperty("password")); configuration.setDataSource(dataSource); //获取mapper 信息 ListmapperList=rootElement.selectNodes("//mapper"); for (int i = 0; i < mapperList.size(); i++) { Element element= (Element) mapperList.get(i); String mapperPath=element.attributeValue("resource"); XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration); configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath)); } return configuration; } } 创建SqlSessionFactory

完成解析后我们创建SqlSessionFactory 用来创建Sqlseesion 的实体,这里为了尽量还原mybatis 设计思路,也也采用的工厂设计模式。SqlSessionFactory 是一个接口,里面就一个用来创建SqlSessionf的方法。如下:

public interface SqlSessionFactory { public SqlSession openSqlSession(); }

单单这个接口是不够的,我们还得写一个接口的实现类,所以我们创建一个DefaultSqlSessionFactory。如下:

public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } public SqlSession openSqlSession() { return new DefaultSqlSeeion(configuration); } }

可以看到就是创建一个DefaultSqlSeeion并将包含配置信息的configuration 传递下去。DefaultSqlSeeion 就是SqlSession 的一个实现类。

创建SqlSession

在SqlSession 中我们就要来处理各种操作了,比如selectList,selectOne,insert.update,delete 等等。我们这里SqlSession 就先写一个selectList 方法。如下:

public interface SqlSession { /** * 条件查找 * @param statementid 唯一标识,namespace.selectid * @param parm 传参,可以不传也可以一个,也可以多个 * @param * @return */ publicListselectList(String statementid,Object...parm) throws Exception;

然后我们创建DefaultSqlSeeion 来实现SqlSeesion 。

public class DefaultSqlSeeion implements SqlSession { private Configuration configuration; private Executer executer=new SimpleExecuter(); public DefaultSqlSeeion(Configuration configuration) { this.configuration = configuration; } @Override publicListselectList(String statementid, Object... parm) throws Exception { Mapper mapper=configuration.getMapperMap().get(statementid); Listquery = executer.query(configuration, mapper, parm); return query; } }

我们可以看到DefaultSqlSeeion 获取到了configuration,并通过statementid 从configuration 中获取mapper。然后具体实现交给了Executer 类来实现。我们这里先不管Executer 是怎么实现的,就假装已经实现了。那么整个框架端就完成了。通过调用Sqlsession.selectList() 方法,来获取结果。

感觉我们都还没有处理,就框架搭建好了?骗鬼呢,确实前面我们从获取文件解析文件,然后创建工厂。都是做好准备工作。下面开始我们JDBC的实现。

SqlSession 具体实现

我们前面说SqlSeesion 的具体实现有下面5步 1、获取数据库连接 2、获取sql,并对sql 进行解析 3、通过内省,将参数注入到preparedStatement 中 4、执行sql 5、通过反射将结果集封装成对象

但是我们在DefaultSqlSeeion 中将实现交给了Executer来执行。所以我们就要在Executer中来实现这些操作。

我们首先来创建一个Executer 接口,并写一个DefaultSqlSeeion中调用的query 方法。

public interface Executer { Listquery(Configuration configuration,Mapper mapper,Object...parm) throws Exception; }

接着我们写一个SimpleExecuter 类来实现Executer 。然后SimpleExecuter.query()方法中,我们一步一步的实现。

获取数据库连接

因为数据库连接信息保存在configuration,所以直接获取就好了。

//获取连接 connection=configuration.getDataSource().getConnection(); 获取sql,并对sql 进行解析

我们这里想一下,我们在Usermapper.xml写的sql 是什么样子?

select * from user where username=#{username}

#{username} 这样的sql 我们该怎么解析呢?

分两步 1、将sql 找到#{*},并将这部分替换成 ?号

2、对 #{*} 进行解析获取到里面的参数对应的paramType 中的值。

具体实现用到下面几个类。GenericTokenParser类,可以看到有三个参数,开始标记,就是我们的“#{” ,结束标记就是 “}”, 标记处理器就是处理标记里面的内容也就是username。

public class GenericTokenParser { private final String openToken; //开始标记 private final String closeToken; //结束标记 private final TokenHandler handler; //标记处理器 public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } /** * 解析${}和#{} * @param text * @return * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。 * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现 */ public String parse(String text) { //具体实现 } }

主要的就是parse() 方法,用来获取操作1 的sql。获取结果例如:

select * from user where username=?

那上面用到TokenHandler 来处理参数。ParameterMappingTokenHandler实现TokenHandler的类

public class ParameterMappingTokenHandler implements TokenHandler { private ListparameterMappings = new ArrayList(); // context是参数名称 #{id} #{username} @Override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; } private ParameterMapping buildParameterMapping(String content) { ParameterMapping parameterMapping = new ParameterMapping(content); return parameterMapping; } public ListgetParameterMappings() { return parameterMappings; } public void setParameterMappings(ListparameterMappings) { this.parameterMappings = parameterMappings; } }

可以看到将参数名称存放 ParameterMapping 的集合中了。ParameterMapping 类就是一个实体,用来保存参数名称的。

public class ParameterMapping { private String content; public ParameterMapping(String content) { this.content = content; } //getter()和setter() 方法。 }

所以我们在我们通过GenericTokenParser类,就可以获取到解析后的sql,以及参数名称。我们将这些信息封装到BoundSql实体类中。

public class BoundSql { private String sqlText; private ListparameterMappingList=new ArrayList<>(); public BoundSql(String sqlText, ListparameterMappingList) { this.sqlText = sqlText; this.parameterMappingList = parameterMappingList; } ////getter()和setter() 方法。 }

好了,那么分两步走,先获取,后解析 获取 获取原始sql 很简单,sql 信息就存在mapper 对象中,直接获取就好了。

String sql=mapper.getSql()

解析 1、创建一个ParameterMappingTokenHandler 处理器 2、创建一个GenericTokenParser 类,并初始化开始标记,结束标记,处理器 3、执行genericTokenParser.parse(sql);获取解析后的sql‘’,以及在parameterMappingTokenHandler 中存放了参数名称的集合。4、将解析后的sql 和参数封装到BoundSql 实体类中。

/** * 解析自定义占位符 * @param sql * @return */ private BoundSql getBoundSql(String sql){ ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler); String parse = genericTokenParser.parse(sql); return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings()); } 将参数注入到preparedStatement 中

上面的就完成了sql,的解析,但是我们知道上面得到的sql 还是包含 JDBC的 占位符,所以我们需要将参数注入到preparedStatement 中。1、通过boundSql.getSqlText()获取带有占位符的sql. 2、接收参数名称集合 parameterMappingList 3、通过mapper.getParmType() 获取到参数的类。4、通过getDeclaredField(content)方法获取到参数类的Field。5、通过Field.get() 从参数类中获取对应的值 6、注入到preparedStatement 中

BoundSql boundSql=getBoundSql(mapper.getSql()); String sql=boundSql.getSqlText(); ListparameterMappingList = boundSql.getParameterMappingList(); //获取preparedStatement,并传递参数值 PreparedStatement preparedStatement=connection.prepareStatement(sql); Class parmType = mapper.getParmType(); for (int i = 0; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String content = parameterMapping.getContent(); Field declaredField = parmType.getDeclaredField(content); declaredField.setAccessible(true); Object o = declaredField.get(parm[0]); preparedStatement.setObject(i+1,o); } System.out.println(sql); return preparedStatement; 执行sql

其实还是调用JDBC 的executeQuery()方法或者execute()方法

//执行sql ResultSet resultSet = preparedStatement.executeQuery(); 通过反射将结果集封装成对象

在获取到resultSet 后,我们进行封装处理,和参数处理是类似的。1、创建一个ArrayList 2、获取返回类型的类 3、循环从resultSet中取数据 4、获取属性名和属性值 5、创建属性生成器 6、为属性生成写方法,并将属性值写入到属性中 7、将这条记录添加到list 中 8、返回list

/** * 封装结果集 * @param mapper * @param resultSet * @param * @return * @throws Exception */ privateListresultHandle(Mapper mapper,ResultSet resultSet) throws Exception{ ArrayListlist=new ArrayList<>(); //封装结果集 Class resultType = mapper.getResultType(); while (resultSet.next()) { ResultSetMetaData metaData = resultSet.getMetaData(); Object o = resultType.newInstance(); int columnCount = metaData.getColumnCount(); for (int i = 1; i <= columnCount; i++) { //属性名 String columnName = metaData.getColumnName(i); //属性值 Object value = resultSet.getObject(columnName); //创建属性描述器,为属性生成读写方法 PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType); Method writeMethod = propertyDescriptor.getWriteMethod(); writeMethod.invoke(o,value); } list.add((E) o); } return list; } 创建SqlSessionFactoryBuilder

我们现在来创建一个SqlSessionFactoryBuilder 类,来为使用端提供一个人口。

public class SqlSessionFactoryBuilder { private Configuration configuration; public SqlSessionFactoryBuilder(){ configuration=new Configuration(); } public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration); configuration=xmlConfigBuilder.loadXmlConfig(in); SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration); return sqlSessionFactory; } }

可以看到就一个build 方法,通过SqlMapConfig的文件流将信息解析到configuration,创建并返回一个sqlSessionFactory 。

到此,整个框架端已经搭建完成了,但是我们可以看到,只实现了select 的操作,update、inster、delete 的操作我们在我后面提供的源码中会有实现,这里只是将整体的设计思路和流程。

测试

终于到了测试的环节啦。我们前面写了自定义的持久层,我们现在来测试一下能不能正常的使用吧。见证奇迹的时刻到啦

我们先引入我们自定义的框架依赖。以及数据库和单元测试

mysql mysql-connector-java 8.0.11 cn.quellanan myself-mybatis 1.0.0 junit junit 4.10

然后我们写一个测试类 1、获取SqlMapperConfig.xml的文件流 2、获取Sqlsession 3、执行查找操作

@org.junit.Test public void test() throws Exception{ InputStream inputStream= Resources.getResources("SqlMapperConfig.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession(); Listlist = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll"); for (User parm : list) { System.out.println(parm.toString()); } System.out.println(); User user=new User(); user.setUsername("张三"); Listlist1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user); for (User user1 : list1) { System.out.println(user1); } }

可以看到已经可以了,看来我们自定义的持久层框架生效啦。

优化

但是不要高兴的太早哈哈,我们看上面的测试方法,是不是感觉和平时用的不一样,每次都都写死statementId ,这样不太友好,所以我们接下来来点骚操作,通用mapper 配置。我们在SqlSession中增加一个getMapper方法,接收的参数是一个类。我们通过这个类就可以知道statementId .

/** * 使用代理模式来创建接口的代理对象 * @param mapperClass * @param * @return */ publicT getMapper(ClassmapperClass);

具体实现就是利用JDK 的动态代理机制。1、通过Proxy.newProxyInstance() 获取一个代理对象 2、返回代理对象 那代理对象执行了哪些操作呢?创建代理对象的时候,会实现一个InvocationHandler接口,重写invoke() 方法,让所有走这个代理的方法都会执行这个invoke() 方法。那这个方法做了什么操作?这个方法就是通过传入的类对象,获取到对象的类名和方法名。用来生成statementid 。所以我们在mapper.xml 配置文件中的namespace 就需要制定为类路径,以及id 为方法名。实现方法:

@Override publicT getMapper(ClassmapperClass) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //获取到方法名 String name = method.getName(); //类型 String className = method.getDeclaringClass().getName(); String statementid=className+"."+name; return selectList(statementid,args); } }); return (T) proxyInstance; }

我们写一个UserDao

public interface UserDao { ListselectAll(); ListselectByName(User user); }

这个是不是我们熟悉的味道哈哈,就是mapper层的接口。然后我们在mapper.xml 中指定namespace 和id

接下来我们在写一个测试方法

@org.junit.Test public void test2() throws Exception{ InputStream inputStream= Resources.getResources("SqlMapperConfig.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession(); UserDao mapper = sqlSession.getMapper(UserDao.class); Listusers = mapper.selectAll(); for (User user1 : users) { System.out.println(user1); } User user=new User(); user.setUsername("张三"); Listusers1 = mapper.selectByName(user); for (User user1 : users1) { System.out.println(user1); } }

番外

自定义的持久层框架,我们就写完了。这个实际上就是mybatis 的雏形,我们通过自己手动写一个持久层框架,然后在来看mybatis 的源码,就会清晰很多。下面这些类名在mybatis 中都有体现。

这里抛砖引玉,祝君阅读源码愉快。觉得有用的兄弟们记得收藏啊。

厚颜无耻的求波点赞!!!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员爱酸奶 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档