5
votes

I have a GAE project written in Java and I have some thoughts about the HRD and a problem that I'm not sure how to solve.

Basically I have users in my system. A user consists of a userid, a username, an email and a password. Each time I create a new user, I want to check that there isn't already a user with the same userid (should never happen), username or email.

The userid is the key, so I think that doing a get with this will be consistent. However, when I do a query (and use a filter) to find possible users with the same username or email, I can't be sure that the results are consistent. So if someone has created a user with the same username or email a couple of seconds ago, I might not find it with my query. I understand that ancestors are used to work around this problem, but what if I don't have an ancestor to use for the query? The user does not have a parent.

I'd be happy to hear your thoughts on this, and what is considered to be best practice in situations like these. I'm using Objectify for GAE if that changes anything.

4
I asked a question that you may find very useful. (stackoverflow.com/questions/6584435/…) I, too, needed to store unique information for my users. In my case, I needed to store both a unique email and unique User ID per user. This is a bit difficult with the HRD, but I did come to a reliable solution. ...cont... - RLH
The only issue with my situation is that my account creation implementation (see my answer) will not scale well. That was OK in my circumstance because my GAE app is very small and has a slow trickle of new users (1 or 2 a month.) Also, this information is in Python, but the code is simple-- you should be able to re-translate it to Java with relative easy. - RLH
@RLH Thank you for your input, always interesting to see different solutions, but I do not think your solution would work well in my case. - Joel

4 Answers

7
votes

I wouldn't recommend using email or any other natural key for your User entity. Users change their email addresses and you don't want to end up rewriting all the foreign key references in your database whenever someone changes their email.

Here's a short blurb on how I solve this issue:

https://groups.google.com/d/msg/google-appengine/NdUAY0crVjg/3fJX3Gn3cOYJ

Create a separate EmailLookup entity whose @Id is the normalized form of an email address (I just lowercase everything - technically incorrect but saves a lot of pain when users accidentally capitalize [email protected]). My EmailLookup looks like this:

@Entity(name="Email")
public class EmailLookup {
    /** Use this method to normalize email addresses for lookup */
    public static String normalize(String email) {
        return email.toLowerCase();
    }

    @Id String email;
    @Index long personId;

    public EmailLookup(String email, long personId) {
        this.email = normalize(email);
        this.personId = personId;
    }
}

There is also a (not-normalized) email field in my User entity, which I use when sending outbound emails (preserve case just in case it matters for someone). When someone creates an account with a particular email, I load/create the EmailLookup and the User entities by key in a XG transaction. This guarantees that any individual email address will be unique.

The same strategy applies for any other kind of unique value; facebook id, username, etc.

3
votes

A way around the HRD's eventual consistency, is to use get instead of query. To be able to do this is you need to generate natural IDs, e.g. generate IDs that consists of data you receive in request: email and username.

Since get in HRD has strong consistency, you will be able to reliably check if user already exists.

For example a readable natural ID would be:

String naturalUserId = userEmail + "-" + userName;

Note: in practice emails are unique. So this is a good natural ID on it's own. No need to add a made-up username to it.

-1
votes

You may also enable cross-group transactions (see https://developers.google.com/appengine/docs/java/datastore/overview#Cross_Group_Transactions) and then in one transaction look for the user and create a new one, if that helps.

-1
votes

Recommend avoiding an indexed field and query unless you have other uses for it. Here is what I have done before (Python) using key_name (since entity ids need to be ints). Easy to use either the key_name or id for other entities that need to link to user:

username = self.request.get('username')
usernameLower = username.lower()
rec = user.get_by_key_name(usernameLower)
if rec is None:
    U = user(
        key_name = usernameLower,
        username = username,
        etc...)
    U.put()
else:
    self.response.out.write(yourMessageHere)