献给转java的c#和java程序员的数据库orm框架 - 薛家明 - 博客园

mikel阅读(524)

来源: 献给转java的c#和java程序员的数据库orm框架 – 薛家明 – 博客园

献给转java的C#和java程序员的数据库orm框架

一个好的程序员不应被语言所束缚,正如我现在开源java的orm框架一样,如果您是一位转java的C#程序员,那么这个框架可以带给你起码没有那么差的业务编写和强类型体验。如果您是一位java程序员,那么该框架可以提供比Mybatis-Plus功能更加丰富、性能更高,更加轻量和完全免费的体验来做一个happy coding crud body。

背景

easy-query该框架是我在使用Mybatis-Plus(下面统称MP) 2年后开发的,因为MP不支持多表(不要提join插件(逻辑删除子表不支持)),并且Mybatis原本的xml十分恶心,导致项目中有非常多的代码需要编写SQL,并且整体数据库架构因为存在逻辑删除字段和多租户字段所以编写的SQL基本上多多少少都会有问题,我不相信大家没遇到过,而且MP得一些功能还需要收费这大大让我坚定还是自己开发一款。

介绍

easy-query 🚀 是一款无任何依赖的JAVA ORM 框架,十分轻量,拥有非常高的性能,支持单表查询、多表查询、union、子查询、分页、动态表名、VO对象查询返回、逻辑删、全局拦截、数据库列加密(支持高性能like查询)、数据追踪差异更新、乐观锁、多租户、自动分库、自动分表、读写分离,支持框架全功能外部扩展定制,拥有强类型表达式。

📚 文档

GITHUB地址 | GITEE地址

缺点

先说一下缺点,目前只适配了MySql,不过基本上如果你是pgsql很少需要改动就直接可以用了,其他数据库可能因为自己的语法和特性会需要稍微做一下修改但是整体而言无需过多的变动,框架已经全部抽象好了。

功能点

  • 实体对象insert,update,delete全部支持
  • 单表查询、多表join查询,in子查询,exists子查询,连表统计(select a,(select count(1) from b) from c),联合查询union | all,分组group | having
  • 分页
  • 动态表名:运行时修改表名
  • 原生sql执行,查询
  • select查询map结果返回
  • select支持直接返回DTO对象实现自定义列查询返回,而不是全部列返回
  • select支持标记large字段不返回(默认返回)
  • 逻辑删除,自定义逻辑删除,支持多字段逻辑删除填充,支持运行时禁用
  • 全局拦截器,支持运行时选择性使用某几个或者不使用,支持entity操作 insert,update,条件拦截 select、update、delete的where条件拦截,update set字段拦截器
  • 多租户,支持表的列范围多租户模式
  • 数据库列加密,支持高性能的like模糊搜索匹配(不是单纯的调用数据库加密函数或者单纯的调用框架加密解密函数)
  • 数据追踪差异更新,而不是全列更新,用过efcore的肯定很熟悉
  • 版本号、乐观锁,支持自定义乐观锁
  • 支持分库分表(身为sharding-core作者不支持说不过去),全自动分库分表,仅需用户新增表和告知easy-query系统中有的表
  • 高性能分库分表分页,支持顺序分页,反向分页,支持高性能顺序分页和反向分页
  • 分库分表多字段分片
  • 分库分表自定义分片路由规则
  • 支持读写分离,一主多从支持分片下读写分离

目前项目正处于起步阶段后续会随着用户不断地完善各数据库的适配和功能的支持

开始使用

安装

以下是spring-boot环境和控制台模式的安装

spring-boot

<properties>
    <easy-query.version>0.8.10</easy-query.version>
</properties>
<dependency>
    <groupId>com.easy-query</groupId>
    <artifactId>sql-springboot-starter</artifactId>
    <version>${easy-query.version}</version>
</dependency>

console

以mysql为例

<properties>
    <easy-query.version>0.8.10</easy-query.version>
</properties>
<dependency>
    <groupId>com.easy-query</groupId>
    <artifactId>sql-mysql</artifactId>
    <version>${easy-query.version}</version>
</dependency>
//初始化连接池
 HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/easy-query-test?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true");
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setMaximumPoolSize(20);
//创建easy-query
 EasyQuery easyQuery = EasyQueryBootstrapper.defaultBuilderConfiguration()
                .setDefaultDataSource(dataSource)
                .useDatabaseConfigure(new MySQLDatabaseConfiguration())
                .build();

开始

sql脚本

create table t_topic
(
    id varchar(32) not null comment '主键ID'primary key,
    stars int not null comment '点赞数',
    title varchar(50) null comment '标题',
    create_time datetime not null comment '创建时间'
)comment '主题表';

create table t_blog
(
    id varchar(32) not null comment '主键ID'primary key,
    deleted tinyint(1) default 0 not null comment '是否删除',
    create_by varchar(32) not null comment '创建人',
    create_time datetime not null comment '创建时间',
    update_by varchar(32) not null comment '更新人',
    update_time datetime not null comment '更新时间',
    title varchar(50) not null comment '标题',
    content varchar(256) null comment '内容',
    url varchar(128) null comment '博客链接',
    star int not null comment '点赞数',
    publish_time datetime null comment '发布时间',
    score decimal(18, 2) not null comment '评分',
    status int not null comment '状态',
    `order` decimal(18, 2) not null comment '排序',
    is_top tinyint(1) not null comment '是否置顶',
    top tinyint(1) not null comment '是否置顶'
)comment '博客表';

查询对象




@Data
public class BaseEntity implements Serializable {
    private static final long serialVersionUID = -4834048418175625051L;

    @Column(primaryKey = true)
    private String id;
    /**
     * 创建时间;创建时间
     */
    private LocalDateTime createTime;
    /**
     * 修改时间;修改时间
     */
    private LocalDateTime updateTime;
    /**
     * 创建人;创建人
     */
    private String createBy;
    /**
     * 修改人;修改人
     */
    private String updateBy;
    /**
     * 是否删除;是否删除
     */
    @LogicDelete(strategy = LogicDeleteStrategyEnum.BOOLEAN)
    private Boolean deleted;
}


@Data
@Table("t_topic")
@ToString
public class Topic {

    @Column(primaryKey = true)
    private String id;
    private Integer stars;
    private String title;
    private LocalDateTime createTime;
}

@Data
@Table("t_blog")
public class BlogEntity extends BaseEntity{

    /**
     * 标题
     */
    private String title;
    /**
     * 内容
     */
    private String content;
    /**
     * 博客链接
     */
    private String url;
    /**
     * 点赞数
     */
    private Integer star;
    /**
     * 发布时间
     */
    private LocalDateTime publishTime;
    /**
     * 评分
     */
    private BigDecimal score;
    /**
     * 状态
     */
    private Integer status;
    /**
     * 排序
     */
    private BigDecimal order;
    /**
     * 是否置顶
     */
    private Boolean isTop;
    /**
     * 是否置顶
     */
    private Boolean top;
}

单表查询

Topic topic = easyQuery
                .queryable(Topic.class)
                .where(o -> o.eq(Topic::getId, "3"))
                .firstOrNull();      
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Time Elapsed: 15(ms)
<== Total: 1     

多表查询

Topic topic = easyQuery
                .queryable(Topic.class)
                .leftJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
                .where(o -> o.eq(Topic::getId, "3"))
                .firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t LEFT JOIN `t_blog` t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t.`id` = ? LIMIT 1
==> Parameters: false(Boolean),3(String)
<== Time Elapsed: 2(ms)
<== Total: 1

复杂查询

join + group +分页


EasyPageResult<BlogEntity> page = easyQuery
        .queryable(Topic.class).asTracking()
        .innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
        .where((t, t1) -> t1.isNotNull(BlogEntity::getTitle))
        .groupBy((t, t1)->t1.column(BlogEntity::getId))
        .select(BlogEntity.class, (t, t1) -> t1.column(BlogEntity::getId).columnSum(BlogEntity::getScore))
        .toPageResult(1, 20);

==> Preparing: SELECT t1.`id`,SUM(t1.`score`) AS `score` FROM `t_topic` t INNER JOIN `t_blog` t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL GROUP BY t1.`id` LIMIT 20
==> Parameters: false(Boolean)
<== Time Elapsed: 5(ms)
<== Total: 20

动态表名


String sql = easyQuery.queryable(BlogEntity.class)
        .asTable(a->"aa_bb_cc")
        .where(o -> o.eq(BlogEntity::getId, "123"))
        .toSQL();
     
 SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM `aa_bb_cc` t WHERE t.`deleted` = ? AND t.`id` = ?  

新增


Topic topic = new Topic();
topic.setId(String.valueOf(0));
topic.setStars(100);
topic.setTitle("标题0");
topic.setCreateTime(LocalDateTime.now().plusDays(i));

long rows = easyQuery.insertable(topic).executeRows();

//返回结果rows1
==> Preparing: INSERT INTO `t_topic` (`id`,`stars`,`title`,`create_time`) VALUES (?,?,?,?) 
==> Parameters: 0(String),100(Integer),标题0(String),2023-03-16T21:34:13.287(LocalDateTime)
<== Total: 1

修改

//实体更新
 Topic topic = easyQuery.queryable(Topic.class)
        .where(o -> o.eq(Topic::getId, "7")).firstNotNull("未找到对应的数据");
        String newTitle = "test123" + new Random().nextInt(100);
        topic.setTitle(newTitle);

long rows=easyQuery.updatable(topic).executeRows();
==> Preparing: UPDATE t_topic SET `stars` = ?,`title` = ?,`create_time` = ? WHERE `id` = ?
==> Parameters: 107(Integer),test12364(String),2023-03-27T22:05:23(LocalDateTime),7(String)
<== Total: 1
//表达式更新
long rows = easyQuery.updatable(Topic.class)
                .set(Topic::getStars, 12)
                .where(o -> o.eq(Topic::getId, "2"))
                .executeRows();
//rows为1
easyQuery.updatable(Topic.class)
                    .set(Topic::getStars, 12)
                    .where(o -> o.eq(Topic::getId, "2"))
                    .executeRows(1,"更新失败");
//判断受影响行数并且进行报错,如果当前操作不在事务内执行那么会自动开启事务!!!会自动开启事务!!!会自动开启事务!!!来实现并发更新控制,异常为:EasyQueryConcurrentException 
//抛错后数据将不会被更新
==> Preparing: UPDATE t_topic SET `stars` = ? WHERE `id` = ?
==> Parameters: 12(Integer),2(String)
<== Total: 1

删除

long l = easyQuery.deletable(Topic.class)
                    .where(o->o.eq(Topic::getTitle,"title998"))
                    .executeRows();
==> Preparing: DELETE FROM t_topic WHERE `title` = ?
==> Parameters: title998(String)
<== Total: 1
Topic topic = easyQuery.queryable(Topic.class).whereId("997").firstNotNull("未找到当前主题数据");
long l = easyQuery.deletable(topic).executeRows();
==> Preparing: DELETE FROM t_topic WHERE `id` = ?
==> Parameters: 997(String)
<== Total: 1

联合查询

Queryable<Topic> q1 = easyQuery
                .queryable(Topic.class);
Queryable<Topic> q2 = easyQuery
        .queryable(Topic.class);
Queryable<Topic> q3 = easyQuery
        .queryable(Topic.class);
List<Topic> list = q1.union(q2, q3).where(o -> o.eq(Topic::getId, "123321")).toList();

==> Preparing: SELECT t1.`id`,t1.`stars`,t1.`title`,t1.`create_time` FROM (SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t UNION SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t UNION SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t) t1 WHERE t1.`id` = ?
==> Parameters: 123321(String)
<== Time Elapsed: 19(ms)
<== Total: 0

子查询

in子查询

Queryable<String> idQueryable = easyQuery.queryable(BlogEntity.class)
        .where(o -> o.eq(BlogEntity::getId, "1"))
        .select(String.class,o->o.column(BlogEntity::getId));
List<Topic> list = easyQuery
        .queryable(Topic.class, "x").where(o -> o.in(Topic::getId, idQueryable)).toList();
==> Preparing: SELECT x.`id`,x.`stars`,x.`title`,x.`create_time` FROM `t_topic` x WHERE x.`id` IN (SELECT t.`id` FROM `t_blog` t WHERE t.`deleted` = ? AND t.`id` = ?) 
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 3(ms)
<== Total: 1    

exists子查询

Queryable<BlogEntity> where1 = easyQuery.queryable(BlogEntity.class)
                .where(o -> o.eq(BlogEntity::getId, "1"));
List<Topic> x = easyQuery
        .queryable(Topic.class, "x").where(o -> o.exists(where1.where(q -> q.eq(o, BlogEntity::getId, Topic::getId)))).toList();
==> Preparing: SELECT x.`id`,x.`stars`,x.`title`,x.`create_time` FROM `t_topic` x WHERE EXISTS (SELECT 1 FROM `t_blog` t WHERE t.`deleted` = ? AND t.`id` = ? AND t.`id` = x.`id`) 
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 10(ms)
<== Total: 1

分片

easy-query支持分表、分库、分表+分库

分表

//创建分片对象
@Data
@Table(value = "t_topic_sharding_time",shardingInitializer = TopicShardingTimeShardingInitializer.class)
@ToString
public class TopicShardingTime {

    @Column(primaryKey = true)
    private String id;
    private Integer stars;
    private String title;
    @ShardingTableKey
    private LocalDateTime createTime;
}
//分片初始化器很简单 假设我们是2020年1月到2023年5月也就是当前时间进行分片那么要生成对应的分片表每月一张
public class TopicShardingTimeShardingInitializer extends AbstractShardingMonthInitializer<TopicShardingTime> {

    @Override
    protected LocalDateTime getBeginTime() {
        return LocalDateTime.of(2020, 1, 1, 1, 1);
    }

    @Override
    protected LocalDateTime getEndTime() {
        return LocalDateTime.of(2023, 5, 1, 0, 0);
    }


    @Override
    public void configure0(ShardingEntityBuilder<TopicShardingTime> builder) {

////以下条件可以选择配置也可以不配置用于优化分片性能
//        builder.paginationReverse(0.5,100)
//                .ascSequenceConfigure(new TableNameStringComparator())
//                .addPropertyDefaultUseDesc(TopicShardingTime::getCreateTime)
//                .defaultAffectedMethod(false, ExecuteMethodEnum.LIST,ExecuteMethodEnum.ANY,ExecuteMethodEnum.COUNT,ExecuteMethodEnum.FIRST)
//                .useMaxShardingQueryLimit(2,ExecuteMethodEnum.LIST,ExecuteMethodEnum.ANY,ExecuteMethodEnum.FIRST);

    }
}
//分片时间路由规则按月然后bean分片属性就是LocalDateTime也可以自定义实现
public class TopicShardingTimeTableRule extends AbstractMonthTableRule<TopicShardingTime> {

    @Override
    protected LocalDateTime convertLocalDateTime(Object shardingValue) {
        return (LocalDateTime)shardingValue;
    }
}

数据库脚本参考源码

其中shardingInitializer为分片初始化器用来初始化告诉框架有多少分片的表名(支持动态添加)

ShardingTableKey表示哪个字段作为分片键(分片键不等于主键)

执行sql

LocalDateTime beginTime = LocalDateTime.of(2021, 1, 1, 1, 1);
LocalDateTime endTime = LocalDateTime.of(2021, 5, 2, 1, 1);
Duration between = Duration.between(beginTime, endTime);
long days = between.toDays();
List<TopicShardingTime> list = easyQuery.queryable(TopicShardingTime.class)
        .where(o->o.rangeClosed(TopicShardingTime::getCreateTime,beginTime,endTime))
        .orderByAsc(o -> o.column(TopicShardingTime::getCreateTime))
        .toList();


==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202101` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> SHARDING_EXECUTOR_3, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202102` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> SHARDING_EXECUTOR_2, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
==> SHARDING_EXECUTOR_3, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
<== SHARDING_EXECUTOR_3, name:ds2020, Time Elapsed: 3(ms)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 3(ms)
==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202103` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> SHARDING_EXECUTOR_3, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202104` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> SHARDING_EXECUTOR_2, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
==> SHARDING_EXECUTOR_3, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
<== SHARDING_EXECUTOR_3, name:ds2020, Time Elapsed: 2(ms)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 2(ms)
==> main, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_time_202105` t WHERE t.`create_time` >= ? AND t.`create_time` <= ? ORDER BY t.`create_time` ASC
==> main, name:ds2020, Parameters: 2021-01-01T01:01(LocalDateTime),2021-05-02T01:01(LocalDateTime)
<== main, name:ds2020, Time Elapsed: 2(ms)
<== Total: 122

分库


@Data
@Table(value = "t_topic_sharding_ds",shardingInitializer = DataSourceAndTableShardingInitializer.class)
@ToString
public class TopicShardingDataSource {

    @Column(primaryKey = true)
    private String id;
    private Integer stars;
    private String title;
    @ShardingDataSourceKey
    private LocalDateTime createTime;
}
public class DataSourceShardingInitializer implements EntityShardingInitializer<TopicShardingDataSource> {
    @Override
    public void configure(ShardingEntityBuilder<TopicShardingDataSource> builder) {
        EntityMetadata entityMetadata = builder.getEntityMetadata();
        String tableName = entityMetadata.getTableName();
        List<String> tables = Collections.singletonList(tableName);
        LinkedHashMap<String, Collection<String>> initTables = new LinkedHashMap<String, Collection<String>>() {{
            put("ds2020", tables);
            put("ds2021", tables);
            put("ds2022", tables);
            put("ds2023", tables);
        }};
        builder.actualTableNameInit(initTables);


    }
}
//分库数据源路由规则
public class TopicShardingDataSourceRule extends AbstractDataSourceRouteRule<TopicShardingDataSource> {
    @Override
    protected RouteFunction<String> getRouteFilter(TableAvailable table, Object shardingValue, ShardingOperatorEnum shardingOperator, boolean withEntity) {
        LocalDateTime createTime = (LocalDateTime) shardingValue;
        String dataSource = "ds" + createTime.getYear();
        switch (shardingOperator){
            case GREATER_THAN:
            case GREATER_THAN_OR_EQUAL:
                return ds-> dataSource.compareToIgnoreCase(ds)<=0;
            case LESS_THAN:
            {
                //如果小于月初那么月初的表是不需要被查询的
                LocalDateTime timeYearFirstDay = LocalDateTime.of(createTime.getYear(),1,1,0,0,0);
                if(createTime.isEqual(timeYearFirstDay)){
                    return ds->dataSource.compareToIgnoreCase(ds)>0;
                }
                return ds->dataSource.compareToIgnoreCase(ds)>=0;
            }
            case LESS_THAN_OR_EQUAL:
                return ds->dataSource.compareToIgnoreCase(ds)>=0;

            case EQUAL:
                return ds->dataSource.compareToIgnoreCase(ds)==0;
            default:return t->true;
        }
    }
}

LocalDateTime beginTime = LocalDateTime.of(2020, 1, 1, 1, 1);
LocalDateTime endTime = LocalDateTime.of(2023, 5, 1, 1, 1);
Duration between = Duration.between(beginTime, endTime);
long days = between.toDays();
EasyPageResult<TopicShardingDataSource> pageResult = easyQuery.queryable(TopicShardingDataSource.class)
        .orderByAsc(o -> o.column(TopicShardingDataSource::getCreateTime))
        .toPageResult(1, 33);

==> SHARDING_EXECUTOR_23, name:ds2022, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_ds` t ORDER BY t.`create_time` ASC LIMIT 33
==> SHARDING_EXECUTOR_11, name:ds2021, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_ds` t ORDER BY t.`create_time` ASC LIMIT 33
==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_ds` t ORDER BY t.`create_time` ASC LIMIT 33
==> SHARDING_EXECUTOR_4, name:ds2023, Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic_sharding_ds` t ORDER BY t.`create_time` ASC LIMIT 33
<== SHARDING_EXECUTOR_4, name:ds2023, Time Elapsed: 4(ms)
<== SHARDING_EXECUTOR_23, name:ds2022, Time Elapsed: 4(ms)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 4(ms)
<== SHARDING_EXECUTOR_11, name:ds2021, Time Elapsed: 6(ms)
<== Total: 33

最后

希望看到这边的各位大佬给我点个star谢谢这对我很重要

javaer你还在手写分表分库?来看看这个框架怎么做的 干货满满 - 薛家明 - 博客园

mikel阅读(519)

来源: javaer你还在手写分表分库?来看看这个框架怎么做的 干货满满 – 薛家明 – 博客园

java orm框架easy-query分库分表之分表

高并发三驾马车:分库分表、MQ、缓存。今天给大家带来的就是分库分表的干货解决方案,哪怕你不用我的框架也可以从中听到不一样的结局方案和实现。

一款支持自动分表分库的orm框架easy-query 帮助您解脱跨库带来的复杂业务代码,并且提供多种结局方案和自定义路由来实现比中间件更高性能的数据库访问。

目前市面上有的分库分表JAVA组件有很多:中间件代理有:sharding-sphere(proxy),mycat 客户端JDBC:sharding-sphere(jdbc)等等,中间件因为代理了一层会导致所有的SQL执行都要经过中间件,性能会大大折扣,但是因为中间部署可以提供更加省的连接池,客户端无需代理,仅需对SQL进行分析即可实现,但是越靠近客户的模式可以优化的性能越高,所以本次带来的框架可以提供前所未有的分片规则自由和前所未有的便捷高性能。

本文 demo地址 https://github.com/xuejmnet/easy-sharding-test

怎么样的orm算是支持分表分库

首先orm是否支持分表分库不仅仅是看框架是否支持动态修改表名,让数据正确存入对应的表或者修改对应的数据,这些说实话都是最最简单的实现,真正需要支持分库分表那么需要orm实现复杂的跨表聚合查询,这才是分表分库的精髓,很显然目前的orm很少有支持的。接下来我将给大家演示基于springboot3.x的分表分库演示,取模分片和时间分片。本章我们主要以使用为主后面下一章我们来讲解优化方案,包括原理解析,后续有更多的关于分表分库的经验是博主多年下来的实战经验分享给大家保证大家的happy coding。

初始化项目

进入 https://start.spring.io/ 官网直接下载

安装依赖


		<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.2.15</version>
		</dependency>
		<!-- mysql驱动 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.17</version>
		</dependency>
		<dependency>
			<groupId>com.easy-query</groupId>
			<artifactId>sql-springboot-starter</artifactId>
			<version>0.9.7</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.18</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

application.yml配置

server:
  port: 8080

spring:

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/easy-sharding-test?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
    username: root
    password: root

logging:
  level:
    com.easy.query.core: debug

easy-query:
  enable: true
  name-conversion: underlined
  database: mysql

取模

常见的分片方式之一就是取模分片,取模分片可以让以分片键为条件的处理完美路由到对应的表,性能上来说非常非常高,但是局限性也是很大的因为无意义的id路由会导致仅支持这一个id条件而不支持其他条件的路由,只能全分片表扫描来获取对应的数据,但是他的实现和理解也是最容易的,当然后续还有基因分片一种可以部分解决仅支持id带来的问题不过也并不是非常的完美。

简单的取模分片

我们本次测试案例采用order表对其进行5表拆分:order_00,order_01,order_02,order_03,order_04,采用订单id取模进行分表
数据库脚本

CREATE DATABASE IF NOT EXISTS `easy-sharding-test` CHARACTER SET 'utf8mb4';
USE `easy-sharding-test`;
create table order_00
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
create table order_01
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
create table order_02
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
create table order_03
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
create table order_04
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int null comment '订单号'
)comment '订单表';
//定义了一个对象并且设置表名和分片初始化器`shardingInitializer`,设置id为主键,并且设置id为分表建
@Data
@Table(value = "order",shardingInitializer = OrderShardingInitializer.class)
public class OrderEntity {
    @Column(primaryKey = true)
    @ShardingTableKey
    private String id;
    private String uid;
    private Integer orderNo;
}
//编写订单取模初始化器,只需要实现两个方法,当然你也可以自己实现对应的`EntityShardingInitializer`这边是继承`easy-query`框架提供的分片取模初始化器
@Component
public class OrderShardingInitializer extends AbstractShardingModInitializer<OrderEntity> {
     /**
     * 设置模几我们模5就设置5
     * @return
     */
    @Override
    protected int mod() {
        return 5;
    }

    /**
     * 编写模5后的尾巴长度默认我们设置2就是左补0
     * @return
     */
    @Override
    protected int tailLength() {
        return 2;
    }
}
//编写分片规则`AbstractModTableRule`由框架提供取模分片路由规则,如果需要自己实现可以继承`AbstractTableRouteRule`这个抽象类
@Component
public class OrderTableRouteRule extends AbstractModTableRule<OrderEntity> {
    @Override
    protected int mod() {
        return 5;
    }

    @Override
    protected int tailLength() {
        return 2;
    }
}

初始化工作做好了开始编写代码

新增初始化


@RestController
@RequestMapping("/order")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class OrderController {

    private final EasyQuery easyQuery;

    @GetMapping("/init")
    public Object init() {
        ArrayList<OrderEntity> orderEntities = new ArrayList<>(100);
        List<String> users = Arrays.asList("xiaoming", "xiaohong", "xiaolan");

        for (int i = 0; i < 100; i++) {
            OrderEntity orderEntity = new OrderEntity();
            orderEntity.setId(String.valueOf(i));
            int i1 = i % 3;
            String uid = users.get(i1);
            orderEntity.setUid(uid);
            orderEntity.setOrderNo(i);
            orderEntities.add(orderEntity);
        }
        long l = easyQuery.insertable(orderEntities).executeRows();
        return "成功插入:"+l;
    }
}

查询单条

按分片键查询

可以完美的路由到对应的数据库表和操作单表拥有一样的性能

    @GetMapping("/first")
    public Object first(@RequestParam("id") String id) {
        OrderEntity orderEntity = easyQuery.queryable(OrderEntity.class)
                .whereById(id).firstOrNull();
        return orderEntity;
    }
http://localhost:8080/order/first?id=20
{"id":"20","uid":"xiaolan","orderNo":20}


http-nio-8080-exec-1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_03` t WHERE t.`id` = ? LIMIT 1
==> http-nio-8080-exec-1, name:ds0, Parameters: 20(String)
<== Total: 1

日志稍微解释一下

  • http-nio-8080-exec-1表示当前语句执行的线程,默认多个分片聚合后需要再线程池中查询数据后聚合返回。
  • name:ds0 表示数据源叫做ds0,如果不分库那么这个数据源可以忽略,也可以自己指定配置文件中或者设置defaultDataSourceName

全程无需您去计算路由到哪里,并且规则和业务代码已经脱离解耦

不按分片键查询

当我们的查询为非分片键查询那么会导致路由需要进行全分片扫描然后来获取对应的数据进行判断哪个时我们要的


    @GetMapping("/firstByUid")
    public Object firstByUid(@RequestParam("uid") String uid) {
        OrderEntity orderEntity = easyQuery.queryable(OrderEntity.class)
                .where(o->o.eq(OrderEntity::getUid,uid)).firstOrNull();
        return orderEntity;
    }

http://localhost:8080/order/firstByUid?uid=xiaoming
{"id":"18","uid":"xiaoming","orderNo":18}

//这边把日志精简了一下可以看到他是开启了5个线程进行分片查询
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_00` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_03` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_04` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_02` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_01` t WHERE t.`uid` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: xiaoming(String)
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: xiaoming(String)
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: xiaoming(String)
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: xiaoming(String)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: xiaoming(String)
<== Total: 1

因为uid不是分片键所以在分片查询的时候需要遍历所有的表然后返回对应的数据,可能有同学会问就这?当然这只是简单演示后续下一篇我会给出具体的优化方案来进行处理。

分页查询

分片后的分页查询是分片下的一个难点,这边框架自带功能,分片后分页之所以难是因为如果是自行实现业务代码会变得非常复杂,有一种非常简易的方式就是把分页重写pageIndex永远为1,然后全部取到内存后在进行stream过滤,但是带来的另一个问题就是pageIndex不能便宜过大不然内存会完全存不下导致内存爆炸,并且如果翻页到最后几页那将是灾难性的,给程序带来极其不稳定,但是easy-query提供了和sharding-sphere一样的分片聚合方式并且因为靠近业务的关系所以可以有效的优化深度分页pageIndex过大


    @GetMapping("/page")
    public Object page(@RequestParam("pageIndex") Integer pageIndex,@RequestParam("pageSize") Integer pageSize) {
        EasyPageResult<OrderEntity> pageResult = easyQuery.queryable(OrderEntity.class)
                .orderByAsc(o -> o.column(OrderEntity::getOrderNo))
                .toPageResult(pageIndex, pageSize);
        return pageResult;
    }


http://localhost:8080/order/page?pageIndex=1&pageSize=10

{"total":100,"data":[{"id":"0","uid":"xiaoming","orderNo":0},{"id":"1","uid":"xiaohong","orderNo":1},{"id":"2","uid":"xiaolan","orderNo":2},{"id":"3","uid":"xiaoming","orderNo":3},{"id":"4","uid":"xiaohong","orderNo":4},{"id":"5","uid":"xiaolan","orderNo":5},{"id":"6","uid":"xiaoming","orderNo":6},{"id":"7","uid":"xiaohong","orderNo":7},{"id":"8","uid":"xiaolan","orderNo":8},{"id":"9","uid":"xiaoming","orderNo":9}]}
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT COUNT(1) FROM `order_02` t
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT COUNT(1) FROM `order_03` t
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT COUNT(1) FROM `order_04` t
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT COUNT(1) FROM `order_01` t
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT COUNT(1) FROM `order_00` t
<== Total: 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_04` t ORDER BY t.`order_no` ASC LIMIT 10
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_03` t ORDER BY t.`order_no` ASC LIMIT 10
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_00` t ORDER BY t.`order_no` ASC LIMIT 10
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_01` t ORDER BY t.`order_no` ASC LIMIT 10
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_02` t ORDER BY t.`order_no` ASC LIMIT 10
<== Total: 10

这边可以看到一行代码实现分页,下面是第二页

http://localhost:8080/order/page?pageIndex=2&pageSize=10
{"total":100,"data":[{"id":"10","uid":"xiaohong","orderNo":10},{"id":"11","uid":"xiaolan","orderNo":11},{"id":"12","uid":"xiaoming","orderNo":12},{"id":"13","uid":"xiaohong","orderNo":13},{"id":"14","uid":"xiaolan","orderNo":14},{"id":"15","uid":"xiaoming","orderNo":15},{"id":"16","uid":"xiaohong","orderNo":16},{"id":"17","uid":"xiaolan","orderNo":17},{"id":"18","uid":"xiaoming","orderNo":18},{"id":"19","uid":"xiaohong","orderNo":19}]}

==> SHARDING_EXECUTOR_9, name:ds0, Preparing: SELECT COUNT(1) FROM `order_02` t
==> SHARDING_EXECUTOR_8, name:ds0, Preparing: SELECT COUNT(1) FROM `order_01` t
==> SHARDING_EXECUTOR_10, name:ds0, Preparing: SELECT COUNT(1) FROM `order_04` t
==> SHARDING_EXECUTOR_7, name:ds0, Preparing: SELECT COUNT(1) FROM `order_03` t
==> SHARDING_EXECUTOR_6, name:ds0, Preparing: SELECT COUNT(1) FROM `order_00` t
<== Total: 1
==> SHARDING_EXECUTOR_9, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_01` t ORDER BY t.`order_no` ASC LIMIT 20
==> SHARDING_EXECUTOR_8, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_03` t ORDER BY t.`order_no` ASC LIMIT 20
==> SHARDING_EXECUTOR_10, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_04` t ORDER BY t.`order_no` ASC LIMIT 20
==> SHARDING_EXECUTOR_6, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_02` t ORDER BY t.`order_no` ASC LIMIT 20
==> SHARDING_EXECUTOR_7, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no` FROM `order_00` t ORDER BY t.`order_no` ASC LIMIT 20
<== Total: 10

按时间分表

这边我们简单还是以order订单为例,按月进行分片假设我们从2022年1月到2023年5月一共17个月表名为t_order_202201t_order_202202t_order_202203t_order_202304t_order_202305

数据库脚本

create table t_order_202201
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int not null comment '订单号',
    create_time datetime not null comment '创建时间'
)comment '订单表';
create table t_order_202202
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int not null comment '订单号',
    create_time datetime not null comment '创建时间'
)comment '订单表';
....
create table t_order_202304
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int not null comment '订单号',
    create_time datetime not null comment '创建时间'
)comment '订单表';
create table t_order_202305
(
    id varchar(32) not null comment '主键ID'primary key,
    uid varchar(50) not null comment '用户id',
    order_no int not null comment '订单号',
    create_time datetime not null comment '创建时间'
)comment '订单表';

@Data
@Table(value = "t_order",shardingInitializer = OrderByMonthShardingInitializer.class)
public class OrderByMonthEntity {

    @Column(primaryKey = true)
    private String id;
    private String uid;
    private Integer orderNo;
    /**
     * 分片键改为时间
     */
    @ShardingTableKey
    private LocalDateTime createTime;
}

//路由规则可以直接继承AbstractShardingMonthInitializer也可以自己实现
@Component
public class OrderByMonthShardingInitializer extends AbstractShardingMonthInitializer<OrderByMonthEntity> {
   /**
     * 开始时间不可以使用LocalDateTime.now()因为会导致每次启动开始时间都不一样
     * @return
     */
    @Override
    protected LocalDateTime getBeginTime() {
        return LocalDateTime.of(2022,1,1,0,0);
    }

    /**
     * 如果不设置那么就是当前时间,用于程序启动后自动计算应该有的表包括最后时间
     * @return
     */
    @Override
    protected LocalDateTime getEndTime() {
        return LocalDateTime.of(2023,5,31,0,0);
    }

    @Override
    public void configure0(ShardingEntityBuilder<OrderByMonthEntity> builder) {
        //后续用来实现优化分表
    }
}
//按月分片路由规则也可以自己实现因为框架已经封装好了所以可以用框架自带的
@Component
public class OrderByMonthTableRouteRule extends AbstractMonthTableRule<OrderByMonthEntity> {
    @Override
    protected LocalDateTime convertLocalDateTime(Object shardingValue) {
        return (LocalDateTime)shardingValue;
    }
}

初始化


@RestController
@RequestMapping("/orderMonth")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class OrderMonthController {

    private final EasyQuery easyQuery;

    @GetMapping("/init")
    public Object init() {
        ArrayList<OrderByMonthEntity> orderEntities = new ArrayList<>(100);
        List<String> users = Arrays.asList("xiaoming", "xiaohong", "xiaolan");
        LocalDateTime beginTime=LocalDateTime.of(2022,1,1,0,0);
        LocalDateTime endTime=LocalDateTime.of(2023,5,31,0,0);
        int i=0;
        while(!beginTime.isAfter(endTime)){

            OrderByMonthEntity orderEntity = new OrderByMonthEntity();
            orderEntity.setId(String.valueOf(i));
            int i1 = i % 3;
            String uid = users.get(i1);
            orderEntity.setUid(uid);
            orderEntity.setOrderNo(i);
            orderEntity.setCreateTime(beginTime);
            orderEntities.add(orderEntity);
            beginTime=beginTime.plusDays(1);
            i++;
        }
        long l = easyQuery.insertable(orderEntities).executeRows();
        return "成功插入:"+l;
    }
}

http://localhost:8080/orderMonth/init
成功插入:516

获取第一条数据

    @GetMapping("/first")
    public Object first(@RequestParam("id") String id) {
        OrderEntity orderEntity = easyQuery.queryable(OrderEntity.class)
                .whereById(id).firstOrNull();
        return orderEntity;
    }

http://localhost:8080/orderMonth/first?id=11
{"id":"11","uid":"xiaolan","orderNo":11,"createTime":"2022-01-12T00:00:00"}
//以每5组一个次并发执行聚合

==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202205` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202207` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202303` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202212` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202302` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202304` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202206` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202305` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202209` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202204` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202208` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202201` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202210` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202202` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202211` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202203` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202301` t WHERE t.`id` = ? LIMIT 1
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 11(String)
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 11(String)
<== Total: 1

获取范围内的数据

    @GetMapping("/range")
    public Object first() {
        List<OrderByMonthEntity> list = easyQuery.queryable(OrderByMonthEntity.class)
                .where(o -> o.rangeClosed(OrderByMonthEntity::getCreateTime, LocalDateTime.of(2022, 3, 1, 0, 0), LocalDateTime.of(2022, 9, 1, 0, 0)))
                .toList();
        return list;
    }
http://localhost:8080/orderMonth/range
[{"id":"181","uid":"xiaohong","orderNo":181,"createTime":"2022-07-01T00:00:00"},{"id":"182","uid":"xiaolan","orderNo":182,"createTime":"2022-07-02T00:00:00"},{"id":"183","uid":"xiaoming","orderNo":183,"createTime":"2022-07-03T00:00:00"},...........,{"id":"239","uid":"xiaolan","orderNo":239,"createTime":"2022-08-28T00:00:00"},{"id":"240","uid":"xiaoming","orderNo":240,"createTime":"2022-08-29T00:00:00"},{"id":"241","uid":"xiaohong","orderNo":241,"createTime":"2022-08-30T00:00:00"},{"id":"242","uid":"xiaolan","orderNo":242,"createTime":"2022-08-31T00:00:00"}]

//可以精准定位到对应的分片路由上获取数据
==> SHARDING_EXECUTOR_1, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202207` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_5, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202209` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202206` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202203` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_3, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202205` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_3, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_5, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_1, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_4, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202208` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_2, name:ds0, Preparing: SELECT t.`id`,t.`uid`,t.`order_no`,t.`create_time` FROM `t_order_202204` t WHERE t.`create_time` >= ? AND t.`create_time` <= ?
==> SHARDING_EXECUTOR_4, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
==> SHARDING_EXECUTOR_2, name:ds0, Parameters: 2022-03-01T00:00(LocalDateTime),2022-09-01T00:00(LocalDateTime)
<== Total: 185

最后

目前为止你已经看到了easy-query对于分片的便捷性,但是本章只是开胃小菜,相信了解分库分表的小伙伴肯定会说就这?不是和sharding-jdbc一样吗为什么要用你的呢。我想说第一篇只是给大家了解一下如何使用,后续的文章才是分表分库的精髓相信我你一定没看过

demo地址 https://github.com/xuejmnet/easy-sharding-test

你没见过的分库分表原理解析和解决方案(一) - 薛家明 - 博客园

mikel阅读(480)

来源: 你没见过的分库分表原理解析和解决方案(一) – 薛家明 – 博客园

你没见过的分库分表原理解析和解决方案(一)

高并发三驾马车:分库分表、MQ、缓存。今天给大家带来的就是分库分表的干货解决方案,哪怕你不用我的框架也可以从中听到不一样的结局方案和实现。

一款支持自动分表分库的orm框架easy-query 帮助您解脱跨库带来的复杂业务代码,并且提供多种结局方案和自定义路由来实现比中间件更高性能的数据库访问。

上篇文章简单的带大家了解了框架如何使用分片本章将会以理论为主加实践的方式呈现不一样的分表分库。

介绍

分库分表一直是老生常谈的问题,市面上也有很多人侃侃而谈,但是大部分的说辞都是一样,甚至给不出一个实际的解决方案,本人经过多年的深耕在其他语言里面多年的维护和实践下来秉着happy coding的原则希望更多的人可以了解和认识到该框架并且给大家一个全新的针对分库分表的认识。
我们也经常戏称项目一开始就用了分库分表结果上线没多少数据,并且整个开发体验来说非常繁琐,对于业务而言也是极其不友好,大大拉长开发周期不说,bug也是更加容易产生,针对上述问题该框架给出了一个非常完美的实现来极大程度上的给用户完美的体验

分片存储

分库分表简单的实现目前大部分框架已经都可以实现了,就是动态表名来实现分表下的简单存储,如果是分库下面的那么就使用动态数据源来切换实现,如果是分库加分表就用动态数据源加动态表名来实现,听上去是不是很完美,但是实际情况下你需要表写非常繁多的业务代码,并且会让整个开发精力全部集中在分库分表下,针对后期的维护也是非常麻烦的一件事。
但是分库分表的分片规则又是和具体业务耦合的所以合理的解耦分片路由是一件非常重要的事情。

插入

假设我们按订单id进行分表存储

通过上述图片我们可以很清晰的了解到分片插入的执行原理,通过拦截执行SQL分析对应的值计算出所属表名,然后改写表名进行插入。该实现方法有一个弊端就是如果插入数据是increment的自增类型,那么这种方法将不适合,因为自增主键只有在插入数据库后才会正真的被确定是什么值,可以通过拦截器设置自定义自增拨号器来实现伪自增,这样也可以实现“自增”列。

更新删除)

这边假设我们也是按照订单id进行分表更新

更新分片键


一模一样的处理,将SQL进行拦截后解析where和分片字段id然后计算后将结果发送到对应路由的表中进行执行。

那么如果我们没办法进行路由确定呢,如果我们使用created字段来更新的那么会发生生呢

更新非分片键


为了得到正确的结果需要将每条SQL进行改写分别发送到对应的表中,然后将各自表的执行结果进行聚合返回最终受影响行数

分片查询

众所周知分库分表的难点并不在如何存储数据到对应的db,也不在于如何更新指定实体数据,因为他们都可以通过分片键的计算来重新路由,可以让分片的操作降为单表操作,所以orm只需要支持动态表名那么以上所有功能都是支持的,
但是实际情况缺是如果orm或者中间件只支持到了这个级别那么对于稍微复杂一点的业务你必须要编写大量的业务代码来实现业务需要的查询,并且会浪费大量的重复工作和精力

单分片表查询

加下来我来讲解单分片表查询,其实原理和上面的insert一样

到这里为止其实都是ok的并没有什么问题.但是如果我们的本次查询需要跨分片呢比如跨两个分片那么应该如何处理

跨分片表查询

到这一步我们已经将对应的数据路由到对应的数据库了,那么我们应该如何获取自己想要的结果呢

通过上图我们可以了解到在跨分片的聚合下我们可以分表通过对a,b两张表进行查询可以并行可以串行,最终将结果汇聚到同一个集合那么返回给用户端就是一个完整的数据包,并没有缺少任何数据

跨分片排序

基于上述分片聚合方式我们清晰的了解到如何可以进行跨分片下降数据获取到内存中,但是通过图中结果可以清晰的了解到返回的数据并不像我们预期的那样有序,那是因为各个节点下的所有数据都是仅遵循各自节点的数据库排序而不受其他节点分片影响。
那么如果我们对数据进行分片聚合+排序那么又会是什么样的场景呢

方案一内存排序

首先我们将执行sql分别路由到t_order_1t_order_2两张表,并且执行order by id desc将其数据id大的排在前面这样可以保证单个ConnectionResultSet肯定是大的先被返回
所以在单个Connection下结果是正确的但是因为多个分片节点间没有交互所以当取到内存中后数据依然是乱的,所以这边需要对sql进行拦截获取排序字段并且将其在内存中的集合里面实现,这样我们就做到了和排序字段一样的返回结果

方案二流式排序

大部分orm到这边就为止了,毕竟已经实现了完美的节点处理,但是我们来看他需要消耗的性能事多少,假设我们分片命中2个节点,每个节点各自返回2条数据,我们对于整个ResultSet的遍历将是每个链接都是2那么就是4次,然后在内存中在进行排序如果性能差一点还需要多次所以这个是相对比较浪费性能的,因为如果我们有1000条数据返回那么内存中的排序是很高效的但是这个也是我们这次需要讲解的更加高效的排序处理流式排序

相较于内存排序这种方式十分复杂并且繁琐,而且对于用户也很不好理解,但是如果你获取的数据是分页,那么内存排序进行获取结果将会变得非常危险,有可能导致内存数据过大从而导致程序崩溃

无order字段

到这边不要以为跨分片聚合已经结束了因为当你的sql查询order by了一个select不存在的字段,那么上述两种排序方式都将无法使用,因为程序获取到的结果集并没有排序字段,这个时候一般我们会改写sql让其select的时候必须要带上对应的order by字段这样就可以保证我们数据的正确返回

以下两个问题因为涉及到过多内容本章节无法呈现所以将会在下一章给出具体解决方案

跨分片分组

如果我们程序遇到了这个那么我们该如何处理呢

跨分片分页

业务中常常需要的跨分片分页我们该如何解决,easy-query又如何处理这种情况,如果跨的分片过多我们又该怎么办,

  • 如何解决深分页问题
  • 如何解决流式瀑布问题
  • 如何进行分页缓存高效获取问题

接下来将在下篇文章中一一解答近

最后

我这边将演示easy-query在本次分片理论中的实际应用
这次采用h2数据库作为演示

CREATE TABLE IF NOT EXISTS `t_order_0`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );
CREATE TABLE IF NOT EXISTS `t_order_1`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );
CREATE TABLE IF NOT EXISTS `t_order_2`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );
CREATE TABLE IF NOT EXISTS `t_order_3`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );
CREATE TABLE IF NOT EXISTS `t_order_4`
(
    `id`  INTEGER PRIMARY KEY,
    `status`       Integer,
    `created` VARCHAR(100)
    );

安装maven依赖


        <dependency>
            <groupId>com.easy-query</groupId>
            <artifactId>sql-h2</artifactId>
            <version>0.9.32</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.easy-query</groupId>
            <artifactId>sql-api4j</artifactId>
            <version>0.9.32</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>

创建实体对象对应数据库


@Data
@Table(value = "t_order",shardingInitializer = H2OrderShardingInitializer.class)
public class H2Order {
    @Column(primaryKey = true)
    @ShardingTableKey
    private Integer id;
    private Integer status;
    private String created;
}
// 分片初始化器

public class H2OrderShardingInitializer extends AbstractShardingTableModInitializer<H2Order> {
    @Override
    protected int mod() {
        return 5;//模5
    }

    @Override
    protected int tailLength() {
        return 1;//表后缀长度1位
    }
}
//分片路由规则

public class H2OrderRule extends AbstractModTableRule<H2Order> {
    @Override
    protected int mod() {
        return 5;
    }

    @Override
    protected int tailLength() {
        return 1;
    }
}

创建datasource和easyquery

   orderShardingDataSource=DataSourceFactory.getDataSource("dsorder","h2-dsorder.sql");
   EasyQueryClient easyQueryClientOrder = EasyQueryBootstrapper.defaultBuilderConfiguration()
                .setDefaultDataSource(orderShardingDataSource)
                .optionConfigure(op -> {
                    op.setMaxShardingQueryLimit(10);
                    op.setDefaultDataSourceName("ds2020");
                    op.setDefaultDataSourceMergePoolSize(20);
                })
                .build();
      EasyQuery   easyQueryOrder = new DefaultEasyQuery(easyQueryClientOrder);

        QueryRuntimeContext runtimeContext = easyQueryOrder.getRuntimeContext();
        QueryConfiguration queryConfiguration = runtimeContext.getQueryConfiguration();
        queryConfiguration.applyShardingInitializer(new H2OrderShardingInitializer());//添加分片初始化器
        TableRouteManager tableRouteManager = runtimeContext.getTableRouteManager();
        tableRouteManager.addRouteRule(new H2OrderRule());//添加分片路由规则

插入代码


  ArrayList<H2Order> h2Orders = new ArrayList<>();
  for (int i = 0; i < 100; i++) {
      H2Order h2Order = new H2Order();
      h2Order.setId(i);
      h2Order.setStatus(i%3);
      h2Order.setCreated(String.valueOf(i));
      h2Orders.add(h2Order);
  }
  easyQueryOrder.insertable(h2Orders).executeRows();
==> main, name:ds2020, Preparing: INSERT INTO t_order_3 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 0(Integer),0(Integer),0(String)
<== main, name:ds2020, Total: 1
==> main, name:ds2020, Preparing: INSERT INTO t_order_4 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 1(Integer),1(Integer),1(String)
<== main, name:ds2020, Total: 1
==> main, name:ds2020, Preparing: INSERT INTO t_order_0 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 2(Integer),2(Integer),2(String)
<== main, name:ds2020, Total: 1
==> main, name:ds2020, Preparing: INSERT INTO t_order_1 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 3(Integer),0(Integer),3(String)
<== main, name:ds2020, Total: 1
==> main, name:ds2020, Preparing: INSERT INTO t_order_2 (id,status,created) VALUES (?,?,?)
==> main, name:ds2020, Parameters: 4(Integer),1(Integer),4(String)
.....省略
       List<H2Order> list = easyQueryOrder.queryable(H2Order.class)
                .where(o -> o.in(H2Order::getId, Arrays.asList(1, 2, 6, 7)))
                .toList();
        Assert.assertEquals(4,list.size());
==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT id,status,created FROM t_order_3 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_4, name:ds2020, Preparing: SELECT id,status,created FROM t_order_0 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_3, name:ds2020, Preparing: SELECT id,status,created FROM t_order_4 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_1, name:ds2020, Preparing: SELECT id,status,created FROM t_order_2 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_4, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_5, name:ds2020, Preparing: SELECT id,status,created FROM t_order_1 WHERE id IN (?,?,?,?)
==> SHARDING_EXECUTOR_3, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_5, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_1, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_2, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_5, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_1, name:ds2020, Time Elapsed: 1(ms)
<== SHARDING_EXECUTOR_4, name:ds2020, Time Elapsed: 1(ms)
<== SHARDING_EXECUTOR_3, name:ds2020, Time Elapsed: 1(ms)
<== Total: 4
``
通过上述sql展示我们可以清晰的看到哪个线程执行了哪个数据源(分片下会不一样),执行了什么sql,最终执行消耗多少时间参数是多少,一共返回多少条数据
分片排序
```java
  List<H2Order> list = easyQueryOrder.queryable(H2Order.class)
                .where(o -> o.in(H2Order::getId, Arrays.asList(1, 2, 6, 7)))
                .orderByDesc(o->o.column(H2Order::getId))
                .toList();
  Assert.assertEquals(4,list.size());
  Assert.assertEquals(7,(int)list.get(0).getId());
  Assert.assertEquals(6,(int)list.get(1).getId());
  Assert.assertEquals(2,(int)list.get(2).getId());
  Assert.assertEquals(1,(int)list.get(3).getId());
==> SHARDING_EXECUTOR_1, name:ds2020, Preparing: SELECT id,status,created FROM t_order_1 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_5, name:ds2020, Preparing: SELECT id,status,created FROM t_order_3 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_4, name:ds2020, Preparing: SELECT id,status,created FROM t_order_2 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_3, name:ds2020, Preparing: SELECT id,status,created FROM t_order_4 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_5, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_1, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_4, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_2, name:ds2020, Preparing: SELECT id,status,created FROM t_order_0 WHERE id IN (?,?,?,?) ORDER BY id DESC
==> SHARDING_EXECUTOR_3, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
==> SHARDING_EXECUTOR_2, name:ds2020, Parameters: 1(Integer),2(Integer),6(Integer),7(Integer)
<== SHARDING_EXECUTOR_1, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_5, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_4, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_2, name:ds2020, Time Elapsed: 0(ms)
<== SHARDING_EXECUTOR_3, name:ds2020, Time Elapsed: 0(ms)
<== Total: 4

最后的最后

附上源码地址,源码中有文档和对应的qq群,如果决定有用请点击star谢谢大家了

你没见过的分库分表原理解析和解决方案(二) - 薛家明 - 博客园

mikel阅读(714)

来源: 你没见过的分库分表原理解析和解决方案(二) – 薛家明 – 博客园

你没见过的分库分表原理解析和解决方案(二)

高并发三驾马车:分库分表、MQ、缓存。今天给大家带来的就是分库分表的干货解决方案,哪怕你不用我的框架也可以从中听到不一样的结局方案和实现。

一款支持自动分表分库的orm框架easy-query 帮助您解脱跨库带来的复杂业务代码,并且提供多种结局方案和自定义路由来实现比中间件更高性能的数据库访问。

上篇文章简单的带大家了解了分表分库的原理和聚合解析,但是还留了两个坑一个是分组如何实现一个是分页如何实现

介绍

分库分表的难题一直不是如何插入一直都是如何实现聚合查询,让用户无感知的使用才是分库分表的最终形态,所以数据坐落和数据聚合将是分库分表的重中之重,随着版本迭代easy-query正式发布了1.0.0版本相对的api基本已经稳定,分库分表和之前稍微有点不一样但是大部分都是一样的,那么这次我们将使用1.0.6来实现分库分表下的数据分组和分页。

数据准备

本次我们以订单为例,然后以订单创建时间进行按年分库,按月分表,最后来实现上述的分组和分页的功能

默认配置项

数据源名称 对应数据库 对应的订单年份 对应的订单表
ds0 sharding-order 2020年 t_order_202001,t_order_202002…..,t_order_202011,t_order_202012
ds1 sharding-order1 2021年 t_order_202101,t_order_202102…..,t_order_202111,t_order_202112
ds2 sharding-order2 2022年 t_order_202201,t_order_202202…..,t_order_202211,t_order_202212
ds3 sharding-order3 2023年 t_order_202301,t_order_202302…..,t_order_202311,t_order_202312

添加依赖

        <!--druid依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.15</version>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
        <dependency>
            <groupId>com.easy-query</groupId>
            <artifactId>sql-processor</artifactId>
            <version>1.1.7</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.easy-query</groupId>
            <artifactId>sql-springboot-starter</artifactId>
            <version>1.1.7</version>
            <scope>compile</scope>
        </dependency>

添加配置文件

server:
  port: 8081

spring:
  profiles:
    active: dev

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/sharding-order?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
    username: root
    password: root
    druid:
      initial-size: 10
      max-active: 100


easy-query:
  enable: true
  name-conversion: underlined
  database: mysql
  default-data-source-merge-pool-size: 60
  default-data-source-name: ds0

新建一个订单QOrderEntity按季度进行分表分库

//分片表
@Data
@Table(value = "t_order", shardingInitializer = OrderInitializer.class)
@EntityProxy
public class OrderEntity {
    @Column(primaryKey = true)
    private String id;
    private Integer orderNo;
    private String userId;
    @ShardingTableKey
    @ShardingDataSourceKey
    private LocalDateTime createTime;
}


//分片初始化器
@Component
public class OrderInitializer extends AbstractShardingMonthInitializer<OrderEntity> {
    /**
     * 分片起始时间
     * @return
     */
    @Override
    protected LocalDateTime getBeginTime() {
        return LocalDateTime.of(2020,1,1,0,0,0);
    }

    /**
     * 格式化时间到数据源
     * @param time
     * @param defaultDataSource
     * @return
     */
    @Override
    protected String formatDataSource(LocalDateTime time, String defaultDataSource) {
        String year = DateTimeFormatter.ofPattern("yyyy").format(time);
        int i = Integer.parseInt(year)-2020;
        
        return "ds"+i;
    }
    @Override
    public void configure0(ShardingEntityBuilder<OrderEntity> builder) {

    }
}

//动态添加spring 启动后的动态数据源额外的ds1、ds2、ds3

@Component
public class ShardingInitRunner implements ApplicationRunner {
    @Autowired
    private EasyQuery easyQuery;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Map<String, DataSource> dataSources = createDataSources();
        DataSourceManager dataSourceManager = easyQuery.getRuntimeContext().getDataSourceManager();
        for (Map.Entry<String, DataSource> stringDataSourceEntry : dataSources.entrySet()) {

            dataSourceManager.addDataSource(stringDataSourceEntry.getKey(), stringDataSourceEntry.getValue(), 60);
        }
        System.out.println("初始化完成");
    }

    private Map<String, DataSource> createDataSources() {
        HashMap<String, DataSource> stringDataSourceHashMap = new HashMap<>();
        for (int i = 1; i < 4; i++) {
            DataSource dataSource = createDataSource("ds" + i, "jdbc:mysql://127.0.0.1:3306/sharding-order" + i + "?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true", "root", "root");
            stringDataSourceHashMap.put("ds" + i, dataSource);
        }
        return stringDataSourceHashMap;
    }

    private DataSource createDataSource(String dsName, String url, String username, String password) {

        // 设置properties
        Properties properties = new Properties();
        properties.setProperty("name", dsName);
        properties.setProperty("driverClassName", "com.mysql.cj.jdbc.Driver");
        properties.setProperty("url", url);
        properties.setProperty("username", username);
        properties.setProperty("password", password);
        properties.setProperty("initialSize", "10");
        properties.setProperty("maxActive", "100");
        try {
            return DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            throw new EasyQueryException(e);
        }
    }
}

//新建分库路由

@Component
public class OrderDataSourceRoute extends AbstractDataSourceRoute<OrderEntity> {
    protected Integer formatShardingValue(LocalDateTime time) {
        String year = time.format(DateTimeFormatter.ofPattern("yyyy"));
        return Integer.parseInt(year);
    }
    public boolean lessThanTimeStart(LocalDateTime shardingValue) {
        LocalDateTime timeYearFirstDay = EasyUtil.getYearStart(shardingValue);
        return shardingValue.isEqual(timeYearFirstDay);
    }

    protected Comparator<String> getDataSourceComparator(){
        return IgnoreCaseStringComparator.DEFAULT;
    }
    @Override
    protected RouteFunction<String> getRouteFilter(TableAvailable table, Object shardingValue, ShardingOperatorEnum shardingOperator, boolean withEntity) {
        //将分片键转成对应的类型
        LocalDateTime shardingTime = (LocalDateTime)shardingValue ;
        Integer intYear = formatShardingValue(shardingTime);
        String dataSourceName="ds"+String.valueOf((intYear-2020));//ds0 ds1 ds2 ds3....
        switch (shardingOperator) {
            case GREATER_THAN:
            case GREATER_THAN_OR_EQUAL:
                return ds -> getDataSourceComparator().compare(dataSourceName, ds) <= 0;
            case LESS_THAN: {
                //如果小于月初那么月初的表是不需要被查询的 如果小于年初也不需要查询
                if (lessThanTimeStart(shardingTime)) {
                    return ds -> getDataSourceComparator().compare(dataSourceName, ds) > 0;
                }
                return ds -> getDataSourceComparator().compare(dataSourceName, ds) >= 0;
            }
            case LESS_THAN_OR_EQUAL:
                return ds -> getDataSourceComparator().compare(dataSourceName, ds) >= 0;

            case EQUAL:
                return ds -> getDataSourceComparator().compare(dataSourceName,ds) == 0;
            default:
                return ds -> true;
        }
    }
}

//新建分表路由
//分表路由由系统提供默认按月分片
@Component
public class OrderTableRoute extends AbstractMonthTableRoute<OrderEntity> {

    @Override
    protected LocalDateTime convertLocalDateTime(Object shardingValue) {
        return (LocalDateTime)shardingValue;
    }
}
```
通过sql脚本我们创建好对应的数据库表结构
![](https://img2023.cnblogs.com/blog/1346660/202306/1346660-20230626215913917-1277133380.png)

初始化项目代码
``java

    private final EasyProxyQuery easyProxyQuery;
    @GetMapping("/init")
    public Object init() {

        long start = System.currentTimeMillis();
        LocalDateTime beginTime = LocalDateTime.of(2020, 1, 1, 0, 0, 0);
        LocalDateTime now = LocalDateTime.now();
        ArrayList<OrderEntity> orderEntities = new ArrayList<>();
        List<String> userIds = Arrays.asList("小明", "小红", "小蓝", "小黄", "小绿");
        int i=0;
        do {
            OrderEntity orderEntity = new OrderEntity();
            String timeFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(beginTime);
            orderEntity.setId(timeFormat);
            orderEntity.setOrderNo(i);
            orderEntity.setUserId(userIds.get(i%5));
            orderEntity.setCreateTime(beginTime);
            orderEntities.add(orderEntity);
            i++;
            beginTime=beginTime.plusMinutes(1);
        } while (beginTime.isBefore(now));

        long end = System.currentTimeMillis();

        long insertStart = System.currentTimeMillis();
        long rows = easyProxyQuery.insertable(orderEntities).executeRows();
        long insertEnd = System.currentTimeMillis();

        return "成功插入:" + rows+",其中路由对象生成耗时:"+(end-start)+"(ms),插入耗时:"+(insertEnd-insertStart)+"(ms)";
    }
```
![](https://img2023.cnblogs.com/blog/1346660/202306/1346660-20230626223148693-872730237.png)
数据初始化成功,接下来演示如何进行分组
# 分组聚合

分表分库下我们应该如何分组聚合

代码很简单就是查询userId in ["小明", "小绿"]的然后对userId分组求对应的订单号求和
````java

    @GetMapping("/groupByWithSumOrderNo")
    public Object groupByWithSumOrderNo() {
        long start = System.currentTimeMillis();
        List<String> userIds = Arrays.asList("小明", "小绿");
        List<OrderGroupWithSumOrderNoVO> list = easyProxyQuery.queryable(OrderEntityProxy.DEFAULT)
                .where((filter, t) -> filter.in(t.userId(), userIds))
                .groupBy((group, t) -> group.column(t.userId()))
                .select(OrderGroupWithSumOrderNoVOProxy.DEFAULT, (selector, t) -> selector.columnAs(t.userId(), r -> r.userId()).columnSumAs(t.orderNo(), r -> r.orderNoSum()))
                .toList();
        long end = System.currentTimeMillis();
        return Arrays.asList(list,(end-start)+"(ms)");
    }

[[{"orderNoSum":993365517,"userId":"小明"},{"orderNoSum":992998911,"userId":"小绿"}],"1768(ms)"]共计耗时约1.8秒

    @GetMapping("/groupByWithSumOrderNoOrderByUserId")
    public Object groupByWithSumOrderNoOrderByUserId() {
        long start = System.currentTimeMillis();
        List<String> userIds = Arrays.asList("小明", "小绿");
        List<OrderGroupWithSumOrderNoVO> list = easyProxyQuery.queryable(OrderEntityProxy.DEFAULT)
                .where((filter, t) -> filter.in(t.userId(), userIds))
                .groupBy((group, t) -> group.column(t.userId()))
                .orderByAsc((order,t)->order.column(t.userId()))
                .select(OrderGroupWithSumOrderNoVOProxy.DEFAULT, (selector, t) -> selector.columnAs(t.userId(), r -> r.userId()).columnSumAs(t.orderNo(), r -> r.orderNoSum()))
                .toList();
        long end = System.currentTimeMillis();
        return Arrays.asList(list,(end-start)+"(ms)");
    }

[[{"orderNoSum":993365517,"userId":"小明"},{"orderNoSum":992998911,"userId":"小绿"}],"1699(ms)"]
我们非常快速的获取了查询结果,那么这个结果是如何获取的呢接下来我将讲解分组聚合的原理并且会讲解order by对group在分片中的影响是有多大的影响

分组求和

原理解析这边以两个分片来进行聚合

SQL进行路由解析后分别对两个分片节点进行查询聚合,然后合并到内存中分别对groupsum进行处理实现无感知分片聚合group,但是大家可能已经发现了这个是sum那么各个节点的数据可以相加如果是avg呢应该怎么办,接下来我来讲解group下如何进行avg的数据聚合查询。

分组取平均数

首先我们来假设一个表a和表b两个表里面数据如下


结果错误!!!
可以看到单纯的通过内存来进行平均值的聚合是不正确的,因为只有当各个分片内的数据和分片数一样才可以简单的avg,那么我们应该如何实现分组求平均呢。

1.avg本质等于什么?

我们都知道avg=sum/count通过这个公式avg是不可以简单分片聚合的那么如果我们知道sumcount呢,是不是就可以知道avg了,在退一万步我们已经知道avg的情况下是不是只需要知道sum或者count也可以算出对应的第三个值

2.如何实现

  • 如果用户存在group+avg那么强制要求进行对应avg字段也进行sum或者count的其中一个
  • 如果发现用户存在group+avg那么就自动重写添加sum或者count的查询

那么之前的group+avg就会变成group+avg+sum

通过上述描述我们应该可以清晰的看到应该如何针对各个节点的分组求平均值来处理正确的方法

内存分组聚合

上述所有例子我们都是通过内存分组聚合来实现各个节点的分组聚合,缺点就是需要先把各个节点的数据存储到内存中然后再次进行分组,那么如果各个节点的数据过多那么在分组聚合的第一阶段可能会导致内存的大量消耗,所以接下来我将给大家讲解流式分组聚合

流式分组聚合

上个文章我们讲解过流式聚合,那么流式分组聚合流式聚合的差别在哪呢,很明显就是分组这个关键字上,我们如何保证下一个next所需的数据就是我上一个需要group的呢

答案就是order by

只要各个节点的order by后的排序字段和编程语言在内存中的一样即可保证

easy-query是如何实现的

        List<String> userIds = Arrays.asList("小明", "小绿");
        List<OrderGroupWithAvgOrderNoVO> list = easyProxyQuery.queryable(OrderEntityProxy.DEFAULT)
                .where((filter, t) -> filter.in(t.userId(), userIds))
                .groupBy((group, t) -> group.column(t.userId()))
                .orderByAsc((order, t) -> order.column(t.userId()))
                .select(OrderGroupWithAvgOrderNoVOProxy.DEFAULT, (selector, t) -> selector.columnAs(t.userId(), r -> r.userId()).columnAvgAs(t.orderNo(), r -> r.orderNoAvg()))
                .toList();

//生成的sql 会自动补齐count和sum来保证数据结果的正确性
SELECT t.`user_id` AS `user_id`,AVG(t.`order_no`) AS `order_no_avg`,COUNT(t.`order_no`) AS `orderNoRewriteCount`,SUM(t.`order_no`) AS `orderNoRewriteSum` 
FROM `t_order_202211` t 
WHERE t.`user_id` IN (?,?) 
GROUP BY t.`user_id` 
ORDER BY t.`user_id` ASC
[[{"orderNoAvg":916515.0000,"userId":"小明"},{"orderNoAvg":916516.5000,"userId":"小绿"}],"1924(ms)"]

分别对其进行求和和求count
[[{"orderNoSum":336000814605,"userId":"小明"}],"914(ms)"]
[[{"orderNoAvg":366607,"userId":"小明"}],"777(ms)"]
916515.0000=336000814605/366607 所以结果是正确的

分页聚合

如果您在项目中使用过分库分表,那么一定知道分库分表的难点在哪里,那么就是聚合数据范围和跨表数据返回,如果跨表数据返回再有一个难点那么就是分片数据跨分片分页,并且支持条件排序等处理操作。

内存分页

内存分页作为最简单的分页方法,在前几页的处理中有着非常方便的和高效的实用,具体原理如下

--原始sql
select * from order where time between 2020 and 2021 order by time limit 1,5
假如他被路由到20202021两张表那么要获取前10条数据应该怎么写
select * from order_2020 where time between 2020 and 2021 order by time limit 1,5
select * from order_2021 where time between 2020 and 2021 order by time limit 1,5

分别对两张表进行前10条数据的获取然后再内存中就有20条数据,针对这20条数据进行order by time的相同操作,然后获取前10条

那么如果是获取第二页呢

--原始sql
select * from order where time between 2020 and 2021 order by time limit 2,5
假如他被路由到20202021两张表那么要获取前10条数据应该怎么写

--错误的做法
select * from order_2020 where time between 2020 and 2021 order by time limit 2,5
select * from order_2021 where time between 2020 and 2021 order by time limit 2,5

通过上图我们清晰地可以知道这个解析是错误那么正确的应该是怎么样的呢

--原始sql
select * from order where time between 2020 and 2021 order by time limit 2,5
假如他被路由到20202021两张表那么要获取前10条数据应该怎么写

--正确的做法
select * from order_2020 where time between 2020 and 2021 order by time limit 1,10
select * from order_2021 where time between 2020 and 2021 order by time limit 1,10

考虑到最坏的情况就是6-10全部在左侧或者全部在右侧,因为数据的分布无法知晓所以我们应该以最坏的情况来获取数据然后获取6-10条数据

好了这样我们就实现了如何用内存来实现

 int pageIndex=x
int pageSize=y;
那么重写后的sql应该是
 int pageIndex=1
int pageSize=x*y;

虽然我们发现了如何正确的获取分页数据但是也存在一个非常严重的问题,就是深度分页导致的内存爆炸,因为每个分片的获取对象都是xy那么如果有n个分片被本次查询覆盖那么就需要获取至多xy*n条数据到内存,其中pageIndex就是x是用户自行选择的所以会存在x的大小不确定这样就会导致内存的严重消耗甚至oom。

流式分页

既然我们已经知道了内存分页的缺点那么是否有办法针对上述缺点进行规避或者优化呢,答案是有的就是流式分页,所谓流式分页就是利用ResultSet的延迟获取特点,配合之前的流式获取来适当性的放弃头部数据来达到节省内存的效果.

流式分页是如何优化程序的

 int pageIndex=x
int pageSize=y;
那么重写后的sql应该是
 int pageIndex=1
int pageSize=x*y;

因为jdbc的resultset延迟获取的特性,所以每次调用next才会将数据取到客户端,利用这个特性可以将前5条数据获取到并且放弃来实现内存严格控制,并且满足获取条数后后面的11-20是不需要获取的,有效的避免网络I/O的浪费和大大提高性能。

虽然流式分页可以大大的提高内存利用率,并且可以用最少的I/O次数来获取正确的分页数量由原先的xyn变成x*y,但是我们会发现在深度分页的情况下网络io还是需要实打实的获取到客户端进行判断,所以在深分页下不仅数据库压力大,客户端网络I/O压力也大,并且在页数很大的情况下默认起始页和结束页是默认显示在分页组件上的那么就就会导致用户很容易点到页尾导致程序进入卡死状态并且响应变慢从而拖慢应用

反排分页

跨分片深度分页解决方案

  • 1.让用户妥协只支持瀑布流分页,就是app滚动相似的分页,不支持跳页只支持next页放弃count仅limit获取,并且无法自定义排序,业务上直接禁止页尾跳页直接避免问题
  • 2.反向排序分页依然是count+limit的组合

我们都知道跨分片的聚合是因为深度分页慢是因为网络I/O的大量读取,所以如果我们可以保证网络I/O的读取次数变少那么是否就能解决这个问题

何谓反向分页首先我们来看一张图

原来我们需要跳过大量的网络I/O才能获取的正确数据如果我们有反向分页那么只需要跳过少量的数据就可以实现深度分页,并且因为大部分业务场景都支持跳页所以count的查询是一定会有的,我们只需要对各个分片的count进行第一次查询的获取那么就可以保证在深度分页下的反向排序分页

通过对order by的反向置换并且将offset重新计算来四线深度跨分片分页下I/O的极大减少保证正序的健壮性

顺序分页

到目前为止我们的页首和页尾节点的分页已经解决了,那么针对分页的中间部分改怎么办呢,是否还有优化方案呢,答案是有的但是这个优化方案对于分片方式有特殊的要求并没有前两种的通用化,顺序分片。
什么叫做顺序分页,顺序分页就是例如按时间按月分表,按年分表,按天分表,每张的内部数据永远是有一个特殊的排序字段可以让其依次从小到大排列。如果分片是这种特性的分片那么可以保证在order by这个特殊字段的时候几乎可以做到除了页首和页尾甚至中间任意节点的高性能

到目前为止如果您是顺序分页并且排序字段是顺序字段那么可以保证跨分片的查询和普通查询基本没有两样,但是我们其实还是发现了一个问题就是每次查询都需要count一下这个其实是很费时间的,并且基本上如果是大数量的情况下基本大致数据不需要更新或者只需要更新最新的一页即可

指定分页

基于上述问题easy-query实现了指定分页,就是可以通过第一次的分片记录下当前条件的各个节点的count数据,那么接下来的查询如果条件没有变化就不需要再进行count了,并且针对最新节点依然可以选择单独查询count从而来保证数据的准确性。

最后

通过上述几个讲述您应该已经对分表分库有了一个全新的理解和优化,接下来的几个篇章我将带你通过简单的实现和高级的抽象来完成easy-query的全新orm的变成之旅让分表分库变得非常简单且非常高效,并且会提出多种解决方案来实现老旧数据的迁移,数据分片不均匀,多字段分片索引的种种解决方案。

如果觉得有用请点击star谢谢大家了

QQ群:170029046

给美女换衣服!做完这个 AI 教程被老婆暴打了一顿...... - 零度解说

mikel阅读(1362)

来源: 给美女换衣服!做完这个 AI 教程被老婆暴打了一顿…… – 零度解说

1.下载最新版 stable-diffusion-webui 【点击下载
2.安装Python 3.10.6(较新版本的 Python 不支持 torch),勾选“Add Python to PATH”。
3.安装Git
4.以普通非管理员用户身份从 Windows 资源管理器运行webui-user.bat。
5.安装中文语言:

https://github.com/VinsonLaro/stable-diffusion-webui-chinese

6.暗黑模式:访问这个地址:

http://127.0.0.1:7860/?__theme=dark

7.安装 sd-webui-controlnet 外挂程序,新版本在插件中心搜索安装即可,如果你是旧版本,可以通过手动安装

https://github.com/Mikubill/sd-webui-controlnet

8.下载模型:【点击获取

 

9.所需主模型:chilloutmix_NiPrunedFp32Fix

Lora模型: 【cuteGirlMix4_v10】、【seeThroughSilhouette_v10

extremely detailed CG unity 8k wallpaper,(masterpiece),(best quality),(ultra detailed),(ultra realistic),(Best character details:1.36),nikon d750 f/1.4 55mm,dynamic angle,professional lighting, photon mapping, radiosity, physically-based rendering,
outdoors,looking at viewer,blush,(taut shirt), jeans,
1girl,(mature female:0.2),tall body,golden proportions,(Kpop idol),(shiny skin:1.2),(oil skin:1.1),makeup,[:(high detailed face:1.2):0.2]:, <lora:cuteGirlMix4_v10:0.8>, (close up), park, depth of field, <lora:seeThroughSilhouette_v10:0.5>,( closed mouth: 0.5)
((wavy gray hair and a sophisticated sense of style)),(aegyo sal:1),(puffy eyes),(eyelashes:1.1),(parted lips:1.1),red lipstick,wide shoulders,
Negative prompt: Multiple people,More than one person,2girl,DeepNegative,
sketches,lowres,polar lowres,(worst quality:2),(low quality:2),(normal quality:2),((monochrome)),((grayscale)),blurry,cropped,mutation,deformed,text,error,signature,watermark,username,extra digit,fewer digits,jpeg artifacts,
skin spots, acnes, skin blemishes,
bad anatomy,bad anatomy,bad proportions,gross proportions,long neck,cross-eyed,malformed limbs,blurred hands,fused fingers,poorly drawn face,poorly drawn hands,
(mutated hands and fingers:1.3),(mutated legs and foots:1.3),bad body,bad limbs,bad arms,bad hands,bad fingers,bad leg,bad feet,missing limbs,missing arms,missing hands,missing fingers,missing legs,missing footextra limbs,extra arms,extra fingers,extra leg,extra foot,
Steps: 28, Sampler: DPM++ SDE Karras, CFG scale: 7.5, Seed: 1340860639, Face restoration: CodeFormer, Size: 640x960, Model hash: fc2511737a, Model: chilloutmix_NiPrunedFp32Fix, Denoising strength: 0.4, Hires upscale: 1.5, Hires steps: 30, Hires upscaler: Latent (bicubic antialiased)

 

 

 

 

8.更多模板下载:

majicMIX sombre

XXMix_9realistic

 

majicMIX realistic

 

B2


B3

B4

 

8.进阶模型

推荐使用control_v11p_sd15_openpose.pth

期待下次更新…..

Stable Diffusion 常用模型下载与说明(保姆级) - 知乎

mikel阅读(1556)

来源: Stable Diffusion 常用模型下载与说明(保姆级) – 知乎

之前咱们一系列的文章介绍了AI绘画以及AI绘画的两大扛把子:Stable Diffusion 和 Midjourney,

之后又介绍了Stable Diffusion 的操作界面和基础参数:

那么,接下来我们就要学习怎么使用Stable Diffusion 中最重要的各类模型了。

因为,相比于Midjourney,Stable Diffusion最大的优势就是开源。相比于Midjourney靠开发人员开发的少数模型,SD则每时每刻都有人在世界各地训练自己的模型并免费公开共享给全世界的使用者。(当然你可以通过训练自己的专有模型而专门用于某一用途,这也将成为你作为AI绘画者的最重要的核心竞争力之一)

因此,学会使用各类模型对于学习使用Stable Diffusion非常重要。

常用模型下载网址推荐

目前,模型数量最多的两个网站civitai.com/huggingface.co/。civitai又称c站,有非常多精彩纷呈的模型,有了这些模型,我们分分钟就可以变成绘画大师,用AI画出各种我们想要的效果。

C站长这样:

你会看到很多模型的预览图被屏蔽了,需要你认证为成人才能浏览。至于为什么要成人才能浏览,想必大家也是懂的都懂。

也正是如此,网站在国内是被屏蔽的。登录需要科学上网。

Huggingface则相对朴实无华一些,对模型的审核也会更加严格一些。但是好处在于不需要科学上网,而且网速很快

Huggingface界面如上。

它是一个综合性的网站,如果我们需要下载模型的话,选择Models。

进入之后,选择Text-to-Image,出来的就都是SD可以用的模型了。

除了C站和huggingface,其他的模型网站还有:

cyberes.github.io/stabl

(SD的基础模型,不用科学上网,但是这些模型都一般般,意义不大)

rentry.co/sdmodels

(模型很多,但是界面没有C站友好,需要科学上网)

炼丹阁 (www.liandange.com)

(国内的网站,很多都是搬运的C站的模型,合规性未知,通过百度网盘下载)

LiblibAI(www.liblibai.com)

LiblibAI,号称是国内最大的原创AI模型分享网站,但其实很多都是搬运的C站的模型,不过确实也有不少人气原创模型发布者入驻了该网站。

不同模型的说明

如果你去自己下载模型,就会发现有各种不同类型的模型。

具体模型类型有checkpoint、Textual lnversion、Hypernetwork、Aesthetic Gradient、LoRA、LyCORIS、Controlnet、Poses、wildcards等等,看得人眼花缭乱。这些都是什么意思呢?

Checkpoint/大模型/底模型/主模型

Checkpoint模型是SD能够绘图的基础模型,因此被称为大模型、底模型或者主模型,WebUI上就叫它Stable Diffusion模型。安装完SD软件后,必须搭配主模型才能使用。不同的主模型,其画风和擅长的领域会有侧重。

checkpoint模型包含生成图像所需的一切,不需要额外的文件。但是它们体积很大,通常为2G-7G。

常见文件模式:尾缀ckpt、safetensors(如果都有提供的话建议下载safetensors,下同)

存放路径: \sd-webui-aki-v4\models\Stable-diffusion

模型的切换界面:

目前比较流行和常见的checkpoint模型有Anything系列(v3、v4.5、v5.0)、AbyssOrangeMix3、ChilloutMix、Deliberate、国风系列等等。这些checkpoint模型是从Stable Diffusion基本模型训练而来的,相当于基于原生安卓系统进行的二次开发。目前,大多数模型都是从 v1.4 或 v1.5 训练的。它们使用其他数据进行训练,以生成特定风格或对象的图像。这个我们后面还会专门开一个专题进行讲解。

不同模型在同一参数下的表现有时候可以用天差地别来形容,下面是个例子:

LoRA

当下最火的微调模型,可以将某一类型的人物或者事物的风格固定下来。它们通常为10-200 MB。必须与checkpoint模型一起使用。

现在比较火的Korean Doll Likeness、Taiwan Doll Likenes、Cute Girl mix都是真人美女LoRA模型,效果很惊艳。还有一些特定风格的LoRA也非常受欢迎,最著名的有墨心等。这个我们后面也会再开一个专题讲解。

常见文件模式:尾缀ckpt、safetensors、pt

存放路径: \sd-webui-aki-v4\models\Lora

有多个方式可以使用

方法1是在生成界面调取选用。这个的好处是可以自己设置预览图,从而有直观的感受。

而且部分LORA只支持这种方式使用(不过AI绘画日新月异,说不定哪天规则又变了~)

方法2是以插件形式使用。好处是可以很方便的灵活调用多个LORA,并对他们按着不同比例进行混合。

在启动器界面选择模型管理,点击LoRA模型(插件),点击添加模型,选择你要添加的LoRA模型,重启启动器。然后在WebUI界面选择相应的插件和权重比例即可。

VAE美化模型/变分自编码器

VAE,全名Variational autoenconder,中文叫变分自编码器。作用是:滤镜+微调。

有的大模型是会自带VAE的,比如Chilloutmix。如果再加VAE则可能画面效果不会更好,甚至适得其反。

顺便说一句,系统自带的VAE是animevae,效果一般,建议可以使用kl-f8-anime2或者vae-ft-mse-840000-ema-pruned。anime2适合画二次元,840000适合画写实人物。

常见文件模式: 尾缀ckpt、pt

存放路径: \sd-webui-aki-v4\ models\ VAE

模型的切换:

Embedding/Textual lnversion/文本反转模型和Hypernetworks

Embeddings 和 Hypernetworks 都属于微调模型,但目前Hypernetworks已经不太用了。

Embeddings/Textual lnversion中文翻译过来叫文本反转,通过仅使用的几张图像,就可以向模型教授新的概念。用于个性化图像生成。Embeddings是定义新关键字以生成新人物或图片风格的小文件。它们很小,通常为10-100 KB。必须将它们与checkpoint模型一起使用。

Embeddings 由于训练简单,文件小,因此一度很受大家欢迎。而且Embeddings 使用方法很简单,在安装之后,只要在提示词中提到它就相当于调用了,很方便。但由于Embeddings使用的训练集较小,因此出来的图片常常只是神似,做不到”形似“,所以目前很多人还是喜欢使用LORA模型。而且Embeddings 是一级目录,每次打开webui时都要加载一遍,太多了会影响webui的“开机速度”(但是不影响运行速度)。

不过有一些Embeddings 还是值得安装,比如EasyNegative这个Embeddings,里面包含了大量的负面词,可以减少你每次打一堆负面词的痛苦。

Embedding

常见文件模式: 尾缀pt

存放路径: \sd-webui-aki-v4\ embeddings

模型的切换通过文件名称来触发

Hypernetworks

常见文件模式: 尾缀pt

存放路径: \sd-webui-aki-v4\ models\ Hypernetworks

模型的切换通过文件名称来触发

DreamBooth模型

DreamBooth,可用于训练预调模型用的。是使用指定主题的图像进行演算,训练后可以让模型产生更精细和个性化的输出图像。

常见模式:尾缀ckpt、safetensors

常见大小:2G-7G

最新版本的DreamBooth是可以把那个Lora算法然后融合进来的

可以训练角色、画风、物件等,使用方法和主模型相同

训练路径:

LyCORIS模型

此类模型也可以归为Lora模型,也是属于微调模型的一种。一般文件大小在340M左右。不同的是训练方式与常见的lora不同,但效果似乎会更好不少。

其中本人较喜欢的“Miniature world style 微缩世界风格”就属于这类模型。

但要使用此类微调模型,需要先安装一个locon插件,直接将压缩包解压后放到StableDiffusion目录的extensions目录里。

插件地址

github.com/KohakuBluele

下载后直接解压缩在extensions中。

使用时注意,除了要将lora调入,还要在正向tag开头添加触发词

例如:这个微缩世界风格的lyCORIS的调用,正向描述语如下

mini\(ttp\), (8k, RAW photo, best quality, masterpiece:1.2), island, cinematic lighting,UHD,miniature, landscape, Crystal ball,on rock, <lora:miniatureWorldStyle_v10:0.8>

小技巧

如果你下载了一个模型,却不知道怎么安装,打开这个网站

spell.novelai.dev/

把你下载的模型拖进去,立马就会帮你解析,告诉你应该放在那里。

不过,由于AI绘画日新月异,有的模型,网站可能还来不及收集和解析,会无法解读。

最后,如果你也对AI绘画感兴趣的话,欢迎关注本专栏,关注AI时代社,也欢迎评论转发点赞分享。

Stable Diffusion 操作界面及基础参数介绍 - 知乎

mikel阅读(659)

来源: Stable Diffusion 操作界面及基础参数介绍 – 知乎

操作界面介绍

典型的Stable Diffusion操作界面如下图:

可以看到里面的参数非常多。

但新手小白其实只需要知道两个参数就行。

基础参数介绍

提示词(Prompt)和反向提示词(Negative Prompt)

提示词内输入的东西就是你想要画的东西,反向提示词内输入的就是你不想要画的东西

提示框内只能输入英文,所有符号都要使用英文半角,词语之间使用半角逗号隔开

反向提示词

这里重点提一下反向提示词,与提示词相反,反向提示词输入的是你不希望SD产生的。这是SD的一个非常强大但未被充分利用的功能。有时候你正面提示词写一堆,出来的效果也不理想,但是加上一个反向提示词就能获得理想的结果。

一般负面提示:低分辨率、错误、裁剪、最差质量、低质量、jpeg伪像、帧外、水印、签名

General: lowres, error, cropped, worst quality, low quality, jpeg artifacts, out of frame, watermark, signature

人物肖像的负面提示:变形、丑陋、残缺、毁容、文本、额外的四肢、面部切割、头部切割、额外的手指、额外的手臂、绘制不佳的脸、突变、比例不良、头部裁剪、四肢畸形、手突变、融合手指、长脖子

Negative prompts for people portraits: deformed, ugly, mutilated, disfigured, text, extra limbs, face cut, head cut, extra fingers, extra arms, poorly drawn face, mutation, bad proportions, cropped head, malformed limbs, mutated hands, fused fingers, long neck

逼真图像的负面提示:逼真:插图、绘画、素描、艺术、素描’

Negative prompts for photorealistic images: Photorealistic: illustration, painting, drawing, art, sketch’

基本上,掌握了提示词和反向提示词的书写,AI绘画就算是入门了(至少可以画出东西来了!)但如果想要更加一步的,请继续往下看(哪怕你是老手,下面的文章也值得你一看哦~)。

采样迭代步数(Steps)

Stable-Diffusion通过从充满噪点的画布开始创建图像,然后逐渐去噪以达到最终输出。Steps就是控制这些去噪步骤的数量。通常,越高越好,但一般情况下,我们使用的默认值是20个步骤,这其实已经足以生成任何类型的图像。

以下是有关在不同情况下使用steps的一般指南:

a 如果你正在测试新提示并希望获得快速结果来调整输入,请使用10-15个steps。

b 找到所需的提示后,将步骤增加到20-30,很多人的习惯是28

c 如果你正在创建带有毛皮或任何具有详细纹理的主题的面部或动物,并且觉得生成的图像缺少其中一些细节,请尝试将其提高到40或者更高

特别提示:

有些人习惯于一上来就创建具有100或150步的图像,这对于LMS等采样器很有用,但除非你有很强的显卡,否则很多时候都是浪费时间。先用小步骤去测试,找到合适的提示词后再提升步数才是正确的方法。

而且,使用改进的快速采样器(如 DDIM 和 DPM++系列)一般用100以内的步数就完全OK了,通过对这些采样器使用大量步骤,很可能只会浪费时间和GPU算力,而不会提高图像质量

采样方法(Sampler)

正如我们之前提到的,SD通过对起始噪声画布进行降噪来工作。这就是扩散采样器发挥作用的地方。简单来说,这些采样器是算法,它们在每个步骤后获取生成的图像并将其与文本提示请求的内容进行比较,然后对噪声进行一些更改,直到它逐渐达到与文本描述匹配的图像。

用户最常用的三个采样器分别是Euler a,DDIM和DPM++系列。你可以尝试这三个,看看哪个更适合你的提示。

由于采样器的规则过于学术化,仔细讲解也未必能说出一个123来,这里以相同的参数,不同的采样器去测试了同一张图,分别出来的效果如上,仅供参考。

总体而言,欧拉采样器(Euler a)具有更平滑的颜色和较少定义的边缘,使其更具“梦幻”外观,因此如果这是你在生成的图像中喜欢的效果,请使用Euler a。

DPM2和DPM++系列更加写实。

LMS、DPM fast 虽然出图快,但有可能人是不完整的。

生成批次和生成数量

生成批次是显卡一共生成几批图片。

每批数量是显卡每批生成几张图片。

也就是说你每点击一次生成按钮,生成的图片数量=批次*数量

需要注意的是每批数量是显卡一次所生成的图片数量,速度要比调高批次快一点,但是调的太高可能会导致显存不足导致生成失败,而生成批次不会导致显存不足,只要时间足够会一直生成直到全部输出完毕。

输出分辨率(宽度和高度)

图片分辨率非常重要,直接决定了你的图片内容的构成和细节的质量。

a.输出大小

输出大小决定了画面内容的信息量,很多细节例如全身构图中的脸部,饰品,复杂纹样等只有在大图上才能有足够的空间表现,如果图片过小,像是脸部则只会缩成一团,是没有办法充分表现的。

但是图片越大ai就越倾向于往里面塞入更多的东西,绝大多数模型都是在512*512分辨率下训练的,少数在768*768下训练,所以当输出尺寸比较大比如说1024*1024的时候,ai就会尝试在图中塞入两到三张图片的内容量,于是会出现各种肢体拼接,不受词条控制的多人,多角度等情况,增加词条可以部分缓解,但是更关键的还是控制好画幅,先画中小图,再放大为大图

大致的输出大小和内容关系参考:

1.约30w像素,如512*512,大头照和半身为主

2.约60w像素,如768*768,单人全身为主,站立或躺坐都有

3.越100w像素,如1024*1024,单人和两三人全身,站立为主

4.更高像素,群像,或者直接画面崩坏

b.宽高比例

宽高比例会直接决定画面内容,同样是1girl的例子:

1.方图512*512,会倾向于出脸和半身像

2.高图512*768,会倾向于出站着和坐着的全身像

3.宽图768*512,会倾向于出斜构图的半躺像

所以要根据想要的内容来调整输出比例。

提示词相关性(CFG Scale)

CFG这个参数可以看作是“创造力与提示”量表。较低的数字使AI有更多的自由发挥创造力,而较高的数字迫使它更多地坚持提示词的内容。

默认的CFG是7,这在创造力和生成你想要的东西之间是最佳平衡。通常不建议低于5,因为图像可能开始看起来更像AI的幻觉,而高于16可能会开始产生带有丑陋伪影的图像。

那么何时使用不同的CFG刻度值呢?CFG量表可以分为不同的范围,每个范围适用于不同的提示类型和目标。

1.CFG 2–6:创意,但可能过于失真,没有按照提示进行操作。对于简短的提示可能很有趣且有用;

2.CFG 7–10:建议用于大多数提示。创造力和引导式生成之间的良好平衡;

3.CFG 10–15:当你确定提示很详细并且非常清楚你希望图像的外观时(有些纯风景和建筑类的图片可能需要的CFG会比较高,大家可以参考相应的模型说明);

4.CFG 16–20:除非提示非常详细,否则通常不建议使用。可能会影响一致性和质量;

5.CFG >20:几乎从不使用

随机种子(seed)

随机种子是一个决定我们之前讨论的初始随机噪声的数字,由于随机噪声决定了最终图像,这就是为什么每次在StableDiffusion系统上运行完全相同的提示时都会得到不同的图像,以及为什么如果多次使用相同的提示运行相同的seed的时候,你会得到相同的生成图像。

由于相同的种子和提示组合每次都提供相同的图像,因此我们可以通过多种方式利用此属性:

a.控制角色的特定特征:在这个例子中,我们改变了情绪,但这也适用于其他物理特征,如头发颜色或肤色,但变化越小,它就越有可能被改变。

b.测试特定单词的效果:如果你想知道提示中的特定单词发生了哪些变化,则可以使用相同的种子和修改后的提示进行测试,最好通过每次更改单个单词或短语来测试提示。

c.更改样式:如果你喜欢图像的构图,但想知道它以不同的样式显示效果。这可用于肖像、风景或你创建的任何场景。

IMG 2 IMG参数

Img2img 功能的工作方式与 txt2img 完全相同,唯一的区别是你提供了一个用作起点的图像,而不是种子编号产生的噪声。

噪点被添加到你用作 img2img 的初始化图像的图像中,然后根据提示继续扩散过程。添加的噪声量取决于“重绘幅度(Denoising)”这个参数,该参数的范围从0到1,其中0根本不添加噪声,你将获得添加的确切图像,1完全用噪声替换图像,几乎就像你使用普通的txt2img而不是img2img一样。

那么如何决定使用什么力量呢?这是一个带有示例的简单指南:

a.要创建图像的变体,建议使用的强度为 0.5-0.75,并且具有相同的提示。当你喜欢创建的图像的构图但某些细节看起来不够好,或者你想创建与你在其他软件(如 Blender或 photoshop)中创建的图像相似的图像时,这可能很有用(在这种情况下,提示将是对原有图像的描述)。

b.要更改图像样式,同时使其与原始图像相似,你可以多次使用较低强度的img2img,与具有较高强度的单个img2img相比,可以获得更好的图像保真度。

在这个例子中,我们使用0.25的强度4次,所以每次我们生成图像时,我们都会将生成的图像重新插入img2img中,并以相同的提示和强度重新运行它,直到我们得到我们需要的样式。如果在img2img中使用相同的图像,强度更高,你将很快失去图像相似性。

基本上到这里的话,基础的SD参数我们都算是学完了。接下来要学的东西会更为进阶,欢迎大家持续关注。

C#使用企业微信群机器人推送生产数据 - Hello-MOMO - 博客园

mikel阅读(886)

来源: C#使用企业微信群机器人推送生产数据 – Hello-MOMO – 博客园

在日常的工作生产中,经常会有将将生产数据或者一些信息主动推送给相关的管理人员,我们公司在开发WMS系统时,为了仓库的储存安全,需要在危废品库存达到一定的储量时,自动通知仓管员去处理危废品,所以就需要程序自动的通过企业微信告知仓管员,这个时候就需要用到企业微信的机器人了。

现在我所知道的企业微信机器人分为两种,一种是机器人,一种是群机器人,机器人开发比较复杂,但是可以像一个企业微信账号一样可以给企业微信中的任意一个人发送信息,第二种群机器人比较简单,只能在群里推送消息。下面要讲的就是群机器人的开发。

第一步,先创建一个企业微信群(好像需要三个人才能达到建群的最小人数),添加一个群机器人,如图:

 

 

然后点击机器人的头像,记住Webhook(这个很重要,记住一定要保密,不能发到网上,不然其他人可以通过这个利用机器人给企业微信群发任何信息)

后面上代码,我写了一个方法如下:

public void WeChatRobot(string message)
{
string cttStr = “”;
cttStr += “# <font color=\\\”warning\\\”>”+ message + “</font>\n”;
string param = “{\”msgtype\”:\”markdown\”,\”markdown\”:{\”content\”:\”” + cttStr + “\”}}”;
string webhookUrl = “此处替换为企业微信群机器人的Webhook”;

using (var client = new RestClient(webhookUrl))
{
var Req = new RestRequest(webhookUrl, Method.Post);
Req.AddHeader(“Content-Type”, “application/json”);
Req.AddJsonBody(param);
var Rsp = client.ExecuteAsync(Req).Result;

}
}

代码这个地方记得替换为企业微信群机器人的Webhook

如果有报错记得引用一下RestSharp库。

将需要推送的信息赋值给该方法的message,就可以使用企业微信机器人将信息推送到群里了。

补充:下面是一些推送文字的格式,现在似乎只支持三种颜色的字体。

 

 

思考(不用看):

在生产过程中经常有订单需要返回上一步,比如有些订单在该工序已经点击生产完工了,但是由于需要补充一些生产信息,需要将订单重新返回到正在生产的状态,这在正常的生产流程中肯定是不被允许的,但是实体制造业的IT部门都是服务于生产的,无论无何都不能耽误生产,最终还是需要IT去数据库改数据,有时常常下班了还要远程电脑改订单的固定数据,就很烦,明明知道有这个需求,又不能放权写个功能让生产自己回退订单,所以就思考写一个企业微信群机器人的推送功能:

生产需要将订单退回上一步时,将订单号和回退原因填写在MES系统上,这是系统后台会生成一个随机的验证码(后台生成,MES操作员不知道),将验证码和订单号、订单回退信息、操作人和时间等信息保存在数据库,并通过企业微信群机器人将验证码和订单回退信息推送到IT群,IT评估后若允许订单回退就将验证码转发给操作员,由操作员在MES上填写验证码,确认后填写验证码与数据库保存的验证码相同时,执行订单回退操作。这样遇到订单回退的突发情况,即使电脑不在身边,也可以通过手机企业微信对订单回退进行管控,运维人员也可以少掉头发。

(使用文章请标明来源——Hello-MOMO

layui table中select显示优化 - &执念 - 博客园

mikel阅读(698)

来源: layui table中select显示优化 – &执念 – 博客园

项目中经常用到table和select的情况,每次都会遇到select显示不全或者隐藏的问题,今天记录下解决方案:

初始化表格后:select 无法正常显示,如下:

 

 

1、我们增加一行样式:

.layui-table-cell {
            overflow: visible;
        }

效果如下:虽然select功能可以使用了 但是还是很别扭  那么我们继续优化;

 

2、继续追加样式:

 

        .layui-table-box , .layui-table-body {
            overflow: visible;
        }

效果如下 :这种解决办法,适用于table不定高的情况;

3、如果table是固定高度 需要内部滚动呢,单纯的设置overflow: visible;效果如下:

 

4、最后来个终极解决办法,效果如下:

 

代码:

主要引入了 optimizeSelectOption 插件,稍后上源码。

复制代码
    layui.config({
        base: "./layui/lay/my-modules/"
    }).extend({
        optimizeSelectOption: "optimizeSelectOption/optimizeSelectOption",//引入js
    }).use(['layer','element', 'table','form','optimizeSelectOption'], function(){
        var  $ = layui.$, layer = layui.layer, form = layui.form, table = layui.table, element = layui.element;

        var pathTemp = layui.cache.modules['optimizeSelectOption'] || '';
        var filePath = pathTemp.substr(0, pathTemp.lastIndexOf('/'));

        // 引入css
        layui.link(filePath + '/optimizeSelectOption.css');



    });
复制代码

optimizeSelectOption .js

复制代码
/*!

 @Title: optimizeSelectOption
 @Description:优化select在layer和表格中显示的问题
 @Site:
 @Author: 岁月小偷
 @License:MIT

 */
;!function (factory) {
    'use strict';
    var modelName = 'optimizeSelectOption';
    if (!window.top.layui) {
        // 顶层引入layui是使用该组件的最低要求
        console.warn('使用插件:' + modelName + '页面顶层窗口必须引入layui');
        layui.define(['form'], function (exports) {
            exports(modelName, {msg: '使用插件:' + modelName + '页面顶层窗口必须引入layui'});
        });
    } else {
        // 利用的是顶层的top去弹出,所以用top的layui去use layer确保顶层
        window.top.layui.use('layer', function () {
            layui.define(['form'], function (exports) { //layui加载
                exports(modelName, factory(modelName));
            });
        });
    }
}(function (modelName) {
    var version = '0.2.0';
    var $ = layui.$;
    var form = layui.form;
    var layer = layui.layer;

    var filePath = layui.cache.modules.optimizeSelectOption
        .substr(0, layui.cache.modules.optimizeSelectOption.lastIndexOf('/'));
    // 引入tablePlug.css
    layui.link(filePath + '/optimizeSelectOption.css?v' + version);

    var selectors = [
        '.layui-table-view',      // layui的表格中
        '.layui-layer-content',   // 用type:1 弹出的layer中
        '.select_option_to_layer' // 任意其他的位置
    ];
    // 记录弹窗的index的变量
    window.top.layer._indexTemp = window.top.layer._indexTemp || {};

    if (!form.render.plugFlag) {
        // 只要未改造过的才需要改造一下
        // 保留一下原始的form.render
        var formRender = form.render;
        form.render = function (type, filter, jqObj) {
            var that = this;
            var retObj;
            // if (jqObj && jqObj.length) {
            if (jqObj) {
                layui.each(jqObj, function (index, elem) {
                    elem = $(elem);
                    var elemP = elem.parent();
                    var formFlag = elemP.hasClass('layui-form');
                    var filterTemp = elemP.attr('lay-filter');
                    // mark一下当前的
                    formFlag ? '' : elemP.addClass('layui-form');
                    filterTemp ? '' : elemP.attr('lay-filter', 'tablePlug_form_filter_temp_' + new Date().getTime() + '_' + Math.floor(Math.random() * 100000));
                    // 将焦点集中到要渲染的这个的容器上
                    retObj = formRender.call(that, type, elemP.attr('lay-filter'));
                    // 恢复现场
                    formFlag ? '' : elemP.removeClass('layui-form');
                    filterTemp ? '' : elemP.attr('lay-filter', null);
                });
            } else {
                retObj = formRender.call(that, type, filter);
            }
            return retObj;
        };
        form.render.plugFlag = true;
    }

    // 关闭弹出的选项
    var close = function () {
        window.top.layer.close(window.top.layer._indexTemp[modelName]);
    };

    // 获得某个节点的位置 offsetTop: 是否获得相对top window的位移
    function getPosition(elem, _window, offsetTop) {
        _window = _window || window;
        elem = elem.length ? elem.get(0) : elem;
        var offsetTemp = {};
        if (offsetTop && _window.top !== _window.self) {
            var frameElem = _window.frames.frameElement;
            offsetTemp = getPosition(frameElem, _window.parent, offsetTop);
        }
        var offset = elem.getBoundingClientRect();

        return {
            top: offset.top + (offsetTemp.top||0),
            left: offset.left + (offsetTemp.left||0)
        };
    }

    var config = {};

    // 针对某个组件的效果优化注册方法
    var render = function (name, options) {
        var that = this;
        if (config.name) {
            console.warn('针对', name, '的显示优化已经存在,请不要重复渲染!');
            return that;
        }

        // 优化select的选项在某些场景下的显示问题
        $(document).on('click'
            , selectors.map(function (value) {
                // return value + ' .layui-select-title,' + value + ' .xm-select-title';
                return value + ' ' + options.triggerElem;
            }).join(',')
            , function (event) {
                layui.stope(event);
                close();
                var triggerElem = $(this);
                var titleElem = triggerElem;
                var dlElem = typeof options.dlElem === 'function' ? options.dlElem(triggerElem) : titleElem.next();
                // var selectElem = titleElem.parent().prev();
                var selectElem = titleElem.parent().prev();
                var selectupFlag = titleElem.parent().hasClass('layui-form-selectup');

                function getDlPosition() {
                    var titleElemPosition = getPosition(titleElem, window, true);
                    var topTemp = titleElemPosition.top;
                    var leftTemp = titleElemPosition.left;
                    if (selectupFlag) {
                        topTemp = topTemp - dlElem.outerHeight() + titleElem.outerHeight() - parseFloat(dlElem.css('bottom'));
                    } else {
                        topTemp += parseFloat(dlElem.css('top'));
                    }
                    if (topTemp + dlElem.outerHeight() > window.top.innerHeight && !selectupFlag) {
                        // 出现原始的form表单判断向下弹出,但是最终弹出超出窗口下边界的情形的处理
                        selectupFlag = true;
                        topTemp -= (dlElem.outerHeight() + (2 * parseFloat(dlElem.css('top')) - titleElem.outerHeight()));
                    }
                    return {
                        top: topTemp,
                        left: leftTemp
                    };
                }

                var dlPosition = getDlPosition();

                titleElem.css({backgroundColor: 'transparent'});
                window.top.layer._indexTemp[modelName] = window.top.layer.open({
                    type: 1,
                    title: false,
                    closeBtn: 0,
                    shade: 0,
                    anim: -1,
                    fixed: titleElem.closest('.layui-layer-content').length || window.top !== window.self,
                    isOutAnim: false,
                    // offset: [topTemp + 'px', leftTemp + 'px'],
                    offset: [dlPosition.top + 'px', dlPosition.left + 'px'],
                    // area: [dlElem.outerWidth() + 'px', dlElem.outerHeight() + 'px'],
                    area: dlElem.outerWidth() + 'px',
                    content: '<div class="layui-unselect layui-form-select layui-form-selected"></div>',
                    skin: 'layui-option-layer',
                    success: function (layero, index) {
                        dlElem.css({
                            top: 0,
                            position: 'relative'
                        }).appendTo(layero.find('.layui-layer-content').css({overflow: 'hidden'}).find('.layui-form-selected'));
                        layero.width(titleElem.width());
                        // 原本的做法在ie下获得的是auto其他的浏览器却是确定的值,目前简单处理,先自行计算出来,后面再调优
                        var bottom_computed = window.top.innerHeight - layero.outerHeight() - parseFloat(layero.css('top'));
                        selectupFlag && (layero.css({top: 'auto', bottom: bottom_computed + 'px'}));
                        // 调用各自的success回调
                        typeof options.success === 'function' && options.success.call(this, index, layero);
                        // 通用的事件处理
                        layero.on('mousedown', function (event) {
                            layui.stope(event);
                        });
                        setTimeout(function () {
                            // 延迟500毫秒添加事件处理,应对ie浏览器下某一些特定场景下点击select出来option的时候回有一个容器滚动导致直接关闭选项的问题
                            // 不包含selectors的选择器的节点
                            titleElem.parentsUntil(selectors.join(',')).one('scroll', function (event) {
                                // 用window.top.layer去弹出的选项在其title所在的容器滚动的时候都关闭
                                close();
                            });
                            // 单独给选择器的节点加上
                            titleElem.parents(selectors.join(',')).one('scroll', function (event) {
                                // 用window.top.layer去弹出的选项在其title所在的容器滚动的时候都关闭
                                close();
                            });

                            var windowTemp = window;
                            do {
                                var $Temp = windowTemp.$ || windowTemp.layui.$;
                                if ($Temp) {
                                    // 点击document的时候触发
                                    $Temp(windowTemp.document).one('click', function (event) {
                                        close();
                                    });

                                    $Temp(windowTemp.document).one('mousedown', function (event) {
                                        close();
                                    });

                                    // 窗口resize的时候关掉表格中的下拉
                                    $Temp(windowTemp).one('resize', function (event) {
                                        close();
                                    });

                                    // 监听滚动在必要的时候关掉select的选项弹出(主要是在多级的父子页面的时候)
                                    $Temp(windowTemp.document).one('scroll', function () {
                                        if (top !== self && parent.parent) {
                                            // 多级嵌套的窗口就直接关掉了
                                            close();
                                        }
                                    });
                                }
                            } while (windowTemp.self !== windowTemp.top ? windowTemp = windowTemp.parent : false);
                        }, 500);
                    },
                    end: function () {
                        typeof options.end === 'function' && options.end.call(this, selectElem);
                    }
                });
            });
    };

    // 内置初始化layui的select的效果,目前还不够晚上不提供给外部注册一些其他有类似问题的组件的处理
    render('layuiSelect', {
        triggerElem: 'div:not(.layui-select-disabled)>.layui-select-title', // 触发的选择器
        success: function (index, layero) {
            // layui 的select是单选点击一个的时候就关闭layer
            layero.find('dl dd').click(function () {
                close();
            });
        },
        end: function (selectElem) {
            form.render('select', null, selectElem);
        }
    });

    return {
        version: version,
        getPosition: getPosition,
        close: close
        // render: render  // 不完善,暂不对外提供
    };
});
复制代码

optimizeSelectOption .css

复制代码
.layui-layer-content div.layui-form-select>div.layui-select-title+ dl,
.select_option_to_layer div.layui-form-select>div.layui-select-title+ dl
{
    visibility: hidden;
}

.layui-layer.layui-option-layer {
    mso-border-shadow: no;
    box-shadow: none;
}
复制代码

最后 奉上一个很棒的demo:https://sun_zoro.gitee.io/layuitableplug/testTableCheckboxDisabled?v0.1.9

thinkphp中的add(),save(),delete()返回值问题_tp3 add_echo_just_do_it的博客-CSDN博客

mikel阅读(524)

来源: thinkphp中的add(),save(),delete()返回值问题_tp3 add_echo_just_do_it的博客-CSDN博客

tp3.2中

1.add()

add()方法成功时,返回的是插入数据的id,失败时,返回的是false。

也就是说我们在判断add()是否成功时,只需要判断结果是否等于false

if($result===false){

echo “添加失败!”;

}else{

echo “添加成功”;

}

2.save()

save()方法成功时,返回影响行的行数。

如果更新的内容没有变化,即更新的数据和表中原数据一模一样,则返回的是0,

新学tp的小伙伴容易犯一个错,用

if($ressult){

echo “更新成功”;

}else{

echo “更新失败”;

}

这样的方式判断更新结果是错误的方式,会带来返回值为0是也报错

应该用

if($result===false){

echo “更新失败!”;

}else{

echo “更新成功”;

}

来判断更新结果

3.delete()

delete()返回的删除影响的行数,跟save()方法差不多,判断时,也要用

if($result===false){

echo “删除失败!”;

}else{

echo “删除成功”;

}

4.综上,我们在判断add(),save().delete()方法的返回值是否正确的时候,都可以用

if($result===false){

echo “操作失败!”;

}else{

echo “操作成功”;

}

来判断,也可以用

if(is_bool($result)){

echo “操作失败!”;

}else{

echo “操作成功!”;

}

来判断。
————————————————
版权声明:本文为CSDN博主「echo_just_do_it」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_27930635/article/details/78853908