存储库和工作单元
仓储和工作单元模式
仓储模式
为什么要用仓储模式
通常不建议在业务逻辑层直接访问数据库。因为这样可能会导致如下结果:
- 重复的代码
- 编程错误的可能性更高
- 业务数据的弱类型
- 更难集中处理数据,比如缓存
- 无法轻松地从外部依赖项测试业务逻辑
在业务逻辑层通过仓库模式访问数据则可以实现如下特点:
- 最大化可以用自动化测试的代码量,并隔离数据层以支持单元测试。
- 对数据集中管理、提供一致的访问规则和逻辑。
- 通过将业务逻辑与数据或服务访问逻辑分隔开,从而提高代码的可维护性和可读性。
- 使用强类型的
Entity
以便在编译时识别问题而不是在运行时
实现仓储模式
使用仓储模式是为了分离业务层和数据源层,并实现业务层的Model和数据源层的Model映射。(ViewModel和Entity之间的映射)。即业务逻辑层应该和数据源层无关,业务层只关心结果,数据源层关心细节。
数据源层和业务层之间的分离有三个好处:
- 集中了数据逻辑或Web服务访问逻辑。
- 为单元测试提供了一个替代点。
- 提供了一种灵活的体系结构,可以作为应用程序的整体设计进行调整。
一、定义仓储接口
所有的仓储要实现该接口。该接口定义了对数据的基本操作。
public interface IRepository<TEntity> where TEntity : class
{
#region 属性
//IQueryable Entities { get; }
#endregion
#region 公共方法
void Insert(TEntity entity);
void Insert(IEnumerable<TEntity> entities);
void Delete(object id);
void Delete(TEntity entity);
void Delete(IEnumerable<TEntity> entities);
void Update(TEntity entity);
TEntity GetByKey(object key);
#endregion
}
二、实现泛型仓储基类
该类为仓储的泛型基类,实现之前定义的仓储接口(IRepository
每个表都会对应一个实体(Entity)。每个实体(Entity)对应一个仓储。把实体作为泛型仓储基类的参数,来实现每个实体对应的仓储。
(使用泛型仓储基类可以把实体作为泛型参数来创建对应的仓储。)
//泛型仓储基类
public class EFBaseRepository<TEntity> : IRepository<TEntity> where TEntity : class
{
//数据上下文
internal DbContext context;
//数据集
internal DbSet<TEntity> dbSet;
public EFBaseRepository(DbContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
//public IQueryable Entities => context.Set<TEntity>();
public void Delete(object id)
{
TEntity entityToDelete = dbSet.Find(id);
Delete(entityToDelete);
}
public void Delete(IEnumerable<TEntity> entities)
{
dbSet.RemoveRange(entities);
}
public void Delete(TEntity entityToDelete)
{
if (context.Entry(entityToDelete).State == EntityState.Detached)
{
dbSet.Attach(entityToDelete);
}
dbSet.Remove(entityToDelete);
}
public TEntity GetByKey(object key)
{
return dbSet.Find(key);
}
public void Insert(TEntity entity)
{
dbSet.Add(entity);
}
public void Insert(IEnumerable<TEntity> entities)
{
dbSet.AddRange(entities);
}
public void Update(TEntity entity)
{
dbSet.Attach(entity);
context.Entry(entity).State = EntityState.Modified;
}
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "", int topNum = 0)
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
query = orderBy(query);
}
if (topNum != 0)
{
return query.Take(topNum);
}
else
{
return query.ToList();
}
}
}
三、访问数据
可以把对Person的相关操作封装到一个类中。在该类中实现PersonRepository(Person仓储),操作PersonRepository来操作数据。
(数据库有一个Person表,代码中有一个TPerson实体)
(该类提供与业务逻辑无关的仓储操作)
public class PersonService
{
private EFBaseRepository<TPerson> _personRepository;
public PersonService(DbContext dbContext)
{
var context = dbContext;
//实现Person仓储,TPerson为对应的Entity
_personRepository = new EFBaseRepository<TPerson>(context);
}
public IEnumerable<TPerson> Get()
{
return _personRepository.Get();
}
public bool AddPerson(TPerson p)
{
try
{
_personRepository.Insert(p);
}
catch (Exception ex)
{
return false;
}
return true;
}
public bool EditPerson(TPerson p)
{
try
{
_personRepository.Update(p);
}
catch (Exception ex)
{
return false;
}
return true;
}
public bool DeletePerson(TPerson p)
{
try
{
_personRepository.Delete(p);
}
catch (Exception)
{
return false;
}
return true;
}
}
四、ViewModel和Entity的映射
该类是对PersonService的封装,是为了提供同一数据上下文,和对数据上下文的释放,及ViewModle和Entity的映射。
该类中每个方法对应一个数据上下文。如果有需要对多个表操作,将这些操作封装到一个数据上下文中。数据上下文的释放在每个方法中实现。
(所有与业务逻辑相关的操作在该类实现)
public class PersonManage
{
public IList<PersonVM> GetPersons()
{
using (var context = new RepositoryDemoEntities())
{
var list = new PersonService(context).Get();
var result = new List<PersonVM>();
foreach (var item in list)
{
result.Add(new PersonVM { Name = item.Name, Age = item.Age, Home = item.Home, PersonID = item.Id });
}
return result;
}
}
public bool AddPerson(PersonVM p)
{
using (var context = new RepositoryDemoEntities())
{
var result = new PersonService(context).AddPerson(new EntityFramework.TPerson { Name = p.Name, Home = p.Home, Age = p.Age, Id = p.PersonID });
context.SaveChanges();
return result;
}
}
public bool DeletePerson(PersonVM p)
{
using (var context = new RepositoryDemoEntities())
{
var result = new PersonService(context).DeletePerson(new EntityFramework.TPerson { Name = p.Name, Home = p.Home, Age = p.Age, Id = p.PersonID });
context.SaveChanges();
return result;
}
}
public bool EditPerson(PersonVM p)
{
using (var context = new RepositoryDemoEntities())
{
var result = new PersonService(context).EditPerson(new EntityFramework.TPerson { Name = p.Name, Home = p.Home, Age = p.Age, Id = p.PersonID });
context.SaveChanges();
return result;
}
}
}
五、在Test中测试
仓储模式使得更容易实现单元测试
- 添加项目引用
- 设置数据库连接字符串
- 添加
EntityFramework
包即可对每个方法测试
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestShowPerson()
{
var res = new PersonManage().GetPersons();
Assert.AreNotEqual(0, res.Count);
}
[TestMethod]
public void TestAddPerson()
{
var p = new PersonVM { Home = "zhengzhou", Age = 22, Name = "Jessica", PersonID = 3 };
var res = new PersonManage().AddPerson(p);
Assert.IsTrue(res);
}
[TestMethod]
public void TestEditPerson()
{
var persons = new PersonManage().GetPersons();
var p = persons[0];
p.Name = "fixed";
var res = new PersonManage().EditPerson(p);
Assert.IsTrue(res);
}
[TestMethod]
public void TestDeletePerson()
{
var persons = new PersonManage().GetPersons();
var p = persons[0];
var res = new PersonManage().DeletePerson(p);
Assert.IsTrue(res);
}
}
小结:
仓储模式通过对数据库操作的封装使数据访问有一致性和对应用层和数据层的隔离,降低代码的耦合性,更加容易实现单元测试。
工作单元模式
工作单元模式是“维护一个被业务事务影响的对象列表,协调变化的写入和并发问题的解决”
比如:新入校一个同学,需要在班级,学校,学生,课程等多个表里同时操作。这些表要么都完成,要么都不完成。具有一致性。
在仓储模式中使用工作单元模式是为了当你操作多个仓储时,共用一个数据上下文(DbContext)使得这些仓储具有一致性。
在Entity Framework中可以把DbContext当作是一个工作单元。在同一个DbContext对多个仓储操作。所以工作单元模式并不是一定要自己实现,通过Entity Framework也可以实现。
上面的仓储模式其实通过对DbContext的使用了也实现了工作单元模式。
还是简单说下如何实现自定义的工作单元 (如果要对每个操作都产生记录的话,可以扩展自定义工作单元来实现)
自定义工作单元
一、定义IUnitOfWork接口
/// <summary>
/// 工作单元接口
/// </summary>
public interface IUnitOfWork
{
/// <summary>
/// 保存当前单元操作的结果
/// </summary>
/// <returns></returns>
void Save();
}
二、定义UnitOfWork类
UnitOfWork包含了所有的仓储,及一个数据上下文,该类实现IDisposable接口(该接口的方法中释放数据上下文)。
public class UnitOfWork : IUnitOfWork, IDisposable
{
private RepositoryDemoEntities1 context = new RepositoryDemoEntities1();
private EFBaseRepository<TPerson> _personRepository;
public EFBaseRepository<TPerson> PersonRepository
{
get
{
return _personRepository ?? new EFBaseRepository<TPerson>(context);
}
}
public void Save()
{
context.SaveChanges();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
三、实现UnitOfWork实例。通过该实例访问仓储。
定义一个UnitOfWork的字段,通过构造函数实例化该UnitOfWork
(该类提供与业务逻辑无关的仓储操作)
public class PersonService
{
private UnitOfWork unit;
public PersonService(UnitOfWork unitOfWork)
{
unit = unitOfWork;
}
public IEnumerable<TPerson> Get()
{
return unit.PersonRepository.Get();
}
public bool AddPerson(TPerson p)
{
try
{
unit.PersonRepository.Insert(p);
}
catch (Exception ex)
{
return false;
}
return true;
}
public bool EditPerson(TPerson p)
{
try
{
unit.PersonRepository.Update(p);
}
catch (Exception ex)
{
return false;
}
return true;
}
public bool DeletePerson(TPerson p)
{
try
{
unit.PersonRepository.Delete(p);
}
catch (Exception)
{
return false;
}
return true;
}
}
四、通过工作单元,保持操作一致性,手动释放数据上下文
在此将PersonService封装,如果有对多个仓储的操作,封装在一个工作单元中。
(所有与业务逻辑相关的操作在该类实现)
public class PersonManage
{
public IList<PersonVM> GetPersons()
{
using (var unit = new UnitOfWork())
{
var list = new PersonService(unit).Get();
var result = new List<PersonVM>();
foreach (var item in list)
{
result.Add(new PersonVM { Name = item.Name, Age = item.Age, Home = item.Home, PersonID = item.Id });
}
return result;
}
}
public bool AddPerson(PersonVM p)
{
using (var unit = new UnitOfWork())
{
var result = new PersonService(unit).AddPerson(new EntityFramework.TPerson { Name = p.Name, Home = p.Home, Age = p.Age, Id = p.PersonID });
unit.Save();
return result;
}
}
public bool DeletePerson(PersonVM p)
{
using (var unit = new UnitOfWork())
{
var result = new PersonService(unit).DeletePerson(new EntityFramework.TPerson { Name = p.Name, Home = p.Home, Age = p.Age, Id = p.PersonID });
unit.Save();
return result;
}
}
public bool EditPerson(PersonVM p)
{
using (var unit = new UnitOfWork())
{
var result = new PersonService(unit).EditPerson(new EntityFramework.TPerson { Name = p.Name, Home = p.Home, Age = p.Age, Id = p.PersonID });
unit.Save();
return result;
}
}
}
五、单元测试
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestShow()
{
var res = new PersonManage().GetPersons();
Console.WriteLine(res.Count);
Assert.AreNotEqual(0, res.Count);
}
[TestMethod]
public void TestAdd()
{
var res = new PersonManage().AddPerson(new PersonVM { Home = "meiguo", Age = 11, Name = "tidy" });
Assert.IsTrue(res);
}
[TestMethod]
public void TestEdit()
{
var pmanage = new PersonManage();
var p = pmanage.GetPersons()[0];
p.Name = "fixed";
var res = pmanage.EditPerson(p);
Assert.IsTrue(res);
}
[TestMethod]
public void TestDelete()
{
var pmanage = new PersonManage();
var p = pmanage.GetPersons()[0];
var res = pmanage.DeletePerson(p);
Assert.IsTrue(res);
}
}
小结:
工作单元模式是为了实现业务的事务功能。通过一个数据上下文对相关的仓储操作。但是也不是必须要自己实现模式,通过ORM也可以实现。
代码下载
如有不对,请多多指教。
下一篇: 帮助您了解计数位置的文章
推荐阅读
-
Kotlin 和 Compose 多平台跨平台(安卓、桌面)开发实践 使用 SQLDelight 将数据存储到数据库中
-
什么是数据库事物?为什么需要数据库事物,事物有哪些特征?事物的隔离级别是什么?-1.什么是数据库事务? 1.事务是作为一个逻辑单元执行的一系列操作。一个逻辑工作单元必须具备四个属性,即ACID(原子性、一致性、隔离性和持久性)属性,只有这样才能成为事务: 原子性 2.事务必须是一个原子工作单元;它的数据修改要么全部执行,要么全部不执行。 一致性 3.事务完成时,所有数据必须保持一致。在相关数据库中,所有规则都必须适用于事务的修改,以保持所有数据的完整性。事务结束时,所有内部数据结构(如 B 树索引或双向链接表)必须正确无误。 隔离 4.并发事务的修改必须与其他并发事务的修改隔离。一个事务会在另一个并发事务修改之前或之后查看某一状态下的数据,而不会查看中间状态下的数据。这就是所谓的可序列化,因为它允许重新加载起始数据和重放一系列事务,从而使数据最终处于与原始事务执行时相同的状态。 持久性 5.事务完成后,它对系统的影响是永久性的。即使在系统发生故障的情况下,修改也会保留。 2. 为什么需要数据库事物,事物有哪些特征? 事物对数据库的作用是对数据进行一系列操作,要么全部成功,要么全部失败,防止出现中间状态,确保数据库中的数据始终处于正确、和谐的状态。 特征:原子性、一致性、隔离性、持久性,以及其他特征 原子性(Atomicity):所有操作在事务开始后,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出现错误时,会回滚到事务开始前的状态,所有操作就像没有发生一样。也就是说,事务是一个不可分割的整体,就像化学中的原子一样,是物质的基本单位。 一致性(Consistency):在事务开始之前和结束之后,数据库的完整性约束都没有被破坏。例如,如果 A 转钱给 B,A 不可能扣除这笔钱,但 B 却没有收到这笔钱。 隔离:在同一时间内,只允许一个事务请求相同的数据,不同事务之间没有干扰。例如,甲正在从一张银行卡上取款,在甲取款过程结束之前,乙不能向这张卡转账。 持久性(耐用性):事务完成后,事务对数据库的所有更新都将保存到数据库中,无法回滚 3.事务的隔离级别有哪些? 数据库事务有四种隔离级别,从低到高分别是未提交读取(Read uncommitted)、已提交读取(Read committed)、可重复读取(Repeatable read)、可序列化(Serializable)。此外,事务的并发操作中可能会出现脏读、不可重复读、幽灵读等情况。事务并发问题 脏读:事务 A 读取事务 B 更新的数据,然后事务 B 回滚操作,那么事务 A 读取的数据就是脏数据。 不可重复读取:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取期间更新并提交数据,导致事务 A 多次读取同一数据时结果不一致。 幻影读取:系统管理员 A 将数据库中所有学生的具体分数改为 ABCDE 等级,但系统管理员 B 在此时插入了具体分数的记录,当系统管理员 A 更改结束后发现仍有一条记录未被更改,仿佛发生了幻觉,这称为幻影读取。 小结:不可重复读和幻读容易混淆,不可重复读侧重于修改,幻读侧重于增删。解决不可重复读问题只需锁定满足条件的行,解决幻读问题则需要锁定表 MySQL 事务隔离级别
-
数据库面试要点:关于MySQL数据库的1000万次数据查询和存储
-
linux 定时备份数据库 sql 文件(不备份表、视图、存储过程和已保存的查询语句)
-
数据库(MySQL)基础]MySQL中的视图、存储过程和触发器 - 1. 视图
-
Docker 映像创建和管理(Hub 官方存储库使用和私人注册表构建)
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为:
-
现在,dataworks 工作台中的运行中心如何看待空间资源、CPU 和内存以及存储容量?
-
基于 NFC 的无线电池管理 BMS - ● 主动读取内部传感器:利用 NFC 技术,BMS 能够主动读取内部传感器的数据 [... 考虑车辆外使用案例中的空闲状态场景:NFC 技术可用于处理闲置状态下的电池组读取,例如在第二次生命转移期间进行存储。 主动诊断读取:在邻近系统中部署了 BMS 的情况下,使用 NFC 技术进行主动诊断读取。 (ii) 系统结构 系统架构如图所示,在建立安全通道之前,需要对设备进行身份验证。数据链路通信层由 NDEF 记录处理,而数据存储可以是离线的,也可以是数据库中的在线存储。活动和空闲状态的诊断读数取决于设备和数据方向,需要与外部 NFC 阅读器进行通信。软件架构分为三层,包括硬件抽象层(HAL)、中间层(中间件)和应用层。HAL 处理硬件驱动组件,中间件执行设备验证,而应用层则由开发人员根据安全漏洞和格式扩展*定义。 为确保安全,系统采用了一个安全模型,为 BMS 和主动诊断读取情况格式化应用数据。安全考虑因素包括设备相互验证、使用安全通道(加密和防篡改)以及确保电池组内读数的安全。 考虑到不同的 BMS 拓扑,包括集中式、调制式、分布式和分散式,系统需要满足设备相互验证和使用安全通道的要求。对于每种拓扑结构,都必须考虑将性能开销降至最低。电池是封闭的,对其进行物理攻击不可行或成本太高。外部攻击可能也很困难。基于对称或非对称加密技术的自动验证可用于保护电池组读数。安全协议在验证阶段和会话密钥确认阶段采用双密钥加密,以抵御攻击。中间件在数据格式验证、确认和处理中发挥关键作用,确保数据传输安全。 (iii) 唤醒模型设计
-
列式存储相关概念和常见列式存储数据库(Hbase、Druid)