Giter Site home page Giter Site logo

troyzhxu / bean-searcher Goto Github PK

View Code? Open in Web Editor NEW
1.2K 23.0 131.0 6.33 MB

🔥🔥🔥 A read-only ORM focusing on advanced query, naturally supports joined tables, and avoids DTO/VO conversion, making it possible to realize complex query in one line of code !

Home Page: https://bs.zhxu.cn

License: Apache License 2.0

Java 6.11% Groovy 0.04% Shell 0.04% Batchfile 0.02% CSS 0.06% JavaScript 93.27% HTML 0.33% TypeScript 0.06% Vue 0.08%
java devtool orm spring-boot

bean-searcher's Introduction

logo

Maven Central License Troy.Zhou

English | 中文

✨ Features

  • Support one entity mapping to multi tables
  • Support dynamic field operator
  • Support group and aggregation query
  • Support Select | Where | From subquery
  • Support embedded params in entity
  • Support field converters
  • Support sql interceptors
  • Support sql dialect extension
  • Support multi datasource and dynamic datasource
  • Support annotation omitting and customizing
  • Support field operator extension
  • and so on

⁉️WHY

This is not a repeating wheel

Although CREATE/UPDATE/DELETE are the strengths of Hibernate, MyBatis, DataJDBC and other ORM, queries, especially complex list queries with multi conditions, multi tables, paging, sorting, have always been their weaknesses.

Traditional ORM is difficult to realize a complex list retrieval with less code, but Bean Searcher has made great efforts in this regard. These complex queries can be solved in almost one line of code.

  • For example, such a typical requirement:

The back-end needs to write a retrieval API, and if it is written with traditional ORM, the complexity of the code is very high

But Bean Searcher can:

💥 Achieved with one line of code

First, you have an Entity class:

@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
  private long id;
  private String username;
  private int status;
  private int age;
  private String gender;
  private Date joinDate;
  private int roleId;
  @DbField("r.name")
  private String roleName;
  // Getters and setters...
}

Then you can complete the API with one line of code :

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private BeanSearcher beanSearcher;              // Inject BeanSearcher

    @GetMapping("/index")
    public SearchResult<User> index(HttpServletRequest request) {
        // Only one line of code written here
        return beanSearcher.search(User.class, MapUtils.flat(request.getParameterMap()), new String[]{ "age" });
    }

}

This line of code can achieve:

  • Retrieval from multi tables
  • Pagination by any field
  • Combined filter by any field
  • Sorting by any field
  • Summary with age field

For example, this API can be requested as follows:

  • GET: /user/index

    Retrieving by default pagination:

    {
      "dataList": [
        {
          "id": 1,
          "username": "Jack",
          "status": 1,
          "age": 25,
          "gender": "Male",
          "joinDate": "2021-10-01",
          "roleId": 1,
          "roleName": "User"
        },
        ...     // 15 records default
      ],
      "totalCount": 100,
      "summaries": [
        2500    // age statistics
      ]
    }
  • GET: /user/index? page=1 & size=10

    Retrieval by specified pagination

  • GET: /user/index? status=1

    Retrieval with status = 1 by default pagination

  • GET: /user/index? name=Jac & name-op=sw

    Retrieval with name starting with Jac by default pagination

  • GET: /user/index? name=Jack & name-ic=true

    Retrieval with name = Jack(case ignored) by default pagination

  • GET: /user/index? sort=age & order=desc

    Retrieval sorting by age descending and by default pagination

  • GET: /user/index? onlySelect=username,age

    Retrieval username,age only by default pagination:

    {
      "dataList": [
        {
          "username": "Jack",
          "age": 25,
        },
        ...     // 15 records default
      ],
      "totalCount": 100,
      "summaries": [
        2500    // age statistics
      ]
    }
  • GET: /user/index? selectExclude=joinDate

    Retrieving joinDate excluded default pagination

✨ Parameter builder

Map<String, Object> params = MapUtils.builder()
        .selectExclude(User::getJoinDate)                 // Exclude joinDate field
        .field(User::getStatus, 1)                        // Filter:status = 1
        .field(User::getName, "Jack").ic()                // Filter:name = 'Jack' (case ignored)
        .field(User::getAge, 20, 30).op(Opetator.Between) // Filter:age between 20 and 30
        .orderBy(User::getAge, "asc")                     // Sorting by age ascending 
        .page(0, 15)                                      // Pagination: page=0 and size=15
        .build();
List<User> users = beanSearcher.searchList(User.class, params);

Demos

🚀 Rapid development

Using Bean Searcher can greatly save the development time of the complex list retrieval apis!

  • An ordinary complex list query requires only one line of code
  • Retrieval from single table can reuse the original domain, without defining new Entity

🌱 Easy integration

Bean Searcher can work with any JavaWeb frameworks, such as: SpringBoot, SpringMVC, Grails, Jfinal and so on.

SpringBoot / Grails

All you need is to add a dependence:

implementation 'cn.zhxu:bean-searcher-boot-stater:4.3.0'

and then you can inject Searcher into a Controller or Service:

/**
 * Inject a MapSearcher, which retrieved data is Map objects
 */
@Autowired
private MapSearcher mapSearcher;

/**
 * Inject a BeanSearcher, which retrieved data is generic objects
 */
@Autowired
private BeanSearcher beanSearcher;

Solon Project

All you need is to add a dependence:

implementation 'cn.zhxu:bean-searcher-solon-plugin:4.3.0'

and then you can inject Searcher into a Controller or Service:

/**
 * Inject a MapSearcher, which retrieved data is Map objects
 */
@Inject
private MapSearcher mapSearcher;

/**
 * Inject a BeanSearcher, which retrieved data is generic objects
 */
@Inject
private BeanSearcher beanSearcher;

Other frameworks

Adding this dependence:

implementation 'cn.zhxu:bean-searcher:4.3.0'

then you can build a Searcher with SearcherBuilder:

DataSource dataSource = ...     // Get the dataSource of the application

// DefaultSqlExecutor suports multi datasources
SqlExecutor sqlExecutor = new DefaultSqlExecutor(dataSource);

// build a MapSearcher
MapSearcher mapSearcher = SearcherBuilder.mapSearcher()
        .sqlExecutor(sqlExecutor)
        .build();

// build a BeanSearcher
BeanSearcher beanSearcher = SearcherBuilder.beanSearcher()
        .sqlExecutor(sqlExecutor)
        .build();

🔨 Easy extended

You can customize and extend any component in Bean Searcher .

For example:

  • Customizing FieldOp to support other field operator
  • Customizing DbMapping to support other ORM‘s annotations
  • Customizing ParamResolver to support JSON query params
  • Customizing FieldConvertor to support any type of field
  • Customizing Dialect to support more database
  • and so and

📚 Detailed documentation

Reference :https://bs.zhxu.cn

🤝 Friendship links

[ Sa-Token ] 一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!

[ Fluent MyBatis ] MyBatis 语法增强框架, 综合了 MyBatisPlus, DynamicSql,Jpa 等框架的特性和优点,利用注解处理器生成代码

[ OkHttps ] 轻量却强大的 HTTP 客户端,前后端通用,支持 WebSocket 与 Stomp 协议

[ hrun4j ] 接口自动化测试解决方案 --工具选得好,下班回家早;测试用得对,半夜安心睡

[ JsonKit ] 超轻量级 JSON 门面工具,用法简单,不依赖具体实现,让业务代码与 Jackson、Gson、Fastjson 等解耦!

[ Free UI ] 基于 Vue3 + TypeScript,一个非常轻量炫酷的 UI 组件库 !

❤️ How to contribute

  1. Fork code!
  2. Create your own branch: git checkout -b feat/xxxx
  3. Submit your changes: git commit -am 'feat(function): add xxxxx'
  4. Push your branch: git push origin feat/xxxx
  5. submit pull request

bean-searcher's People

Contributors

click33 avatar corey77 avatar humlzy avatar muyuanjin avatar noear avatar troyzhxu avatar vampireachao avatar zbcloading avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bean-searcher's Issues

SearchException: 不能把【class java.lang.Boolean】类型的数据库值转换为【class java.lang.Integer】类型的字段值,你可以添加一个 BFieldConvertor 来转换它!

异常信息:

[class com.songlanyun.modules.user.entity.SysUserEntity#isEpiboly] is mismatch with it's database table field type,异常类型:class com.ejlchina.searcher.SearchException

com.ejlchina.searcher.SearchException: 不能把【class java.lang.Boolean】类型的数据库值转换为【class java.lang.Integer】类型的字段值,你可以添加一个 BFieldConvertor 来转换它!

dfa9b2dc62c1df007245014f2ef7696

Unhandled Exception org.springframework.beans.BeansException

bean-searcher-boot-starter模块下的BeanSearcherAutoConfiguration文件,出现未处理的异常。
如果try- catch捕获或是将异常添加到签名,捕获 BeansException或是其子类,又会报
需要的类型:Throwable
提供的类型:BeansException。

截屏2022-05-20 上午10 15 13

BEP 51: 参数构建器 可为字段 自定义 SQL 条件

目的(BeanSearcher Enhancement Proposal)

  • 让使用者可以在 Java 代码里更灵活的定义条件

使用效果

Map<String, Object> params = MapUtils.builder()
    // 接收一个 SQL 片段作为参数值,$ 表示当前字段
    .field(User::getId).sql("$1 in (select id from xxx)")
    .build();
List<User> users = beanSearcher.searchList(User.class, params);

// select * from user u where u.id in (select id from xxx)
Map<String, Object> params = MapUtils.builder()
    // 接收一个 SQL 片段作为参数值,$ 表示当前字段
    .field(User::getId).sql("exsits (select 1 from xxx where user_id = $1)")
    .build();
List<User> users = beanSearcher.searchList(User.class, params);

// select * from user u where exsits (select 1 from xxx where user_id = u.id)

同时操作多个字段:

Map<String, Object> params = MapUtils.builder()
    // 接收一个 SQL 片段作为参数值,$ 表示当前字段
    .field(User::getId, User::getName).sql("$1 in (10, 11) or $2 like '张%'")
    .build();
List<User> users = beanSearcher.searchList(User.class, params);

// select * from user u where u.id in (10, 11) or u.name like '张%'

欢迎讨论

你是否有更好的语法,说说你的理由吧 ^_^

Boolean 类型的字段,当检索时 该字段传入的参数值为 空串时,BoolValueFilter 会将其转换为 true, 而没有忽略它

这个问题将在 v3.5.3 中修复。
再此之前,我们可以声明一个自定义的 参数过滤器 来绕过它:

@Bean
public ParamFilter myBoolValueFilter() {
    return new BoolValueFilter() {
	@Override
	protected Boolean toBoolean(Object value) {
            if (value instanceof String && StringUtils.isBlank((String) value)) {
                return null;
            }
            return super.toBoolean(value);
	}
    };
}

v3.8 新增 NotLike 运算符

用法:

Map<String, Object> params = MapUtils.builder()
    .field(User::getName, "张%").op(NotLike.class)
    .build();
List<User> users = beanSearcher.searchList(User.class, params);

生成的 SQL:

select * from user where name not like ?
-- 参数:'张%'

添加 PostgreSql 方言实现

public class PostgreSqlDialect implements Dialect {

	@Override
	public void toUpperCase(StringBuilder builder, String dbField) {
		builder.append("upper").append("(").append(dbField).append(")");
	}
	
	@Override
	public SqlWrapper<Object> forPaginate(String fieldSelectSql, String fromWhereSql, Paging paging) {
		SqlWrapper<Object> wrapper = new SqlWrapper<>();
		StringBuilder ret = new StringBuilder();
		ret.append(fieldSelectSql).append(fromWhereSql);
		if (paging != null) {
			ret.append(" offset ? limit ?");
			wrapper.addPara(paging.getOffset());
			wrapper.addPara(paging.getSize());
		}
		wrapper.setSql(ret.toString());
		return wrapper;
	}

}

如何实现 更加复杂 的 or 条件分组查询

Bean Searcher v3.1.x 及之前版本只支持静态的 or 条件,即在 @SearchBean 注解的 joinCond 里写 SQL 片段,例如:

@SearchBean(joinCond="name like ':value%' or phone = ':value%'")
public class User {
   // ...
}

它生的 SQL 片段是 name like '?%' or phone = '?%',这是使用了内嵌参数的语法:https://bs.zhxu.cn/guide/latest/params.html#%E6%99%AE%E9%80%9A%E5%86%85%E5%B5%8C%E5%8F%82%E6%95%B0

但这有一个缺点,就是,如果前端没传参数 value 的话,JDBC 传的参数会是 null,这有时可能并不是我们期望的。
此时可以在 Controller 层判断一下参数,如果没传,就手动放一个 空串值进来,例如:

@GetMapping("/index")
public SearchResult<User> index(@RequestParam Map<String, Object> params) {
    params.putIfAbsent("value", "");     // 如果没有,则给一个空值
    return beanSearcher.search(User.class, params);
}

简化

既然追求代码层面的简洁,为什么不把 mapSearcher.search(User.class, MapUtils.flat(request.getParameterMap())) 简化成mapSearcher.search(User.class)这样,不更好吗?

查询结果拼装的问题

调用多个service 查询数据后,拼装数据,还需要将 List<Map<String,Object>> 转换为 相应的对象 List

是否有计划处理这种问题?

BEP 55:实体类 内嵌对象 与 列表

实体类内 支持嵌套 对象

用法1:内嵌对象

@SearchBean(tables="user u, profile p", joinCond="u.id = p.user_id")
public class User {

    // 和主表数据在同一个 SQL 查出来,参数 value=p 指定该对象的数据来自 profile 表
    @DbFiled(type=DbType.INLINE, value="p")
    private Profile profile;        

    // 省略其它...
}

public class Profile {

    private String address;

    // 省略其它...
}

用法2:内嵌列表

@SearchBean(tables="user u, user_role ur, role r", joinCond="u.id = ur.user_id and ur.role_id = r.id")
public class User {

    private Long id;

    // 类似 mybatis 的 <collection> 标签
    // 和主表数据在同一个 SQL 查出来,value=r 指定该集合的数据来自 role 表
    @DbFiled(type=DbType.COLLECTION, value="r")
    private List<Role> roles;

    // 省略其它...
}

public class Role {

    private Long id;

    // 省略其它...
}

JDK8 实体类有 `LocalDate` 类型的字段时报错:NoSuchMethodError: java.time.LocalDate.ofInstant(Ljava/time/Instant;Ljava/time/ZoneId;)

解决方法:

方法一:

使用 java.sql.Date 类型替代 LocalDate

方法二:

添加一个自定义的转换器,继承 DateFieldConvertor ,注册为 Spring 的 Bean:

@Bean
public DateFieldConvertor dateFieldConvertor() {
    return new DateFieldConvertor() {

        @Override
        public Object convert(Class<?> targetType, Object value) {
            Class<?> valueType = value.getClass();
            if (Date.class.isAssignableFrom(valueType)) {
                Date date = (Date) value;
                if (targetType == java.sql.Date.class) {
                    return new java.sql.Date(date.getTime());
                }
                if (targetType == Timestamp.class) {
                    return new Timestamp(date.getTime());
                }
                if (targetType == LocalDateTime.class) {
                    // 注意:java.sql.Date 的 toInstant() 方法会抛异常
                    if (date instanceof java.sql.Date) {
                        LocalDate localDate = ((java.sql.Date) date).toLocalDate();
                        return LocalDateTime.of(localDate, LocalTime.of(0, 0, 0, 0));
                    }
                    return LocalDateTime.ofInstant(date.toInstant(), getZoneId());
                }
                if (targetType == LocalDate.class) {
                    // 注意:java.sql.Date 的 toInstant() 方法会抛异常
                    if (date instanceof java.sql.Date) {
                        return ((java.sql.Date) date).toLocalDate();
                    }
                    return toLocalDate(date.toInstant());
                }
                if (targetType == Date.class) {
                    return date;
                }
            }
            LocalDateTime dateTime;
            if (valueType == LocalDateTime.class) {
                dateTime = (LocalDateTime) value;
            } else {
                dateTime = LocalDateTime.of((LocalDate) value, LocalTime.of(0, 0));
            }
            if (targetType == LocalDateTime.class) {
                return dateTime;
            }
            Instant instant = dateTime.atZone(getZoneId()).toInstant();
            if (targetType == Date.class) {
                return new Date(instant.toEpochMilli());
            }
            if (targetType == java.sql.Date.class) {
                return new java.sql.Date(instant.toEpochMilli());
            }
            if (targetType == Timestamp.class) {
                return new Timestamp(instant.toEpochMilli());
            }
            if (targetType == LocalDate.class) {
                return toLocalDate(instant);
            }
            throw new UnsupportedOperationException();
        }

        private LocalDate toLocalDate(Instant instant) {
            ZoneOffset offset = getZoneId().getRules().getOffset(instant);
            long localSecond = instant.getEpochSecond() + offset.getTotalSeconds();
            long localEpochDay = Math.floorDiv(localSecond, 86400);
            return LocalDate.ofEpochDay(localEpochDay);
        }

    };
}

新增 `ResultFilter` 可对检索结果做进一步转换处理

public interface ResultFilter {

    /**
     * ResultFilter
     * 对 {@link BeanSearcher } 的检索结果做进一步转换处理
     * @param result 检索结果
     * @param beanMeta 检索实体类的元信息
     * @param paraMap 检索参数
     * @param fetchType 检索类型
     * @param <T> 泛型
     * @return 转换后的检索结果
     */
    default <T> SearchResult<T> doBeanFilter(SearchResult<T> result, BeanMeta<T> beanMeta, Map<String, Object> paraMap, FetchType fetchType) {
        return result;
    }

    /**
     * 对 {@link MapSearcher } 的检索结果做进一步转换处理
     * @param result 检索结果
     * @param beanMeta 检索实体类的元信息
     * @param paraMap 检索参数
     * @param fetchType 检索类型
     * @param <T> 泛型
     * @return 转换后的检索结果
     */
    default <T> SearchResult<Map<String, Object>> doMapFilter(SearchResult<Map<String, Object>> result, BeanMeta<T> beanMeta, Map<String, Object> paraMap, FetchType fetchType) {
        return result;
    }

}

BEP 56:分组动态查询条件优化

实体类:

@SearchBean(tables = "student_course sc", groupBy = "sc.course_id", autoMapTo = "sc")
public class ScoreSum {

    private long courseId;

    @DbField("sum(sc.score)")
    private long totalScore;

}

检索时

分组字段加条件,条件放到 where 子句中:

beanSearcher.searchList(ScoreSum.class, MapUtils.builder()
        .field(ScoreSum::getCourseId, 10)
        .build());

需要执行的 SQL 是:

select sc.course_id c_1, sum(sc.score) c_2 from student_course sc where sc.course_id = 10 group by sc.course_id

聚合字段加条件,条件放到 having 子句中:

beanSearcher.searchList(ScoreSum.class, MapUtils.builder()
        .field(ScoreSum::getTotalScore, 500)
        .build());

需要执行的 SQL 是:

select sc.course_id c_1, sum(sc.score) c_2 from student_course sc group by sc.course_id having c_2 = 500

使用 `SqlInterceptor` 实现 多字段 排序

v3.1.x 版本 不直接支持 多字段排序,但可以通过 SQL 拦截器扩展实现该功能,如在 SpringBoot 项目中使用 bean-searcher-boot-starter 依赖时,只需要配置一个 Bean 就好:

@Bean
public SqlInterceptor sqlInterceptor() {
    return new SqlInterceptor() {

        @Override
        public <T> SearchSql<T> intercept(SearchSql<T> searchSql, Map<String, Object> paraMap) {
            if (searchSql.isShouldQueryList()) {
                String listSql = searchSql.getListSqlString();
                // TODO: 1、从 paraMap 中取出其它排序参数
                // TODO: 2、对 listSql 的 order by 部分进行修改替换
                // TODO: 3、再把修改后的 listSql 放进 searchSql
                searchSql.setListSqlString(listSql); 
            }
            return searchSql;
        }

    };
}

一对多如何映射?

类似于以下的数据结构
public class User {

private Long id;  
private String name; 
private int age;  
private List<Role> role;  

// Getter and Setter ...

}

一点小疑问

查询性能上是否有专门优化呢?还是说保持和Join一样,并没有特别优化。

BEP 54: 注解内的 SQL 片段 支持 条件模板

目的(BeanSearcher Enhancement Proposal)

弥补 拼接参数 的不足:

  • 复杂情况下用法不太优雅
  • 存在注入风险,需要用户自己控制

参考:https://bs.zhxu.cn/guide/latest/params.html#%E6%8B%BC%E6%8E%A5%E5%8F%82%E6%95%B0

特性(用法)

当逻辑表达式 expression真值 时才 连接 yyy

@SearchBean(
    tables="xxx x <#[expression]> join yyy y on y.x_id = x.id </#>"       // 方式 1
    tables="xxx x <[expression]> join yyy y on y.x_id = x.id </>"         // 方式 2
    tables="xxx x #[expression] join yyy y on y.x_id = x.id #"            // 方式 3
    tables="xxx x #[expression] join yyy y on y.x_id = x.id [#]"          // 方式 4
    tables="xxx x <#[expression] join yyy y on y.x_id = x.id #>"          // 方式 5
    tables="xxx x <# expression ? join yyy y on y.x_id = x.id #>"         // 方式 6
)

方式 1、2、3、4、5、6,大家觉得 哪种 方式 更好一点呢?

表达式 expression 组成 与 真值 判断

  • 检索参数(形如 :name,参与逻辑计算)
  • 实体属性名(判断规则:是否在 select 列表 或 where 条件中存在)
  • 关系运算符:== != < > <= >=
  • 布尔运算符:或 |&!

例如:

  • :useYyy - 当检索参数中有 useYyy 且值 非空 非null 非 0 时 为真
  • :type == 1 - 当检索参数中有 type 且值 等于 1"1" 时为真
  • :foo == 'bar' - 当检索参数中有 foo 且值 等于 bar 时为真
  • :foo > :bar - 当检索参数中有 foobar 且值 foo 大于 bar 时为真
  • yId - 当 select 列表 或 where 条件中有 yId 为真
  • yId | yName - 当 select 列表 或 where 条件中有 yIdyName 时 为真
  • :useYyy & (yId | yName) - 当检索参数中有 useYyy 且值 非空 非null 非 0 且 当 select 列表 或 where 条件中有 yIdyName 时为真

欢迎讨论

大家觉得 哪种 方式 更简单 更可读一点呢(或者你是否有更好的语法),说说你的理由吧 ^_^

v3.7 新增 `OrLike` 运算符

用法(可以接收多个字段值):

Map<String, Object> params = MapUtils.builder()
    .field(User::getName, "张%", "李%", "王%").op(OrLike.class)
    .build();
List<User> users = beanSearcher.searchList(User.class, params);

生成的 SQL:

select * from user where (name like ? or name like ? or name like ?)
-- 参数:'张%', '李%', '王%'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.