只有 2000 行 Java 代码写出来的支持分库分表的 ORM 值不值得看

2018-05-03 09:43:05 +08:00
 shellquery

OrmKids

https://github.com/pyloque/ormkids

假期花了几天时间撸了一个支持分库分表的 MySQL 单表 ORM 框架,暂用于学习

功能特性

  1. 代码简洁,没有任何依赖项
  2. 易于使用,无须复杂的配置
  3. 提供自动创建表功能
  4. 支持分库又分表,可以只分库,也可以只分表
  5. 支持 groupby/having
  6. 支持原生 SQL
  7. 支持事件回调,可用于服务跟踪调试和动态 sql 改写

不支持多表关联

  1. 多表比较复杂,实现成本高,学习成本也高,容易出错
  2. 常用的多表的操作一般都可以使用多条单表操作组合实现
  3. 在分库分表的场合,很少使用多表操作
  4. 不使用外键,专注于 sql 逻辑

db.withinTx

对于复杂的多表查询和批量数据处理,可以使用该方法。 用户可以获得原生的 jdbc 链接,通过编写 jdbc 代码来实现。

Q

用户可以使用 Q 对象构建复杂的 SQL 查询

其它数据库支持

暂时没有

分库分表

@Table
public class BookShelf implements IEntity {

	public final static int PARTITIONS = 4;

	@Column(name = "user_id", type = "varchar(255)", nullable = false)
	private String userId;
	@Column(name = "book_id", type = "varchar(255)", nullable = false)
	private String bookId;
	@Column(name = "comment", type = "varchar(255)")
	private String comment;
	@Column(name = "created_at", type = "datetime", nullable = false, defaultValue = "now()")
	private Date createdAt;

	public BookShelf() {
	}

	public BookShelf(String userId, String bookId, String comment, Date createdAt) {
		this.userId = userId;
		this.bookId = bookId;
		this.comment = comment;
		this.createdAt = createdAt;
	}

	public String getUserId() {
		return userId;
	}

	public String getBookId() {
		return bookId;
	}

	public void setComment(String comment) {
		this.comment = comment;
	}

	public String getComment() {
		return comment;
	}

	public Date getCreatedAt() {
		return createdAt;
	}

	@Override
	public String table() {
		return "book_shelf";
	}

	@Override
	public TableOptions options() {
		return new TableOptions().option("engine", "innodb");
	}

	@Override
	public TableIndices indices() {
		return new TableIndices().primary("user_id", "book_id");
	}

	/* 
	 * 分表策略
	 */
	@Override
	public String suffix() {
		var crc32 = new CRC32();
		crc32.update(userId.getBytes(Utils.UTF8));
		return String.valueOf(Math.abs(crc32.getValue()) % PARTITIONS);
	}

	/**
	 * 分库策略
	 */
	public static class GridStrategy implements IGridable<BookShelf> {

		@Override
		public int select(int dbs, BookShelf t) {
			return Math.abs(t.getUserId().hashCode()) % dbs;
		}

		@Override
		public int select(int dbs, Object... params) {
			String userId = (String) params[0];
			return Math.abs(userId.hashCode()) % dbs;
		}

	}

}

定义单个数据库

public class DemoDB extends DB {

	private DataSource ds;

	public DemoDB(String name, String uri) {
		this(name, new HashMap<>(), uri);
	}

	public DemoDB(String name, Map<Class<? extends IEntity>, Meta> metas, String uri) {
		super(name, metas);
		var ds = new MysqlConnectionPoolDataSource(); // 连接池
		ds.setUrl(uri);
		this.ds = ds;
	}

	@Override
	protected Connection conn() {  // 获取链接
		try {
			return ds.getConnection();
		} catch (SQLException e) {
			throw new KidsException(e);
		}
	}

}

定义网格数据库——分库

public class GridDemoDB extends GridDB<DemoDB> {

	/**
	 * 传进来多个 DB 对象
	 */
	public GridDemoDB(DemoDB[] dbs) {
		super(dbs);
		this.registerGridables();
	}

	/* 
	 * 注册实体类的分库策略
	 */
	@Override
	public void registerGridables() {
		this.gridWith(BookShelf.class, new BookShelf.GridStrategy<DemoDB>());
	}

}

分库增删改查

public class DemoSharding {

	private static DemoDB[] dbs = new DemoDB[3];
	static {
		Map<Class<? extends IEntity>, Meta> metas = new HashMap<>();
		dbs[0] = new DemoDB("demo-0", metas,
				"jdbc:mysql://localhost:3306/mydrc?user=mydrc&password=mydrc&useUnicode=true&characterEncoding=UTF8");
		dbs[1] = new DemoDB("demo-1", metas,
				"jdbc:mysql://localhost:3307/mydrc?user=mydrc&password=mydrc&useUnicode=true&characterEncoding=UTF8");
		dbs[2] = new DemoDB("demo-2", metas,
				"jdbc:mysql://localhost:3308/mydrc?user=mydrc&password=mydrc&useUnicode=true&characterEncoding=UTF8");
	}

	public static void main(String[] args) {
		var grid = new GridDemoDB(dbs); // 构造 Grid 实例
		try {
			for (int k = 0; k < BookShelf.PARTITIONS; k++) {
				grid.create(BookShelf.class, String.valueOf(k)); // 创建所有分库中的分表
			}
			var bss = new ArrayList<BookShelf>();
			for (int i = 0; i < 100; i++) {
				var bs = new BookShelf("user" + i, "book" + i, "comment" + i, new Date());
				bss.add(bs);
				grid.insert(bs); // 插入,自动分发到相应的分库中的分表
			}
			for (int k = 0; k < grid.size(); k++) {
				for (int i = 0; i < BookShelf.PARTITIONS; i++) {
					System.out.printf("db %d partition %d count %d\n", k, i,
							grid.count(BookShelf.class, k, String.valueOf(i))); // 依次查询出所有分库的分表的行数
				}
			}
			Random random = new Random();
			for (var bs : bss) {
				bs.setComment("comment_update_" + random.nextInt(100));
				grid.update(bs); // 更新,自动分发到相应的分库中的分表
			}
			for (var bs : bss) {
				bs = grid.get(BookShelf.class, bs.getUserId(), bs.getBookId()); // 主键查询,自动分发到相应的分库中的分表
				System.out.println(bs.getComment());
			}
			for (var bs : bss) {
				grid.delete(bs); // 删除,自动分发到相应的分库中的分表
			}
			for (int k = 0; k < grid.size(); k++) {
				for (int i = 0; i < BookShelf.PARTITIONS; i++) {
					System.out.printf("db %d partition %d count %d\n", k, i,
							grid.count(BookShelf.class, k, String.valueOf(i))); // 依次查询出所有分库的分表的行数
				}
			}
		} finally {
			for (int k = 0; k < BookShelf.PARTITIONS; k++) {
				grid.drop(BookShelf.class, String.valueOf(k)); // 删除所有分库中的分表
			}
		}
	}

}

事件上下文对象

public class Context {

	private DB db; // 数据库实例
	private Connection conn;  // 当前的链接
	private Class<? extends IEntity> clazz; // 当前的实体类
	private Q q; // 查询 sql
	private Object[] values; // 查询的绑定参数
	private boolean before; // before or after
	private Exception error; // 异常
	private long duration; // 耗时 microsecond

}

事件回调,用于服务跟踪调试

public class DemoEvent {

	private final static String URI = "jdbc:mysql://localhost:3306/mydrc?user=mydrc&password=mydrc&useUnicode=true&characterEncoding=UTF8";

	public static void main(String[] args) {
		var db = new DemoDB("demo", URI);
		db.on(ctx -> { // 全局事件回调
			System.out.printf("db=%s sql=%s cost=%dus\n", ctx.db().name(), ctx.q().sql(), ctx.duration());
			return true; // 返回 false 会导致事件链终止,后续的 ORM 操作也不会执行
		});
		try {
			db.create(User.class);
			db.scope(ctx -> { // 范围回调,execute 方法内部的所有 ORM 操作都会回调
				System.out.printf("db=%s sql=%s cost=%dus\n", ctx.db().name(), ctx.q().sql(), ctx.duration());
				return true;
			}).execute(() -> {
				db.count(User.class);
				db.find(User.class);
			});
		} finally {
			db.drop(User.class); // 删除表
		}
	}

}

鉴于文章太长花费 v 币太多,更多内容请阅读 github 的 README

https://github.com/pyloque/ormkids

3265 次点击
所在节点    程序员
13 条回复
enhancer
2018-05-03 09:51:43 +08:00
支持分表之间的 JOIN 吗?
shellquery
2018-05-03 10:03:41 +08:00
@enhancer 同是天涯沦落人,何苦为难自家人
ylcc
2018-05-03 10:13:30 +08:00
看了楼主的 gayhub,觉得非常有趣,小孩有点多哈哈哈,赞学习精神
micean
2018-05-03 10:16:40 +08:00
以前也造过轮子
现在觉得小的用表分区就搞完了,大的就直接上中间件了……
shellquery
2018-05-03 10:23:46 +08:00
@micean 中间件也是一种常见的方案
admol
2018-05-03 11:31:36 +08:00
var 用的 JDK10 吗?
shellquery
2018-05-03 11:37:01 +08:00
@admol 没错,提前尝鲜了
lihongjie0209
2018-05-03 11:48:54 +08:00
同样的文章为什么昨天发今天也发
THP301
2018-05-03 11:49:11 +08:00
难得见这么简洁优雅的代码,已经收藏了
shellquery
2018-05-03 11:53:53 +08:00
@lihongjie0209 昨天的文章是依赖注入
tianzx
2018-05-03 12:16:21 +08:00
m
sethverlo
2018-05-03 12:18:38 +08:00
楼主的头像!

good news everyone (自带音效)
shellquery
2018-05-03 12:43:43 +08:00
@sethverlo 老司机

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/451636

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX