首页 文章 精选 留言 我的

精选列表

搜索[crud],共2277篇文章
优秀的个人博客,低调大师

极简Mybatis之旅(一):CRUD

摘要 最近在研究mybatis框架,作为一个优秀ORM框架,mybatis很多优秀的设计思想值得借鉴。 mybatis的配置文件里,主要是config和mapper。config定义了全局参数:数据源类型(POOL, UNPOOLED, JNDI)、事务管理类型(默认为managed)、库url、账户信息和mapper文件路径。 config配置 // config.xml <configuration> <environments default="classfly"> <environment id="classfly"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8" /> <property name="username" value="yourusername" /> <property name="password" value="yourpassword" /> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/UserMapper.xml" /> </mappers> </configuration> DBNAME='test',是mybatis默认分配的库: mapper配置 首先我们要问自己以下三个问题: 为什么要配置mapper文件? 怎么配置mapper文件? mybatis如何解析mapper文件? mapper文件的作用 mapper文件提供一种持久化层与应用层的"通信协议",通过mysql关键字resultMap, parameterType等维护应用代码DO对象与持久化存储数据之间的关联关系。 mapper文件里定义了select, insert, update, delete四种常用的DML语句,并定义statement={mapper namespace}.{operation}实现在应用代码层执行调用DB操作。 配置mapper文件 这里先给出测试用例用到的mapper配置文件的关键部分: <!-- 代码清单-1 --> <mapper namespace="com.classfly.mapper.UserMapper"> <resultMap id="user" type="pojo.User"> <result column="user_id" property="userId" /> <result column="user_name" property="userName" /> <result column="password" property="password" /> <result column="age" property="age" /> </resultMap> <select id="query" resultMap="user"> SELECT * FROM user; </select> <insert id="insert" parameterType="pojo.User"> insert into user ( user_id, user_name, password, age) values ( #{userId}, #{userName}, #{password}, #{age} ) <selectKey resultType="pojo.User" keyProperty="id" order="AFTER"> select LAST_INSERT_ID() as id </selectKey> </insert> <update id="update" parameterType="pojo.User"> UPDATE user SET user_name = #{userName} , password = #{password} , age = #{age} WHERE user_id = #{userId} </update> <delete id="delete"> DELETE FROM user WHERE user_id = #{userId} </delete> </mapper> mapper namespace定义了DML语句的作用范围,那如果两个不同的mapper文件定义相同的namespace会发生什么?由于mysql保持namespace的全局唯一性,所以在解析mapper文件时mysql会抛异常提示开发者修改mapper文件以保持namespace的全局唯一性。 resultMap resultMap关键字将代码和持久化层的数据映射抽象出来,用户无需关心两者之间如何映射。 我们可以使用HashMap完成代码和持久化层的数据映射关系,不但缺乏通用型且代码层需要将对象转换成map结构。resultMap接受HashMap结构和JavaBean或者POJO对象,提供轻量级的参数映射方案。 resultMap有"别名"的功能,你无需每个DML都写一大串的映射语句,秉着"仅定义一次"的原则,我们可以这么做: // config.xml <typeAlias type="pojo.User" alias="User"/> // 用typeAlias的User别名替换了resultMap中的user <select id="query" resultType="User"> SELECT * FROM user ORDER BY id DESC </select> Now if only the world was always that simple! 当POJO类成员变量并未完全和Table列名"对齐"时,可通过定义resultMap的方式来实现mapper: <resultMap id="user" type="User"> <result property="userId" column="user_id" /> <result property="username" column="user_name"/> <result property="password" column="hashed_password"/> </resultMap> <select id="query" resultMap="user"> SELECT * FROM user ORDER BY id DESC </select> '#'和'$' 从"代码清单-1"里可以看出,#和$均可实现POJO字段和DB表列名的映射关系。#将参数部分用?替换,而$仅根据执行sql时通过POJO对象透传的值进行字符串替换,容易被注入恶意代码,不可取!如: select * from ${tableName} where name = #{name} 如果tableName='user; delete user; --',最终sql语句变为: select * from user; delete user; -- where name = #{name} 导致user整张表数据被删除。 mybatis如何解析mapper文件 To be continued... 测试用例 /** * Created by fujianbo on 2018/4/22. * * @author fujianbo * @date 2018/04/22 */ public class TestMybatis { @Test public void testQuery() { SqlSession sqlSession = buildMySqlEnv("config.xml"); List<User> userList = sqlSession.selectList("com.classfly.mapper.UserMapper.query"); for (User p : userList) { System.out.println(p); } } @Test public void testInsert() { SqlSession sqlSession = buildMySqlEnv("config.xml"); User user = new User(); user.setUserId(124L); user.setAge(26); user.setPassword("test_123"); user.setUserName("芸Rey"); if (sqlSession.insert("com.classfly.mapper.UserMapper.insert", user) > 0) { sqlSession.commit(); } } @Test public void testUpdate() { SqlSession sqlSession = buildMySqlEnv("config.xml"); User user = new User(); user.setUserId(124L); user.setAge(26); user.setUserName("芸Rey"); user.setPassword("test_modified"); if (sqlSession.update("com.classfly.mapper.UserMapper.update", user) > 0) { sqlSession.commit(); } } @Test public void testDelete() { SqlSession sqlSession = buildMySqlEnv("config.xml"); User user = new User(); user.setUserId(123L); if (sqlSession.update("com.classfly.mapper.UserMapper.delete", user) > 0) { sqlSession.commit(); } } private static SqlSession buildMySqlEnv(String resource) { try { return new SqlSessionFactoryBuilder() .build(org.apache.ibatis.io.Resources.getResourceAsStream(resource)) .openSession(); } catch (IOException e) { System.out.printf("Failed to build mysql environment!"); return null; } } } 代码链接实现

优秀的个人博客,低调大师

突破CRUD | 万能树工具类封装

0、学完本文你或许可以收获 感受一个树工具从初始逐步优化完善的过程 树工具封装的设计思考与实现思路 最后收获一款拿来即用的树工具源代码 对于前端树组件有一定了解和使用过的同学可直接跳跃到第3章节开始。 1、树长什么样 ? 前端的树组件大多数情况下出现在后端的管理系统中,比如我们常见的菜单树、机构树、某某分类树、树表格等。大致像下方图片所展示的这样。 菜单树 机构树 org_tree.png 树表格 大致上来说,前端树的展现形式就是上面3张图所列的几种形式。而这种前端树组件的展现构成需要依赖于后端返回的数据格式。 2、数据格式 结合我自身使用过的前端树组件来说,大致可以分为如下两种。 列表形式 [ {id:1,pId:0,name:"父节点1"} {id:11,pId:1,name:"父节点11"}, {id:111,pId:11,name:"叶子节点111"}, {id:112,pId:11,name:"叶子节点112"}, {id:113,pId:11,name:"叶子节点113"}, {id:114,pId:11,name:"叶子节点114"}, {id:12,pId:1,name:"父节点12"}, {id:121,pId:12,name:"叶子节点121"}, {id:122,pId:12,name:"叶子节点122"}, {id:123,pId:12,name:"叶子节点123"}, {id:124,pId:12,name:"叶子节点124"} ] 树形结构 [{name:"父节点1", children:[ { name:"父节点11", children:[ {name:"叶子节点111"}, {name:"叶子节点112"}, {name:"叶子节点113"}, {name:"叶子节点114"} ] }, { name:"父节点12", children:[ {name:"叶子节点121"}, {name:"叶子节点122"}, {name:"叶子节点123"}, {name:"叶子节点124"} ] } ] }] 本文所讲的树工具封装主要是针对第二种数据格式树形结构来说,因为第一种本身不需要特殊处理,也就不存在什么封装,就是简单的列表查询展示,与一般数据列表数据格式的区别是多了数据ID与父ID属性提供给前端进行树组件的构造。 而第二种是要在列表形式的数据格式上进行转换,形成如上所示的树形结构。但是,我们发现里面没有数据ID与父ID属性,why ? 因为后端完成了数据层面树结构的构造工作,前端树组件再无需根据这两个属性进行树结构的判断构建,直接展示就OK,当然也不绝对,最终还得看前端的树组件是否需要。 但一般都会保留这两个属性,因为除过树组件自身的构造需求,业务处理上往往需要这两个属性,而后端树工具要构造树结构,那一定是需要数据ID与父ID的。 如果感觉上面说的麻烦你就记住一点,不管是列表结构还是树形结构,始终保留数据ID与父ID两个属性就对了。 到这里又有一个新问题了,上面说了列表形式无需封装什么可以直接使用,既然如此那用列表形式的结构就完了呗,为什么写个工具类搞个树结构出来呢 ? 原因是,前端树组件的实现方式非常多,不同树插件或组件需要的数据格式可能不一样,有的列表、树形格式都支持,有的仅支持列表或树形的一种,所以为了满足不同前端树的展示需求,提供树形结构的构造工具是必要的。 3、话不多说,先实现个初版 从上面的内容我们了解了前端树组件的渲染展现需要后端提供满足需求的数据格式,那么实际上也就决定了树工具类的核心职责就是将一般的数据列表结构转换为树形结构,从而提供给前端使用。 解读上面所述的核心职责,首先一般列表是什么列表,此处我们假设为菜单列表,这就有了第一个类MenuEntity,紧接着是转换,谁转换成谁 ?数据列表转换树结构,树结构本身那应该就是个类,我们暂且叫它 TreeNode,结合我们第一步假设的菜单列表,那实际上就是 List< MenuEntity > 转换为 List < TreeNode > ,如此就得到了第二个类TreeNode,最后还剩转换这个动作谁去做 ? 那就是我们今天的主角 TreeUtil 了。 好,至此,通过分析树工具类的核心职责,我们分析得到了三个类。 MenuEntity TreeNode TreeUtil OK,有了上面的内容那就来个简单的实现。 树节点类 publicclassTreeNode{ //树节点ID privateStringid; //树节点名称 privateStringname; //树节点编码 privateStringcode; //树节点链接 privateStringlinkUrl; //树节点图标 privateStringicon; //父节点ID privateStringparentId; } 菜单类 publicclassMenuEntity{ //菜单ID privateStringid; //上级菜单ID privateStringpid; //菜单名称 privateStringname; //菜单编码 privateStringcode; //菜单图标 privateStringicon; //菜单链接 privateStringurl; } 树工具类 publicclassTreeUtil{ /** *树构建 */ publicstaticList<TreeNode>build(List<TreeNode>treeNodes,ObjectparentId){ List<TreeNode>finalTreeNodes=CollectionUtil.newArrayList(); for(TreeNodetreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add(treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } privatestaticvoidinnerBuild(List<TreeNode>treeNodes,TreeNodeparentNode){ for(TreeNodechildNode:treeNodes){ if(parentNode.getId().equals(childNode.getParentId())){ List<TreeNode>children=parentNode.getChildren(); if(children==null){ children=CollectionUtil.newArrayList(); parentNode.setChildren(children); } children.add(childNode); childNode.setParentId(parentNode.getId()); innerBuild(treeNodes,childNode); } } } } 树工具类实现的两个关键点,第一,树构建的开始位置也就是从哪里开始构建,所以需要一个父ID参数来指定构建的起始位置,第二,构建到什么时候结束,不做限制的的话,我们的树是可以无限延伸的,所以此处innerBuild方法进行递归操作。 测试代码 publicstaticvoidmain(String[]args){ //1、模拟菜单数据 List<MenuEntity>menuEntityList=CollectionUtil.newArrayList(); menuEntityList.add(newMenuEntity("1","0","系统管理","sys","/sys")); menuEntityList.add(newMenuEntity("11","1","用户管理","user","/sys/user")); menuEntityList.add(newMenuEntity("111","11","用户添加","userAdd","/sys/user/add")); menuEntityList.add(newMenuEntity("2","0","店铺管理","store","/store")); menuEntityList.add(newMenuEntity("21","2","商品管理","shop","/shop")); //2、MenuEntity->TreeNode List<TreeNode>treeNodes=CollectionUtil.newArrayList(); for(MenuEntitymenuEntity:menuEntityList){ TreeNodetreeNode=newTreeNode(); treeNode.setId(menuEntity.getId()); treeNode.setParentId(menuEntity.getPid()); treeNode.setCode(menuEntity.getCode()); treeNode.setName(menuEntity.getName()); treeNode.setLinkUrl(menuEntity.getUrl()); treeNodes.add(treeNode); } //3、树结构构建 List<TreeNode>treeStructureNodes=TreeUtil.build(treeNodes,"0"); Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes))); } 收工,第一版简单的树工具就实现了。 4、迭代优化 1.0 这不是我的事 但是,通过测试代码我们发现这个用起来不是太爽,要将菜单数据转换为树结构竟然需要我先把菜单列表转换成树结构的列表才能调用树工具类的build方法,这里的转换操作仅仅是属性的拷贝,并未完成树状结构的生成构建,但这是调用者需要关心的吗 ?很显然TreeNode集合创建生成这个过程应该是树工具类应该做的事情。所以做了如下调整。 1 调整了build方法参数,将原有treeNodes 调整为 menuEntityList,意味着将上面说的treeNodes构建构成交给TreeUtil去做。 2 新增了Convert类,并包含convert方法,该方法的职责是完成菜单实体到树节点属性的拷贝。 3 再次调整build方法参数,新增Convert转换。 调整完成的结果,看下代码。 树工具 publicclassTreeUtil_1_0{ //新增的属性转换方法 publicinterfaceConvert<MenuEntity,TreeNode>{ publicvoidconvert(MenuEntitymenuEntity,TreeNodetreeNode); } /** *树构建 */ publicstaticList<TreeNode>build(List<MenuEntity>menuEntityList,ObjectparentId,Convert<MenuEntity,TreeNode>convert){ //原来调用方做的事情 List<TreeNode>treeNodes=CollectionUtil.newArrayList(); for(MenuEntitymenuEntity:menuEntityList){ TreeNodetreeNode=newTreeNode(); convert.convert(menuEntity,treeNode); treeNodes.add(treeNode); } List<TreeNode>finalTreeNodes=CollectionUtil.newArrayList(); for(TreeNodetreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add(treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } privatestaticvoidinnerBuild(List<TreeNode>treeNodes,TreeNodeparentNode){ for(TreeNodechildNode:treeNodes){ if(parentNode.getId().equals(childNode.getParentId())){ List<TreeNode>children=parentNode.getChildren(); if(children==null){ children=CollectionUtil.newArrayList(); parentNode.setChildren(children); } children.add(childNode); childNode.setParentId(parentNode.getId()); innerBuild(treeNodes,childNode); } } } } 测试代码 publicstaticvoidmain(String[]args){ //1、模拟菜单数据 List<MenuEntity>menuEntityList=CollectionUtil.newArrayList(); menuEntityList.add(newMenuEntity("1","0","系统管理","sys","/sys")); menuEntityList.add(newMenuEntity("11","1","用户管理","user","/sys/user")); menuEntityList.add(newMenuEntity("111","11","用户添加","userAdd","/sys/user/add")); menuEntityList.add(newMenuEntity("2","0","店铺管理","store","/store")); menuEntityList.add(newMenuEntity("21","2","商品管理","shop","/shop")); //2、树结构构建 List<TreeNode>treeStructureNodes=TreeUtil_1_0.build(menuEntityList,"0",newConvert<MenuEntity,TreeNode>(){ @Override publicvoidconvert(MenuEntitymenuEntity,TreeNodetreeNode){ treeNode.setId(menuEntity.getId()); treeNode.setParentId(menuEntity.getPid()); treeNode.setCode(menuEntity.getCode()); treeNode.setName(menuEntity.getName()); treeNode.setLinkUrl(menuEntity.getUrl()); } }); Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes))); } 比较1.0与初版的测试代码,发现少了树节点列表构建的过程,属性拷贝的工作作为回调过程在转换过程中进行处理。 2.0 仅支持造菜单树哪够 1.0优化完后,我们来了新的需求,有个机构树也需要生成,此时的树工具仅支持了菜单树,所以我们进行改造,让其支持其他任何对象的树生成。 改造点主要是将的TreeUtil中的菜单实体转换为泛型,限于篇幅,就贴个核心方法的代码 publicstatic<T>List<TreeNode>build(List<T>list,ObjectparentId,Convert<T,TreeNode>convert){ List<TreeNode>treeNodes=CollectionUtil.newArrayList(); for(Tobj:list){ TreeNodetreeNode=newTreeNode(); convert.convert(obj,treeNode); treeNodes.add(treeNode); } List<TreeNode>finalTreeNodes=CollectionUtil.newArrayList(); for(TreeNodetreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add(treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } 如此一来,我们就可以支持任意类型的树构造。 3.0 哥们,你返回的属性不够用啊 前两点比较容易想到,也比较容易实现,但这时候前端同学抛来了新的问题,哥们,你返回的树节点属性不够用啊,你看我这界面。需要备注你没返回来啊。 好吧,这种情况确实没考虑到。 要满足上述需求,简单做法就将remark属性直接添加到 TreeNode 类中,Convert中赋下值,这不就满足了,但想想又不对,今天这个前端伙计缺个remark,明天可能别的伙计又缺个其他属性,全加到TreeNode中,TreeNode到底是树节点还是业务实体,所以不能这么搞。 这里要处理成可扩展,同时满足开闭原则,所以此处比较妥的处理方式是继承,TreeNode属性满足不了的情况下,通过继承扩展具体业务的树节点来实现。 具体改造点如下 1 新增菜单实体扩展树节点如下 publicclassMenuEntityTreeNodeextendsTreeNode{ //扩展备注属性 privateStringremark; //省略setget... } 2 改造TreeUtil.build方法参数,新增TreeNode Class类型参数,如下 /** *树构建 */ publicstatic<T,EextendsTreeNode>List<E>build(List<T>list,ObjectparentId,Class<E>treeNodeClass,Convert<T,E>convert){ List<E>treeNodes=CollectionUtil.newArrayList(); for(Tobj:list){ EtreeNode=(E)ReflectUtil.newInstance(treeNodeClass); convert.convert(obj,treeNode); treeNodes.add(treeNode); } List<E>finalTreeNodes=CollectionUtil.newArrayList(); for(EtreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add((E)treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } 测试代码 publicstaticvoidmain(String[]args){ //...此处省略模拟数据创建过程 //2、树结构构建 List<MenuEntityTreeNode>treeStructureNodes=TreeUtil_3_0.build(menuEntityList,"0",MenuEntityTreeNode.class,newTreeUtil_3_0.Convert<MenuEntity,MenuEntityTreeNode>(){ @Override publicvoidconvert(MenuEntityobject,MenuEntityTreeNodetreeNode){ treeNode.setId(object.getId()); treeNode.setParentId(object.getPid()); treeNode.setCode(object.getCode()); treeNode.setName(object.getName()); treeNode.setLinkUrl(object.getUrl()); //新增的业务属性 treeNode.setRemark("添加备注属性"); } }); Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes))); } 如此一来,不同业务场景下需要添加不同的属性时,即可做到可扩展,且对现有代码不造成任何影响和改动。 4.0 哥们,我的属性名不叫code 完成了3.0版本,基本上大部分需求就都可以满足了,但是这时候前端同学又抛来了新的问题,哥们,你返回的树节点编号属性是code,但我这边的叫number,对应不上,我这边调整的话影响比较大,你看后端返回的时候能不能处理下。 code属性名肯定是不能调整的,因为其他模块树的节点编号都叫code。 那怎么办 ?其实也简单,跟3.0版本一样,在扩展的业务树节点去加个属性,这样问题是解决了,但万一出现所有treeNode的属性名都跟前端需要的不对应这种极端情况,那意味着所有树属性都需要自行扩展定义,这种岂不是返回了没什么用的父TreeNode心中的所有属性。序列化时倒是可以控制,为空的不进行序列化,但不是依赖序列化框架了么。还有没有其他办法。 稍微整理下需求,就是树节点属性在返回前端时要能够支持自定义属性名。 类属性定义好就改不了了,怎么自定义,除了新增类和改现有的属性,还有什么办法呢 ?这时候我们应该想到map 具体怎么做 1 首先,定义新的类TreeNodeMap,看名字就知道基于map实现 publicclassTreeNodeMapextendsHashMap{ privateTreeNodeConfigtreeNodeConfig; publicTreeNodeMap(){ this.treeNodeConfig=TreeNodeConfig.getDefaultConfig(); } publicTreeNodeMap(TreeNodeConfigtreeNodeConfig){ this.treeNodeConfig=treeNodeConfig; } public<T>TgetId(){ return(T)super.get(treeNodeConfig.getIdKey()); } publicvoidsetId(Stringid){ super.put(treeNodeConfig.getIdKey(),id); } public<T>TgetParentId(){ return(T)super.get(treeNodeConfig.getParentIdKey()); } publicvoidsetParentId(StringparentId){ super.put(treeNodeConfig.getParentIdKey(),parentId); } public<T>TgetName(){ return(T)super.get(treeNodeConfig.getNameKey()); } publicvoidsetName(Stringname){ super.put(treeNodeConfig.getNameKey(),name); } public<T>TgetCode(){ return(T)super.get(treeNodeConfig.getCodeKey()); } publicTreeNodeMapsetCode(Stringcode){ super.put(treeNodeConfig.getCodeKey(),code); returnthis; } publicList<TreeNodeMap>getChildren(){ return(List<TreeNodeMap>)super.get(treeNodeConfig.getChildrenKey()); } publicvoidsetChildren(List<TreeNodeMap>children){ super.put(treeNodeConfig.getChildrenKey(),children); } publicvoidextra(Stringkey,Objectvalue){ super.put(key,value); } } 2 既然支持属性名自定义,新增配置类TreeNodeConfig来完成这个事情,同时提供默认属性名 publicclassTreeNodeConfig{ //默认属性的单例对象 privatestaticTreeNodeConfigdefaultConfig=newTreeNodeConfig(); //树节点默认属性常量 staticfinalStringTREE_ID="id"; staticfinalStringTREE_NAME="name"; staticfinalStringTREE_CODE="code"; staticfinalStringTREE_CHILDREN="children"; staticfinalStringTREE_PARENT_ID="parentId"; //属性 privateStringidKey; privateStringcodeKey; privateStringnameKey; privateStringchildrenKey; privateStringparentIdKey; publicStringgetIdKey(){ returngetOrDefault(idKey,TREE_ID); } publicvoidsetIdKey(StringidKey){ this.idKey=idKey; } publicStringgetCodeKey(){ returngetOrDefault(codeKey,TREE_CODE); } publicvoidsetCodeKey(StringcodeKey){ this.codeKey=codeKey; } publicStringgetNameKey(){ returngetOrDefault(nameKey,TREE_NAME); } publicvoidsetNameKey(StringnameKey){ this.nameKey=nameKey; } publicStringgetChildrenKey(){ returngetOrDefault(childrenKey,TREE_CHILDREN); } publicvoidsetChildrenKey(StringchildrenKey){ this.childrenKey=childrenKey; } publicStringgetParentIdKey(){ returngetOrDefault(parentIdKey,TREE_PARENT_ID); } publicvoidsetParentIdKey(StringparentIdKey){ this.parentIdKey=parentIdKey; } publicStringgetOrDefault(Stringkey,StringdefaultKey){ if(key==null){ returndefaultKey; } returnkey; } publicstaticTreeNodeConfiggetDefaultConfig(){ returndefaultConfig; } } 3 最后,改造TreeUtil.build 方法,基于2.0版本,只需将TreeNode替换成TreeNodeMap即可。 /** *树构建 */ publicstatic<T>List<TreeNodeMap>build(List<T>list,ObjectparentId,Convert<T,TreeNodeMap>convert){ List<TreeNodeMap>treeNodes=CollectionUtil.newArrayList(); for(Tobj:list){ TreeNodeMaptreeNode=newTreeNodeMap(); convert.convert(obj,treeNode); treeNodes.add(treeNode); } List<TreeNodeMap>finalTreeNodes=CollectionUtil.newArrayList(); for(TreeNodeMaptreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add(treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } 测试代码 publicstaticvoidmain(String[]args){ //...省略菜单模拟数据的创建过程 TreeNodeConfigtreeNodeConfig=newTreeNodeConfig(); //自定义属性名 treeNodeConfig.setCodeKey("number"); List<TreeNodeMap>treeNodes=TreeUtil_4_0.build(menuEntityList,"0",treeNodeConfig,newTreeUtil_4_0.Convert<MenuEntity,TreeNodeMap>(){ @Override publicvoidconvert(MenuEntityobject,TreeNodeMaptreeNode){ treeNode.setId(object.getId()); treeNode.setParentId(object.getPid()); treeNode.setCode(object.getCode()); treeNode.setName(object.getName()); //属性扩展 treeNode.extra("extra1","123"); } }); Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeNodes))); } 经过上面的改造,我们实现了树节点属性的自定义,顺便还实现了属性可扩展,一举两得。 3、总结 目前这个程度可能仍有些场景无法满足,但是对于大部分的问题场景基于3.0或4.0版本稍加改造应该都可以解决。剩下的就结合场景再酌情优化调整。 4、源代码&视频 springboot-tree 5、更多精彩 觉得还行,动动手指留个赞。 以上就是今天的内容,我们下期见。 更多优质内容,首发公众号【风象南】,欢迎关注。

优秀的个人博客,低调大师

TiDB和C#的简单CRUD应用程序

\n> 原文来源:https://tidb.net/blog/845e490e\n\n本文演示如何使用C#语言实现对TiDB的基础增删改查操作,包含了C#中常用的几种数据库访问方式。 相关环境 Ubuntu 18.04 .NET 6.0 C# 10 Visual Studio Code 1.63.2 TiDB 6.0-DMR 创建TiDB测试集群 你可以使用以下方式快速搭建一个TiDB测试集群: 使用TiDB Cloud免费创建在线集群 使用TiUP部署本地测试集群 使用TiUP部署标准TiDB集群 使用 TiDB Operator在Kubernetes中部署TiDB集群 本文仅用于代码演示,在单机环境使用TiUP Playground搭建了一套最基础的测试集群: [root@dbserver1 ~]# tiup playground v6.0.0 --db 1 --pd 1 --kv 3 --tag tidb tiup is checking updates for component playground ...timeout! Starting component `playground`: /root/.tiup/components/playground/v1.9.5/tiup-playground /root/.tiup/components/playground/v1.9.5/tiup-playground v6.0.0 --db 1 --pd 1 --kv 3 --tag tidb Playground Bootstrapping... Start pd instance:v6.0.0 Start tikv instance:v6.0.0 Start tikv instance:v6.0.0 Start tikv instance:v6.0.0 Start tidb instance:v6.0.0 Waiting for tidb instances ready 127.0.0.1:4000 ... Done Start tiflash instance:v6.0.0 Waiting for tiflash instances ready 127.0.0.1:3930 ... Done CLUSTER START SUCCESSFULLY, Enjoy it ^-^ To connect TiDB: mysql --comments --host 127.0.0.1 --port 4000 -u root -p (no password) To view the dashboard: http://127.0.0.1:2379/dashboard PD client endpoints: [127.0.0.1:2379] To view the Prometheus: http://127.0.0.1:9090 To view the Grafana: http://127.0.0.1:3000 数据库启动成功之后,我们用官方提供的Bookshop示例应用作为测试数据,使用如下命令生成数据: tiup demo bookshop prepare --users=1000 --books=5000 --authors=1000 --ratings=10000 --orders=2000 --drop-tables 最后看一下测试数据的生成情况: +-----------------------+----------------+-----------+------------+--------+ | Table Name | Number of Rows | Data Size | Index Size | Total | +-----------------------+----------------+-----------+------------+--------+ | bookshop.orders | 2000 | 0.08MB | 0.02MB | 0.09MB | | bookshop.ratings | 10000 | 0.31MB | 0.31MB | 0.61MB | | bookshop.book_authors | 5000 | 0.08MB | 0.08MB | 0.15MB | | bookshop.authors | 1000 | 0.04MB | 0.00MB | 0.04MB | | bookshop.users | 999 | 0.03MB | 0.01MB | 0.04MB | | bookshop.books | 5000 | 0.28MB | 0.00MB | 0.28MB | +-----------------------+----------------+-----------+------------+--------+ 6 rows in set (0.01 sec) 创建C#应用程序 为了简化演示代码,这里构建一个最简单的控制台应用程序用于数据库访问。 dotnet new console --name tidb-example-csharp --framework net6.0 看一下这个控制台程序的项目结构: dc@dc-virtual-machine:~/dotnet$ ll tidb-example-csharp/ total 20 drwxrwxr-x 3 dc dc 4096 May 17 16:43 ./ drwxrwxr-x 3 dc dc 4096 May 17 16:43 ../ drwxrwxr-x 2 dc dc 4096 May 17 16:43 obj/ -rw-rw-r-- 1 dc dc 105 May 17 16:43 Program.cs -rw-rw-r-- 1 dc dc 305 May 17 16:43 tidb-example-csharp.csproj tidb-example-csharp.csproj是项目工程文件,Program.cs是程序入口文件。 验证一下程序是否能正常运行: dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run Hello, World! 使用Oracle Connector/NET Connector/NET是MySQL官方提供的符合标准ADO.NET体系的数据库访问驱动,由于TiDB高度兼容MySQL协议,所以市面上MySQL能使用的驱动基本都能用在TiDB上面。 ADO.NET体系结构(图片来自微软官网): 如果要以ADO.NET接口方式访问TiDB,首先安装驱动程序包: dotnet add package MySql.Data --version 8.0.29 测试是否能连接上TiDB: using MySql.Data.MySqlClient; const string conectionStr = "Server=127.0.0.1;UserId=root;Password=;Port=4000;Database=bookshop"; ​ public void TestConnection() { MySqlConnection conn = new MySqlConnection(conectionStr); conn.Open(); var cmd = conn.CreateCommand(); cmd.CommandText = "select tidb_version()"; var result = cmd.ExecuteScalar(); Console.WriteLine(result); conn.Close(); } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run Release Version: v6.0.0 Edition: Community Git Commit Hash: 36a9810441ca0e496cbd22064af274b3be771081 Git Branch: heads/refs/tags/v6.0.0 UTC Build Time: 2022-03-31 10:33:28 GoVersion: go1.18 Race Enabled: false TiKV Min Version: v3.0.0-60965b006877ca7234adaced7890d7b029ed1306 Check Table Before Drop: false 一个简单的查询示例: public void TestRead() { MySqlConnection conn = new MySqlConnection(conectionStr); conn.Open(); var cmd = conn.CreateCommand(); cmd.CommandText = "select * from books limit 5"; MySqlDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { Console.WriteLine($"id: {reader["id"]} title: {reader["title"]} type: {reader["type"]} published_at: {reader["published_at"]}"); } conn.Close(); } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run id: 648872 title: Sam Mayert type: Life published_at: 2/26/1953 12:53:33 PM id: 6583703 title: Aron Kilback type: Kids published_at: 11/23/1923 9:19:43 AM id: 6810515 title: Chelsey Dickens type: Education & Reference published_at: 4/8/1985 9:23:37 PM id: 7884508 title: Annetta Rodriguez type: Education & Reference published_at: 5/11/1962 9:54:58 PM id: 8683541 title: The Documentary of hamster type: Magazine published_at: 10/3/1945 1:44:52 AM 一个简单的新增修改删除示例: public void TestWrite() { MySqlConnection conn = new MySqlConnection(conectionStr); conn.Open(); ​ int bookId = 888888; ​ var cmd1 = new MySqlCommand("insert into books values (@id,@title,@type,@published_at,@stock,@price)", conn); cmd1.Parameters.AddWithValue("@id", bookId); cmd1.Parameters.AddWithValue("@title", "TiDB in action"); cmd1.Parameters.AddWithValue("@type", "Science & Technology"); cmd1.Parameters.AddWithValue("@published_at", DateTime.Now); cmd1.Parameters.AddWithValue("@stock", 1000); cmd1.Parameters.AddWithValue("@price", 66.66); int insertCnt = cmd1.ExecuteNonQuery(); Console.WriteLine($"insert successed {insertCnt} books."); TestQueryBook(conn, bookId); ​ var cmd2 = new MySqlCommand("update books set stock=stock-1 where id=@id", conn); cmd2.Parameters.AddWithValue("@id", bookId); int updateCnt = cmd2.ExecuteNonQuery(); Console.WriteLine($"update successed {updateCnt} books."); TestQueryBook(conn, bookId); ​ var cmd3 = new MySqlCommand("delete from books where id=@id", conn); cmd3.Parameters.AddWithValue("@id", bookId); int deleteCnt = cmd3.ExecuteNonQuery(); Console.WriteLine($"delete successed {updateCnt} books."); TestQueryBook(conn, bookId); ​ conn.Close(); } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run insert successed 1 books. id: 888888 title: TiDB in action type: Science & Technology published_at: 5/18/2022 3:22:02 PM stock: 1000 price: 66.66 update successed 1 books. id: 888888 title: TiDB in action type: Science & Technology published_at: 5/18/2022 3:22:02 PM stock: 999 price: 66.66 delete successed 1 books. book id 888888 not found. 更多API用法可以参考官方文档: https://dev.mysql.com/doc/connector-net/en/connector-net-programming.html https://dev.mysql.com/doc/connector-net/en/connector-net-tutorials.html` 注意:对于这种数据库IO类型请求,建议使用API的异步同名方法提高程序处理效率,例如ExecuteNonQueryAsync。 这里梳理一下连接字符串中的核心参数。 参数名 描述 默认值 Server , Host , Data Source , DataSource 数据库连接主机 localhost Port 数据库连接端口 3306 UserID , User Id , Username , Uid , User name , User 数据库登录用户名 null Password , pwd 数据库登录用户密码 ConnectionTimeout , Connect Timeout , Connection Timeout 数据量连接超时时间 15s DefaultCommandTimeout , Default Command Timeout SQL执行的超时时间 30s Pooling 是否启用连接池 true ConnectionLifeTime , Connection Lifetime 连接过期时间 超过这个时间的连接会被销毁重新创建 0(不限制) MaxPoolSize, Max Pool Size 连接池最大连接数 100 MinPoolSize, Min Pool Size 连接池最小连接数 0 TableCaching , Table Cache , TableCache 是否开启客户端表数据缓存 false DefaultTableCacheAge , Default Table Cache Age 客户端表数据缓存时间 60s AllowBatch , Allow Batch 是否允许批量SQL执行 true 关于连接池参数的最佳实践可以参考TiDB官网文档。 使用MySqlConnector MySqlConnector也是广泛使用的一种实现了ADO.NET接口的MySQL驱动,它提供了比MySql.Data更好的异步性能,很多ORM框架底层都是依赖于MySqlConnector实现对MySQL的访问。 首先在项目中安装依赖包: dotnet add package MySqlConnector --version 2.1.9 看一个数据库连接示例: using MySqlConnector; ​ const string conectionStr = "Server=127.0.0.1;UserId=root;Password=;Port=4000;Database=bookshop"; ​ public async Task TestConnection() { using (var conn = new MySqlConnection(conectionStr)) { await conn.OpenAsync(); using (var cmd = new MySqlCommand("select tidb_version()", conn)) { var result = await cmd.ExecuteScalarAsync(); Console.WriteLine(result); } } } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run Release Version: v6.0.0 Edition: Community Git Commit Hash: 36a9810441ca0e496cbd22064af274b3be771081 Git Branch: heads/refs/tags/v6.0.0 UTC Build Time: 2022-03-31 10:33:28 GoVersion: go1.18 Race Enabled: false TiKV Min Version: v3.0.0-60965b006877ca7234adaced7890d7b029ed1306 Check Table Before Drop: false 可以看到使用方式和Connector/NET非常相似都是标准的ADO.NET风格,MySqlConnector的连接字符串参数绝大部分都兼容 Connector/NET,并且在此基础上提供了一些新的特性,比如负载均衡功能: 参数名 说明 默认值 Server, Host, Data Source, DataSource, Address, Addr, Network Address 数据库请求地址 可以用逗号分隔填写多个地址实现负载均衡 localhost Load Balance, LoadBalance 负载均衡策略, 支持RoundRobin、LeastConnections、Failover三种模式 需要开启连接池 RoundRobin 【注意】 有些参数在MySqlConnector已经禁用了,更多差异和新增功能参考官网文档:https://mysqlconnector.net/connection-options/ 除了可以使用连接字符串,MySqlConnector还支持Builder对象模式,连接串里的参数都能在MySqlConnectionStringBuilder找到对应的字段,例如: var builder = new MySqlConnectionStringBuilder { Server = "your-server", UserID = "database-user", Password = "P@ssw0rd!", Database = "database-name", }; ​ // open a connection asynchronously using var connection = new MySqlConnection(builder.ConnectionString); await connection.OpenAsync(); 一个简单的查询示例: public async Task TestRead() { using (var conn = new MySqlConnection(conectionStr)) { await conn.OpenAsync(); ​ var cmd = conn.CreateCommand(); cmd.CommandText = "select * from users limit 5;"; MySqlDataReader reader =await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { Console.WriteLine($"id: {reader.GetInt32("id")} balance: {reader.GetDecimal("balance")} nicknmame: {reader.GetString("nickname")} "); } } } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run id: 525196 balance: 9490.89 nicknmame: Goodwin4601 id: 822804 balance: 1197.81 nicknmame: Treutel4269 id: 4147652 balance: 349.36 nicknmame: Pacocha6285 id: 9704562 balance: 2292.28 nicknmame: Grady8130 id: 17101775 balance: 5054.69 nicknmame: Macejkovic7559 一个简单的增删改示例: public async Task TestWrite() { using (var conn = new MySqlConnection(conectionStr)) { await conn.OpenAsync(); ​ int userId = 888888; ​ var cmd1 = new MySqlCommand("insert into users values (@id,@balance,@nickname)", conn); cmd1.Parameters.AddWithValue("@id", userId); cmd1.Parameters.AddWithValue("@balance", 0.01); cmd1.Parameters.AddWithValue("@nickname", "hey-hoho"); int insertCnt = await cmd1.ExecuteNonQueryAsync(); Console.WriteLine($"insert successed {insertCnt} users."); TestQueryUser(conn, userId); ​ var cmd2 = new MySqlCommand("update users set balance=balance+99 where id=@id", conn); cmd2.Parameters.AddWithValue("@id", userId); int updateCnt =await cmd2.ExecuteNonQueryAsync(); Console.WriteLine($"update successed {updateCnt} users."); TestQueryUser(conn, userId); ​ var cmd3 = new MySqlCommand("delete from users where id=@id", conn); cmd3.Parameters.AddWithValue("@id", userId); int deleteCnt = await cmd3.ExecuteNonQueryAsync(); Console.WriteLine($"delete successed {updateCnt} users."); TestQueryUser(conn, userId); ​ } } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run insert successed 1 users. id: 888888 balance: 0.01 nicknmame: hey-hoho update successed 1 users. id: 888888 balance: 99.01 nicknmame: hey-hoho delete successed 1 users. user id 888888 not found. 一个简单的事务使用示例: public async Task TestTransaction() { // 用一个事务演示购书流程 using (var conn = new MySqlConnection(conectionStr)) { await conn.OpenAsync(); ​ int userId = 525196, bookId = 648872; decimal price = 15; ​ var tnx = await conn.BeginTransactionAsync(); ​ try { // 新增一个订单 var cmd1 = new MySqlCommand("insert into orders values(@id, @book_id, @user_id, @quality, @ordered_at)", conn, tnx); cmd1.Parameters.AddWithValue("@id", 999999); cmd1.Parameters.AddWithValue("@book_id", bookId); cmd1.Parameters.AddWithValue("@user_id", userId); cmd1.Parameters.AddWithValue("@quality", 1); cmd1.Parameters.AddWithValue("@ordered_at", DateTime.Now); await cmd1.ExecuteNonQueryAsync(); ​ // 扣减账户余额 var cmd2 = new MySqlCommand("update users set balance=balance-@price where id =@id", conn, tnx); cmd2.Parameters.AddWithValue("@id", userId); cmd2.Parameters.AddWithValue("@price", price); await cmd2.ExecuteNonQueryAsync(); ​ // 更新商品库存 var cmd3 = new MySqlCommand("update books set stock=stock-1 where id =@id", conn, tnx); cmd3.Parameters.AddWithValue("@id", bookId); await cmd3.ExecuteNonQueryAsync(); ​ // 提交事务 await tnx.CommitAsync(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); // 异常回滚事务 await tnx.RollbackAsync(); } } } 需要注意的是,如果有大量重复的SQL需要执行,建议使用TiDB的Prepare Statement特性,它能有效减少SQL编译解析的时间提升执行效率,使用示例: cmd.CommandText = "insert into books values (@id, @title, @type, @published_at, @stock, @price)"; cmd.Parameters.Add("@id", MySqlDbType.Int32); cmd.Parameters.Add("@title", MySqlDbType.String); ... cmd.Prepare(); ​ for (int i = 1; i <= 1000; i++) { cmd.Parameters["@id"].Value = i; cmd.Parameters["@title"].Value = $"TiDB in action - {i}"; ... await cmd.ExecuteNonQueryAsync(); } 更多用法可以参考官网文档:https://mysqlconnector.net/ 使用Entity Framework Entity Framework (EF)是.NET领域最知名的跨平台数据库访问ORM框架,它最早在2008年作为.NET Framework的一部分发布,现在的最新版本是EF Core 6.0,也已经开源。 它支持丰富的数据访问驱动,基于这个特性我们可以使用一套统一的API接口访问各种类型的数据库,比如Sqlite、SQL Server、MySQL、PostgreSQL、Spanner等等。在MySQL协议上,广泛使用的驱动有两个: Pomelo.EntityFrameworkCore.MySql MySql.EntityFrameworkCore 在使用之前先安装Entity Framework的基础包: dotnet add package Microsoft.EntityFrameworkCore --version 6.0.5 dotnet add package Microsoft.Extensions.Logging.Console //记录日志用,非必须 Pomelo.EntityFrameworkCore.MySql Pomelo.EntityFrameworkCore.MySql是最流行的兼容MySQL协议的EF Core驱动,也是微软官方推荐使用的方式,它底层依赖于前面提到的MySqlConnector,所以在连接串配置上并无差别。 dotnet add package Pomelo.EntityFrameworkCore.MySql --version 6.0.1 先构造一个DbContext类型和对应的实体类: using Microsoft.EntityFrameworkCore; using Pomelo.EntityFrameworkCore.MySql.Infrastructure; using Pomelo.EntityFrameworkCore.MySql; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.Extensions.Logging; ​ public class BookShopContext : DbContext { const string conectionStr = "Server=127.0.0.1;UserId=root;Password=;Port=4000;Database=bookshop"; ​ public DbSet<User> Users { get; set; } public DbSet<Book> Books { get; set; } ​ protected override void OnConfiguring(DbContextOptionsBuilder options) { options.UseMySql(conectionStr, ServerVersion.Create(new Version("5.7.25"), ServerType.MySql)); ​ options.UseLoggerFactory(LoggerFactory.Create(builder => { builder.AddConsole(); })); } } ​ public class User { [Column("id")] public long Id { get; set; } [Column("balance")] public decimal Balance { get; set; } [Column("nickname")] public string Nickname { get; set; } } ​ public class Book { [Column("id")] public long Id { get; set; } [Column("title")] public string Title { get; set; } [Column("type")] public string Type { get; set; } [Column("published_at")] public DateTime PublishedAt { get; set; } [Column("stock")] public int Stock { get; set; } [Column("price")] public decimal Price { get; set; } } 验证是否能连接上TiDB: public async Task TestConnection() { using var context = new BookShopContext(); Console.WriteLine($"TiDB CanConnect: {await context.Database.CanConnectAsync()}"); } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run TiDB CanConnect: True 一个单表分页查询结果集的示例: public async Task TestRead() { using var context = new BookShopContext(); ​ var books = await context.Books.Where(b => b.Title.Contains("db")).OrderBy(b => b.Id).Skip(2).Take(5).ToListAsync(); foreach (var book in books) { Console.WriteLine($"id: {book.Id} title: {book.Title} type: {book.Type} published_at: {book.PublishedAt}"); } } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run info: Microsoft.EntityFrameworkCore.Infrastructure[10403] Entity Framework Core 6.0.5 initialized 'BookShopContext' using provider 'Pomelo.EntityFrameworkCore.MySql:6.0.1' with options: ServerVersion 5.7.25-mysql info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (47ms) [Parameters=[@__p_1='?' (DbType = Int32), @__p_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SELECT `b`.`id`, `b`.`price`, `b`.`published_at`, `b`.`stock`, `b`.`title`, `b`.`type` FROM `Books` AS `b` WHERE `b`.`title` LIKE '%db%' ORDER BY `b`.`id` LIMIT @__p_1 OFFSET @__p_0 id: 474329852 title: Geovany Padberg type: Science & Technology published_at: 2010/1/28 6:45:33 id: 1890134379 title: The Adventures of Hilda Padberg type: Comics published_at: 1943/7/17 16:28:02 id: 2181887016 title: Catalina Padberg type: Education & Reference published_at: 1961/3/28 18:18:37 id: 2193223665 title: Caroline Padberg type: Kids published_at: 1994/11/12 20:56:53 id: 2359817065 title: Darryl Padberg type: Science & Technology published_at: 1940/11/13 9:53:21 一个简单的增删改示例: public async Task TestWrite() { using var context = new BookShopContext(); ​ long userId = 888888; ​ var user = new User { Id = userId, Balance = 0.01M, Nickname = "hey-hoho" }; context.Add(user); int insertCnt = await context.SaveChangesAsync(); Console.WriteLine($"insert successed {insertCnt} users."); TestQueryUser(context, userId); ​ user.Balance += 99; int updateCnt = await context.SaveChangesAsync(); Console.WriteLine($"update successed {updateCnt} users."); TestQueryUser(context, userId); ​ context.Remove(user); int deleteCnt = await context.SaveChangesAsync(); Console.WriteLine($"delete successed {deleteCnt} users."); TestQueryUser(context, userId); } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run insert successed 1 users. id: 888888 balance: 0.01 nicknmame: hey-hoho update successed 1 users. id: 888888 balance: 99.01 nicknmame: hey-hoho delete successed 1 users. user id 888888 not found. 【注意】 当只需要查询结果而不用对结果进行修改时,建议关闭EF Core的tracking功能,它能提高执行速度。比如: // 单个查询关闭跟踪 var books = await context.Books.OrderBy(b => b.Id).Take(3).AsNoTracking().ToListAsync(); // 整个上下文关闭跟踪 options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); Pomelo.EntityFrameworkCore.MySql的文档地址: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/wiki MySql.EntityFrameworkCore MySql.EntityFrameworkCore是Oracle官方发布的Entity Framework支持MySQL的数据库驱动,所以它的底层是依赖于Connector/NET。 如果你想使用Connector/NET 8.0.28及以后的版本,那么安装这个包: dotnet add package MySql.EntityFrameworkCore --version 6.0.1 //版本对应.NET的版本 如果你使用的是Connector/NET早期版本,那么安装这个包: dotnet add package MySql.Data.EntityFrameworkCore --version 8.0.22 在使用方式上只有配置连接串上的一点点差别: protected override void OnConfiguring(DbContextOptionsBuilder options) { options.UseMySQL("server=localhost;database=library;user=user;password=password"); } 其他数据库操作都是用标准的EF API。 MySql.EntityFrameworkCore的文档地址: https://dev.mysql.com/doc/connector-net/en/connector-net-entity-framework.html 总体来说,使用Entity Framework操作TiDB并没有什么特殊的地方,有使用经验的开发者几乎不用任何学习成本就能快速上手。 Entity Framework更多用法可以参考官网文档:https://docs.microsoft.com/en-us/ef/core/ 使用Dapper Dapper是StackExchange开源的一款轻量级ORM框架,它以高性能著称,在.NET领域使用非常广泛。它扩展了ADO.NET的IDbConnection接口,底层同样依赖于MySqlConnector或者Connector/NET,使用方式介于原生ADO.NET和Entity Framework之间。 dotnet add package Dapper.StrongName --version 2.0.123 测试数据库连接: using Dapper; using MySqlConnector; ​ const string conectionStr = "Server=127.0.0.1;UserId=root;Password=;Port=4000;Database=bookshop"; ​ public async Task TestConnection() { MySqlConnection conn = new MySqlConnection(conectionStr); var version =await conn.ExecuteScalarAsync<string>("select tidb_version()"); Console.WriteLine(version); } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run Release Version: v6.0.0 Edition: Community Git Commit Hash: 36a9810441ca0e496cbd22064af274b3be771081 Git Branch: heads/refs/tags/v6.0.0 UTC Build Time: 2022-03-31 10:33:28 GoVersion: go1.18 Race Enabled: false TiKV Min Version: v3.0.0-60965b006877ca7234adaced7890d7b029ed1306 Check Table Before Drop: false 使用强类型的查询示例: public class Book { public long Id { get; set; } public string Title { get; set; } public string Type { get; set; } public DateTime Published_At { get; set; } public int Stock { get; set; } public decimal Price { get; set; } } ​ public async Task TestQuery() { MySqlConnection conn = new MySqlConnection(conectionStr); ​ var book = await conn.QueryFirstAsync<Book>("select * from books where id = @id", new { id = 8683541 }); Console.WriteLine($"id: {book.Id} title: {book.Title} type: {book.Type} published_at: {book.Published_At} stock: {book.Stock} price: {book.Price}"); ​ var books = await conn.QueryAsync<Book>("select * from books where type = @type and stock < @stock order by published_at limit 3", new { type = "Sports" stock = 100 }); books.ToList().ForEach(b => Console.WriteLine($"id: {b.Id} title: {b.Title} type: {b.Type} published_at: {b.Published_At} stock: {b.Stock} price: {b.Price}") ); } dc@dc-virtual-machine:~/dotnet/tidb-example-csharp$ dotnet run id: 8683541 title: The Documentary of hamster type: Magazine published_at: 1945/10/3 1:44:52 stock: 190 price: 74.38 ​ id: 1201887882 title: Nayeli Luettgen type: Sports published_at: 1901/11/20 19:37:37 stock: 53 price: 141.50 id: 3705800883 title: Arianna Considine type: Sports published_at: 1905/8/31 5:25:53 stock: 72 price: 312.93 id: 1811359739 title: Dawson Hackett type: Sports published_at: 1910/12/1 21:22:47 stock: 72 price: 415.72 带事务的增删改示例: public class Order { public long Id { get; set; } public long Book_Id { get; set; } public long User_Id { get; set; } public int Quality { get; set; } public DateTime Ordered_At { get; set; } } // 用一个事务演示购书流程 public async Task TestTransaction() { MySqlConnection conn = new MySqlConnection(conectionStr); ​ int userId = 525196, bookId = 648872; decimal price = 15; ​ await conn.OpenAsync(); var tnx = await conn.BeginTransactionAsync(); ​ try { // 新增一个订单 var order = new Order { Id = 666666, Book_Id = bookId, User_Id = userId, Quality = 1, Ordered_At = DateTime.Now }; await conn.ExecuteAsync("insert into orders values(@id, @book_id, @user_id, @quality, @ordered_at)", order, tnx); ​ // 更新账户余额 await conn.ExecuteAsync("update users set balance=balance-@price where id =@id", new { id = userId, price = price }, tnx); ​ // 更新商品库存 await conn.ExecuteAsync("update books set stock=stock-1 where id =@id", new { id = bookId }, tnx); ​ // 提交事务 await tnx.CommitAsync(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); // 异常回滚事务 await tnx.RollbackAsync(); } } Dapper一般情况下会自动管理连接的开启关闭状态,使用起来比原生ADO.NET更加方便。但是要注意的是,在使用显式事务的时候要提前手动打开连接,否则会报异常System.InvalidOperationException: Connection is not open.。 Dapper会缓存每一次查询语句,因此推荐的做法是使用参数化方式进行传参,一方面能提高SQL执行效率,另一方面可以减少内存占用。 更多用法可以参考官方文档:https://github.com/DapperLib/Dapper 最佳实践 原生ADO.NET能够带来非常优秀的性能,但是缺点就是需要大量的手写SQL和类型转换,对于应用开发不是很友好。而ORM框架虽然解决了代码复杂度的问题,但也带来了新的问题,就是无法精准控制SQL的行为不够灵活,以及大幅性能损耗。 所以在实际项目中,推荐使用Entity Framework+Dapper的组合,底层数据驱动选择MySqlConnector,简单读写场景交给Entity Framework去处理,想高度控制SQL的场景交给Dapper去处理,兼顾了性能和开发效率两方面。Entity Framework的上下文对象提供了一个访问原始ADO.NET Connection的入口,这就使得把两者结合起来非常方便。 需要导入的程序集: Microsoft.EntityFrameworkCore Pomelo.EntityFrameworkCore.MySql Dapper 使用示例: using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Pomelo.EntityFrameworkCore.MySql.Infrastructure; using Dapper; ​ public async Task TestWithDapper() { using var context = new BookShopContext(); var conn = context.Database.GetDbConnection(); ​ // 使用dapper查询 string version = conn.ExecuteScalar<string>("select tidb_version()"); Console.WriteLine(version); ​ // 使用EF查询 var book = context.Books.Where(u => u.Id == 1).FirstOrDefault(); ​ // 共享事务,类似的还可以使用TransactionScope或EF的UseTransaction using IDbContextTransaction tnx = context.Database.BeginTransaction(); conn.ExecuteScalar<dynamic>("select /*+ read_from_storage(tiflash[ratings]) */ book_id,count(*),avg(score) from ratings group by book_id", transaction: tnx.GetDbTransaction()); context.Books.Add(new Book { }); await tnx.CommitAsync(); } 【注意】 using会自动处理IDbContextTransaction对象的rollback和dispose,如果没有使用using语法,需要手动处理事务异常情况。 还有其他一些ORM框架例如NHibernate、FreeSql等使用方式大同小异,底层都是依赖前面提到的数据库驱动,参考各自API文档即可。 推荐阅读: TiDB与MySQL兼容性对比 TiDB SQL开发规范 TiDB性能调优最佳实践

优秀的个人博客,低调大师

HasorDB 4.3.2 发布,增强纯 Map 模式下 CRUD 的使用

介绍 HasorDB 是一个全功能数据库访问工具,提供对象映射、丰富的类型处理、动态SQL、存储过程、内置分页方言20+、支持嵌套事务、多数据源、条件构造器、INSERT 策略、多语句/多结果。并兼容 Spring 及 MyBatis 用法。它不依赖任何其它框架,因此可以很方便的和任意一个框架整合在一起使用。 功能特性 熟悉的方式 JdbcTemplate 接口方式(高度兼容 Spring JDBC) Mapper 文件方式(高度兼容 MyBatis) LambdaTemplate (高度接近 MyBatis Plus、jOOQ 和 BeetlSQL) @Insert、@Update、@Delete、@Query、@Callable 注解(类似 JPA) 事务支持 支持 5 个事务隔离级别、7 个事务传播行为(与 Spring tx 相同) 提供 TransactionTemplate、TransactionManager 接口方式声明式事务控制能力(用法与 Spring 相同) 特色优势 支持 分页查询 并且提供多种数据库方言(20+) 支持 INSERT 策略(INTO、UPDATE、IGNORE) 更加丰富的 TypeHandler(MyBatis 40+,HasorDB 60+) Mapper XML 支持多语句、多结果 提供独特的@{xxx, expr , xxxxx }规则扩展机制,让动态 SQL 更加简单 支持 存储过程 支持 JDBC 4.2 和 Java8 中时间类型 支持多数据源 Release.Node 新特性,针对纯 Map 的 Lambda 支持更加友好,目前可以无实体方式使用 LambdaTemplate 官方文档站:http://www.hasor.cn、https://www.hasordb.net 引入依赖 截止到目前为止 HasorDB 的最新版本为:4.3.2 在https://mvnrepository.com/artifact/net.hasor/hasor-db上也可以查询到最新版本 <dependency> <groupId>net.hasor</groupId> <artifactId>hasor-db</artifactId> <version>4.3.2</version> </dependency> 然后再引入数据库驱动以 MySQL,Maven 方式为例: <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.22</version> </dependency> 使用 HasorDB 可以不依赖数据库连接池,但有数据库连接池是大多数项目的标配。这里选用 Alibaba 的 Druid <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.23</version> </dependency> 最后准备一个数据库表,并初始化一些数据(CreateDB.sql文件) drop table if exists `test_user`; create table `test_user` ( `id` int(11) auto_increment, `name` varchar(255), `age` int, `create_time` datetime, primary key (`id`) ); insert into `test_user` values (1, 'mali', 26, now()); insert into `test_user` values (2, 'dative', 32, now()); insert into `test_user` values (3, 'jon wes', 41, now()); insert into `test_user` values (4, 'mary', 66, now()); insert into `test_user` values (5, 'matt', 25, now()); 执行 SQL 使用 SQL 的方式读取数据,PrintUtils和DsUtils两个工具类可以在例子工程中找到 // 创建数据源 DataSource dataSource = DsUtils.dsMySql(); LambdaTemplate lambdaTemplate = new LambdaTemplate(dataSource); // 新增Map<String, Object> newValue = new HashMap<>(); newValue.put("id", 20); newValue.put("name", "new name"); newValue.put("age", 88); newValue.put("create_time", new Date()); InsertOperation<Map<String, Object>> insert = lambdaTemplate.lambdaInsert("test_user"); int result = insert.applyMap(newValue).executeSumResult() // 更新Map<String, Object> updateValue = new HashMap<>(); updateValue.put("name", "new name"); updateValue.put("age", 88); MapUpdateOperation update = lambdaTemplate.lambdaUpdate("test_user"); int result = update.eq("id", 1).updateByMap(updateValue).doUpdate(); // 查询 List<Map<String, Object>> mapList = jdbcTemplate.queryForList("select * from test_user"); // 打印测试数据 PrintUtils.printMapList(mapList)

优秀的个人博客,低调大师

mybatis-plus-generator 3.5.0 发布,CRUD 代码也不用写了

mybatis-plus-generator 3.5.0 发布,该版本为重构版本,可能存在部分历史API不兼容情况,本次主要升级内容为简化配置链式操作。 源码仓库 H2 生成代码示例: new SimpleAutoGenerator() { @Override public IConfigBuilder<DataSourceConfig> dataSourceConfigBuilder() { return new DataSourceConfig.Builder("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;CASE_INSENSITIVE_IDENTIFIERS=TRUE", "sa", ""); } }.execute(); 升级日志 优化代码重构核心构建方式 修复swagger注释包含双引号生成代码错误 swagger2 修改为 swagger 避免以为支持 swagger2 版本 修复生成驼峰命名属性字段转换错误 修复mysql自动生成代码类型错误(bit,year类型) 修复h2数据库主键自增判断错误 修复无乐观锁或逻辑删除字段导入多余的包 修复Oscar(神通数据库)生成错误 修复高斯数据库生成错误 修复SQLServer日期生成错误 增加虚谷数据库代码生成支持 增加ClickHouse代码生成支持 支持Beetl模板3.2.x版代码生成 去除PG中不包含的clob、blob类型,二进制类型调整为byte类型 支持PostgreSql大写表名 支持基于模型属性字段填充 支持通过数据源构建DataSourceConfig velocity提示1.x版本依赖过时,输出日志警告信息 表的主键为自增主键时会导致全局主键的ID类型设置生效,输出日志警告信息 移除lombok依赖 优化 Jdbc 连接关闭逻辑 PackageConfig,DataSourceConfig,GlobalConfig,StrategyConfig,TemplateConfig更改为构建者模式. Entity 新增 ignoreColumns 支持忽略指定字段不生成 文本输入 scanner 读取 next 修改为 nextLine

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册