用neo4j构建Twitter克隆:第二部分


一定要退房Part I先!

Image title

我喜欢的工作之一就是proof of concept boot camps这需要我(或我的一个团队成员)到现场与你的团队一起工作,在一周内完成一个POC。它们都有些不同,但我试着坚持一个适合我的公式。第一天我和整个团队一起熨模型。这是最困难的部分,因为如果模型是正确的,查询就会恰到好处。如果模型必须在第3天进行重大更改,那么大量的工作必须重做,或者至少进行重大修改。第一天结束时的目标是拥有如下内容:

这是模型,我将使用这个PoC。至少在这一点上,一路上可能会有一些变化。在那个模型中隐藏着大量的讨论和替代方案。如果您希望看到模型出现的示例,请查看我的modeling airline flights blog post。为什么我选择按日期拆分职位关系,而不是以下关系?要回答这个问题,重要的是既要看看我们所掌握的数据,也要看看我们将要提出的问题。

我们很幸运,我们已经有点了解Twitter的工作原理了,所以我们不会去开发一个全新的应用程序。我们可以有些肯定地说,为快速阅读时间轴而进行优化比快速获得最近的追随者列表更重要。

我们在Twitter上有两种类型的用户。最多也就几千名用户关注和被关注的普通用户……还有那些拥有数百万追随者的名人。对于这两种类型的用户来说,获得他们的时间线都是非常重要的。我们可以通过一个关系类型从用户遍历到他们所关注的用户,然后遍历到当天的帖子。如果我们把下面的关系分开,我们每次都得更加努力地寻找所有我们跟踪的人--而且随着时间的推移,这种工作会变得更糟。

拥有单一跟随关系还使我们能够使用getDegree用于输入和输出方向。推特上的名人不太关心谁追随他们,而是非常关心追随者的数量作为衡量他们受欢迎程度的标准。我们可以做的一件事,以安抚偶尔的名人好奇心,看看谁跟随他们,是保持跟踪的最后几个追随者在一个阵列和抓直接他们,而不是遵循以下关系。我们将认为这是一个优化,并排除它从这个POC现在。

一旦我们有了我们的模型,接下来我喜欢做的是构建一个API。在一个典型的POC新兵训练营中,我们可能会被要求解决 a single very complex or time sensitive traversal,或者我们可能会被要求帮助建立一个最低可行的产品。在那么短的时间内,我们真正能做的最多就是20个左右的HTTP端点。我喜欢HTTP API方法,因为它允许Neo4j作为一个服务而不仅仅是一个数据库。一种直接插入任何现代体系结构的服务,使用标准HTTP在服务之间进行通信。不管客户机是Java,。NET,Ruby还是任何其他语言,它们都有健壮成熟的HTTP库。对于这个POC,我想出的HTTP API如下所示:

:GET    /v1/users/{username}   
:GET    /v1/users/{username}/profile   
:POST   /v1/users {username:'', password:'', email:'', name:''}
:GET    /v1/users/{username}/followers
:GET    /v1/users/{username}/following
:POST   /v1/users/{username}/follows/{username2}
:DELETE /v1/users/{username}/follows/{username2}
:GET    /v1/users/{username}/posts
:POST   /v1/users/{username}/posts {status:''}
:POST   /v1/users/{username}/posts/{username2}/{time} 
:GET    /v1/users/{username}/likes
:POST   /v1/users/{username}/likes/{username2}/{time}
:DELETE /v1/users/{username}/likes/{username2}/{time}
:GET    /v1/users/{username}/blocks
:POST   /v1/users/{username}/blocks/{username2}
:DELETE /v1/users/{username}/blocks/{username2}
:GET    /v1/users/{username}/mentions
:GET    /v1/users/{username}/timeline
:GET    /v1/users/{username}/recommendations/friends
:GET    /v1/users/{username}/recommendations/follows
:GET    /v1/tags/{tag}

我喜欢HTTP API方法,因为如果出于某种原因,Neo4j不是合适的,至少客户有API来显示他们可以尝试用其他技术实现它。我在Part I仅此而已the source code已经可用了。因此,虽然我可以剪切和粘贴块来解释我所做的,但为了清楚起见,您也可以遵循完整的源代码。请记住,随着我们深入本系列,其中一些内容可能会发生变化。

我们开始吧。向上旋转IntelliJ并创建一个new project using Maven。您将希望编辑您的pom.xml file看起来像存储库中的一个,它引入了所需的Neo4j依赖项。我正在插入CodeshipCoveralls让我保持诚实。

代码处理器负责我们的构建,当测试失败时会向我们抱怨。

工作服确保我真的写测试。像我们许多来自Ruby on Rails社区的人一样,我所做的任何测试都很大程度上归功于Gregg PollackJason Seifer。我只想对你说声谢谢并表达我的敬意。

让我们从我们的第一堂课--用户开始。应用程序将希望对用户进行身份验证,因此让我们编写我们的createUser方法。这将是一个带有JSON有效负载的POST操作调用,我们需要验证它。假设它是好的,我们将看看用户名或电子邮件是否已经采取。接下来,我们将创建该节点,并从JSON负载中为其分配属性,同时添加它们的小写电子邮件地址的MD5散列属性。我知道MD5作为加密函数已经过时了,但是它被Gravatar为我们的用户提供形象服务。

@Path("/users")
public class Users {
 
    private static final ObjectMapper objectMapper = new ObjectMapper();
    @POST
    public Response createUser(String body, @Context GraphDatabaseService db) throws IOException {
        HashMap parameters = UserValidator.validate(body);
        Map<String, Object> results;
        try (Transaction tx = db.beginTx()) {
            Node user = db.findNode(Labels.User, USERNAME, parameters.get(USERNAME));
            if (user == null) {
                user = db.findNode(Labels.User, EMAIL, parameters.get(EMAIL));
                if (user == null) {
                    user = db.createNode(Labels.User);
                    user.setProperty(EMAIL, parameters.get(EMAIL));
                    user.setProperty(NAME, parameters.get(NAME));
                    user.setProperty(USERNAME, parameters.get(USERNAME));
                    user.setProperty(PASSWORD, parameters.get(PASSWORD));
                    user.setProperty(HASH, new Md5Hash(((String)parameters.get(EMAIL)).toLowerCase()).toString());
                    results = user.getAllProperties();
                } else {
                    throw UserExceptions.existingEmailParameter;
                }
            } else {
                throw UserExceptions.existingUsernameParameter;
            }
            tx.success();
        }
        return Response.ok().entity(objectMapper.writeValueAsString(results)).build();
    }

这里没什么太疯狂的。获取我们的用户也非常简单。这个findUser方法要么按用户名查找用户,要么引发异常。一旦找到用户,我们只想发回他们的所有属性,包括他们的密码。

@GET
@Path("/{username}")
public Response getUser(@PathParam("username") final String username, @Context GraphDatabaseService db) throws IOException {
    Map<String, Object> results;
    try (Transaction tx = db.beginTx()) {
        Node user = findUser(username, db);
        results = user.getAllProperties();
        tx.success();
    }
    return Response.ok().entity(objectMapper.writeValueAsString(results)).build();
}

我们需要一种不同的方法来提供关于用户的公共信息。那是哪里getProfile进来。在getProfile方法中,我们将遵循与getUser完全相同的步骤,但不是返回所有属性,而是删除EMAIL和PASSWORD,并计算有关它们之间关系的一些统计信息。那些node::getDegree运算非常快,因为Neo4j在添加关系时预先计算并存储答案,而不是每次都计算。唯一棘手的是“posts”,因为它们是“日期关系类型”,但是我们可以使用其他关系类型的程度来计算正确的答案。

public static Map<String, Object> getUserAttributes(Node user) {
    Map<String, Object> results;
    results = user.getAllProperties();
    results.remove(EMAIL);
    results.remove(PASSWORD);
    Integer following = user.getDegree(RelationshipTypes.FOLLOWS, Direction.OUTGOING);
    Integer followers = user.getDegree(RelationshipTypes.FOLLOWS, Direction.INCOMING);
    Integer likes = user.getDegree(RelationshipTypes.LIKES, Direction.OUTGOING);
    Integer posts = user.getDegree(Direction.OUTGOING) - following - likes;
    results.put("following", following);
    results.put("followers", followers);
    results.put("likes", likes);
    results.put("posts", posts);
    return results;
}

我们的一部分CreateUserTest可以在下面看到。我们解析URI并发送一个JSON有效负载作为输入,以确保实际结果与预期结果相匹配。哈希字符串是唯一的惊喜,因为它是在创建用户时添加的。我们不会以纯文本存储密码。我们的前端应用程序将向我们发送加密字符串,这些字符串看起来更像$2A$10$8ZnG1WQExOY9T/5AET62LUHNO5TV.DXTN3T2UAOA5T5GIXLOVNKW,所以别担心。但我太超前了。

public class CreateUserTest {
    @Rule
    public Neo4jRule neo4j = new Neo4jRule()
            .withExtension("/v1", Users.class);
 
    @Test
    public void shouldCreateUser() {
        HTTP.POST(neo4j.httpURI().resolve("/v1/schema/create").toString());
 
        HTTP.Response response = HTTP.POST(neo4j.httpURI().resolve("/v1/users").toString(), input);
        HashMap actual  = response.content();
        Assert.assertEquals(expected, actual);
    }
    private static final HashMap input = new HashMap<String, Object>() {{
        put("username", "maxdemarzi");
        put("email", "maxdemarzi@hotmail.com");
        put("name", "Max De Marzi");
        put("password", "swordfish");
    }};
 
    private static final HashMap expected = new HashMap<String, Object>() {{
        put("username", "maxdemarzi");
        put("email", "maxdemarzi@hotmail.com");
        put("name", "Max De Marzi");
        put("password", "swordfish");
        put("hash","58750f2179edbd650b471280aa66fee5");
    }};


我们今天将称之为“好”,但请继续关注下一次的更多内容,因为我们将添加以下功能。