Springboot+Docker+Neo4j实现组织树

组织树是一个常见的数据结构,通常处理方法有用关系型数据库的二维表变相处理的,也有用LDAP数据库处理的,在这里就不过多解释,各有利弊。特别是通过关系型数据库变相处理树状数据结构从思维模式到编程进而到效率上都存在先天问题,如果遇到数十万或者更多的节点的时候处理起来就更麻烦,这个是因为树状数据结构和二维表数据结构的巨大差异造成的。那么针对树状数据结构就应该用树的算法解决,目前有很多成熟的基于图的数据库具备处理各种复杂结构图的优势,而树是一种最简单的图。今天就介绍开源界最为流行的图数据库Neo4j。

Spring 访问Neo4j整体架构

Neo4j架构

备注:Neo4j数据库提供3种协议对外提供访问接口:bolt、http、java嵌入客户端。

以docker方式部署Neo4j数据库

  1. 从docker镜像仓库拉去镜像,执行下面命令
docker pull neo4j
  1. 运行neo4j,执行如下命令
 docker run -d -p 7473:7473 -p 7687:7687 -p 7474:7474 neo4j

其中3个端口是通过不同协议对外提供访问的。

springboot开发相关功能

  1. 依赖包引入
    neo4j已经很好的支持springboot特性,只需要引入starter包即可,这里要特别说明的是,因为对接neo4j的协议是可选择的,所以要根据所选择的协议引入相应的驱动包(driver)。
    <dependencies>
        <!-- 引入图数据库neo4j依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-neo4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.neo4j</groupId>
            <artifactId>neo4j-ogm-http-driver</artifactId>
        </dependency>
    </dependencies>
  1. 配置数据库连接
    neo4j的数据库连接配置非常简单,和其他关系数据库配置方法类似,只需要把配置属性写入到application.yml中即可。
#neo4j配置
spring:
  data:
    neo4j:
      uri: http://172.16.15.239:7474
      username: neo4j
      password: syswinneo4j
工程结构
  1. 编写节点数据结构Bean
    这个Bean是neo4j节点数据结构,该数据结构包含三个部分:
    3.1 节点ID,这个属性是必须的,是节点在neo4j中唯一的标识;
    3.2 节点数据属性,这些部分是节点本身的一些属性定义,可以是java的所有可识别类型,包括复杂类型;
    3.3 子节点,这些子节点就是要和该数据节点产生关联关系的数据节点,可以是一种,也可以是多种,可以是一种关系,也可以是多种关系。
    3.4 典型组织树的4中节点数据结构:
    3.4.1 组织节点
//定义组织节点数据结构
@Data
@NodeEntity(label = "Organization")
public class Organization {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @Relationship(type = "group", direction = Relationship.OUTGOING)
    private List<Group> groups=new ArrayList<>();

    public void addGroup(Group node) {
        this.groups.add(node);
    }

}

3.4.2 集团节点

//定义集团节点数据结构
@Data
@NodeEntity(label = "Group")
public class Group {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @Relationship(type = "department", direction = Relationship.OUTGOING)
    private List<Department> departments=new ArrayList<>();

    public void addDepartment(Department node) {
        this.departments.add(node);
    }

}

3.4.3 部门节点

//定义部门节点数据结构
@Data
@NodeEntity(label = "Department")
public class Department {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @Relationship(type = "department", direction = Relationship.OUTGOING)
    private List<Department> departments=new ArrayList<>();
    @Relationship(type = "employee", direction = Relationship.OUTGOING)
    private List<Employee> employees=new ArrayList<>();

    public void addDepartment(Department node) {
        this.departments.add(node);
    }

    public void addEmployee(Employee node) {
        this.employees.add(node);
    }
}

3.4.4 员工节点

//定义员工节点数据结构
@Data
@NodeEntity(label = "Employee")
public class Employee {
    @Id
    @GeneratedValue
    private Long id;
    private String userId;
    private String name;
}
  1. 编写Repository
    repository是扩展了Neo4jRepository的接口,是对neo4j数据表进行操作的统一入口,Neo4jRepository接口已经定义了绝大多数的CURD操作,如果不满足需求,可以扩展功能。下面是对照上面定义的四种节点数据操作的repository接口定义。
    4.1 组织节点数据操作接口
@Repository
public interface OrganizationRepository extends Neo4jRepository<Organization, Long> {
    Organization findByName(@Param("name") String name);
}

4.2 集团节点数据操作接口

@Repository
public interface GroupRepository extends Neo4jRepository<Group, Long> {
    Group findByName(@Param("name") String name);
}

4.3 部门节点数据操作接口

@Repository
public interface DepartmentRepository extends Neo4jRepository<Department, Long> {
    Department findByName(@Param("name") String name);
}

4.4 员工节点数据操作接口

@Repository
public interface EmployeeRepository extends Neo4jRepository<Employee, Long> {
    Employee findByName(@Param("name") String name);
}
  1. 实现neo4j数据库逻辑的服务
    复杂的业务逻辑以服务的方式实现,通过组合调用上面定义的接口方法来完成。
@Service
@Slf4j
public class KernalService {
    @Autowired
    private OrganizationRepository organizationRepository;
    @Autowired
    private GroupRepository groupRepository;
    @Autowired
    private DepartmentRepository departmentRepository;
    @Autowired
    private EmployeeRepository employeeRepository;

    public Long createOrg(String name){
        //存储节点
        Organization organization=new Organization();
        organization.setName(name);
        organizationRepository.save(organization);
        return organization.getId();
    }

    public Long createGroup(Long id,String name){
        //存储节点
        Group group=new Group();
        group.setName(name);
        groupRepository.save(group);

        Optional<Organization> organization=organizationRepository.findById(id);
        if(organization.get().getId()!=id){
            organization.get().addGroup(group);
            organizationRepository.save(organization.get());
            return group.getId();
        }
        return -1L;
    }

    public Long createDep(Long id,String name){
        //存储节点
        Department department=new Department();
        department.setName(name);
        departmentRepository.save(department);

        Optional<Group> group=groupRepository.findById(id);
        if(group.get().getId()!=id){
            group.get().addDepartment(department);
            groupRepository.save(group.get());
            return department.getId();
        }
        return -1L;
    }

    public Long createEmployee(Long id,String userId,String name){
        //存储节点
        Employee employee=new Employee();
        employee.setUserId(userId);
        employee.setName(name);
        employeeRepository.save(employee);

        Optional<Department> parentDepartment=departmentRepository.findById(id);
        if(parentDepartment.get().getId()!=id){
            parentDepartment.get().addEmployee(employee);
            departmentRepository.save(parentDepartment.get());
            return employee.getId();
        }
        return -1L;
    }
}
  1. 编写RestController以web的方式对外提供服务
@Slf4j
@RestController
public class Controller {
    @Autowired
    private KernalService kernalService;

    @PostMapping(value = "/createOrg")
    @ApiOperation(value = "创建组织", notes = "用户中台组织创建组织")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "orgName", value = "组织名称", dataType = "String")
    })
    public ResultData createOrg(@RequestParam String orgName) {
        ResultData result=new ResultData();
        Long orgId=kernalService.createOrg(orgName);
        if(orgId>0) {
            result.setRetCode(0);
            result.setRetMessage("创建组织成功!");
            JSONObject data = new JSONObject();
            data.put("orgId", orgId);
            data.put("orgName", orgName);
            result.setData(data);
        }
        else {
            result.setRetCode(-100);
            result.setRetMessage("创建组织失败!");
        }
        return result;
    }

    @PostMapping(value = "/createGroup")
    @ApiOperation(value = "创建集团", notes = "用户中台组织创建集团")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "id", value = "组织ID", dataType = "Long"),
            @ApiImplicitParam(paramType = "query", name = "groupName", value = "集团名称", dataType = "String")
    })
    public ResultData createGroup(@RequestParam Long id,@RequestParam String groupName) {
        ResultData result=new ResultData();
        Long groupId=kernalService.createGroup(id,groupName);
        if(groupId>0) {
            result.setRetCode(0);
            result.setRetMessage("创建集团成功!");
            JSONObject data = new JSONObject();
            data.put("groupId", groupId);
            data.put("groupName", groupName);
            result.setData(data);
        }
        else {
            result.setRetCode(-100);
            result.setRetMessage("创建集团失败!");
        }
        return result;
    }

    @PostMapping(value = "/createDep")
    @ApiOperation(value = "创建部门", notes = "用户中台组织创建部门")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "id", value = "组织/上级部门ID", dataType = "Long"),
            @ApiImplicitParam(paramType = "query", name = "depName", value = "部门名称", dataType = "String")
    })
    public ResultData createDepartment(@RequestParam Long id,@RequestParam String depName) {
        ResultData result=new ResultData();
        Long depId=kernalService.createDep(id,depName);
        if(depId>0) {
            result.setRetCode(0);
            result.setRetMessage("创建部门成功!");
            JSONObject data = new JSONObject();
            data.put("depId", depId);
            data.put("depName", depName);
            result.setData(data);
        }
        else {
            result.setRetCode(-100);
            result.setRetMessage("创建部门失败!");
        }
        return result;
    }

    @PostMapping(value = "/createEmployee")
    @ApiOperation(value = "创建员工", notes = "用户中台组织创建员工")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "id", value = "所属部门ID", dataType = "Long"),
            @ApiImplicitParam(paramType = "query", name = "userId", value = "用户ID", dataType = "String"),
            @ApiImplicitParam(paramType = "query", name = "userName", value = "员工名称", dataType = "String")
    })
    public ResultData createDepartment(@RequestParam Long id,@RequestParam String userId,@RequestParam String userName) {
        ResultData result=new ResultData();
        Long employeeId=kernalService.createEmployee(id,userId,userName);
        if(employeeId>0) {
            result.setRetCode(0);
            result.setRetMessage("创建员工成功!");
            JSONObject data = new JSONObject();
            data.put("employeeId", employeeId);
            data.put("userId", userId);
            data.put("userName", userName);
            result.setData(data);
        }
        else {
            result.setRetCode(-100);
            result.setRetMessage("创建员工失败!");
        }
        return result;
    }

    @PostMapping(value = "/register")
    @ApiOperation(value = "注册", notes = "用户中台组织用户注册")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "userId", value = "基础用户ID", dataType = "String"),
            @ApiImplicitParam(paramType = "query", name = "orgId", value = "组织用户ID", dataType = "String"),
            @ApiImplicitParam(paramType = "query", name = "departmentId", value = "部门ID", dataType = "String")
    })
    public ResultData register(@RequestParam String userId, @RequestParam String orgId, @RequestParam String departmentId) {
        ResultData result=new ResultData();
        result.setRetCode(0);
        result.setRetMessage("注册成功!");
        JSONObject data=new JSONObject();
        data.put("id", userId);
        data.put("orgId",orgId);
        data.put("departmentId", departmentId);
        result.setData(data);
        return result;
    }
}
  1. 测试验证功能及性能
    这里为了验证neo4j的性能,批量创建了40万个节点,40万个管理关系,在300s内全部执行完成,整体性能方面非常优秀。
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
    static final int groupNumber=20;
    static final int departmentNumber=100;
    static final int employeeNumber=200;

    @Autowired
    private OrganizationRepository organizationRepository;
    @Autowired
    private GroupRepository groupRepository;
    @Autowired
    private DepartmentRepository departmentRepository;
    @Autowired
    private EmployeeRepository employeeRepository;

    @Test
    public void contextLoads() {
    }

    @Test
    public void createOrgTest(){
        //存储节点
        Organization organization=new Organization();
        organization.setName("控股");
        organizationRepository.save(organization);

        List<Group> groups=new ArrayList<>();
        for(int i=0;i<groupNumber;i++) {
            Group group=new Group();
            group.setName("集团"+"—"+i);
            groups.add(group);
        }
        groupRepository.saveAll(groups);

        List<Department> departments=new ArrayList<>();
        for(int i=0;i<groupNumber;i++) {
            for (int j=0;j<departmentNumber;j++) {
                Department department=new Department();
                department.setName("部门" + "-"+i + "-"+j);
                departments.add(department);
            }
        }
        departmentRepository.saveAll(departments);

        List<Employee> employees=new ArrayList<>();
        for(int i=0;i<groupNumber;i++) {
            for (int j=0;j<departmentNumber;j++) {
                for (int k = 0; k < employeeNumber; k++) {
                    Employee employee=new Employee();
                    employee.setName("员工" + "-"+i + "-"+j + "-"+k);
                    employees.add(employee);
                }
            }
        }
        employeeRepository.saveAll(employees);
        //建立关系
        groups.forEach(v->{
            organization.addGroup(v);
        });
        organizationRepository.save(organization);
        log.info("组织创建集团关系完成!");

        int countOrganization=0;
        int countDepartment=0;
        int countEmployee=0;

        for (int i = 0; i < groupNumber; i++) {
            for (int j = 0; j < departmentNumber; j++) {
                groups.get(countOrganization).addDepartment(departments.get(countDepartment++));
            }
            groupRepository.save(groups.get(countOrganization++));
        }
        log.info("集团创建部门关系完成!");

        countDepartment=0;
        for (int i = 0; i < groupNumber; i++) {
            for (int j = 0; j < departmentNumber; j++) {
                for (int k = 0; k < employeeNumber; k++) {
                    departments.get(countDepartment).addEmployee(employees.get(countEmployee++));
                }
                departmentRepository.save(departments.get(countDepartment++));
            }
        }
        log.info("部门创建员工关系完成!");

    }

    @Test
    public void cleanDataTest(){
        organizationRepository.deleteAll();
        groupRepository.deleteAll();
        departmentRepository.deleteAll();
        employeeRepository.deleteAll();
    }

}
2019-11-26 15:42:36.444  INFO 98261 --- [           main] c.s.p.middle.user.org.ApplicationTests   : 组织创建集团关系完成!
2019-11-26 15:42:37.681  INFO 98261 --- [           main] c.s.p.middle.user.org.ApplicationTests   : 集团创建部门关系完成!
2019-11-26 15:45:24.000  INFO 98261 --- [           main] c.s.p.middle.user.org.ApplicationTests   : 部门创建员工关系完成!
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 312.644 sec

图形化界面展示neo4j数据

neo4j本身提供了较为完善的图形化管理系统,方便对neo4j数据库进行管理和维护,相关功能不再赘述,可以通过浏览器打开。


neo4j图书化管理
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容