Database design - google app engine
Asked Answered
P

3

7

I am working with google app engine and using the low leval java api to access Big Table. I'm building a SAAS application with 4 layers:

  • Client web browser
  • RESTful resources layer
  • Business layer
  • Data access layer

I'm building an application to help manage my mobile auto detailing company (and others like it). I have to represent these four separate concepts, but am unsure if my current plan is a good one:

  • Appointments
  • Line Items
  • Invoices
  • Payments

Appointment: An "Appointment" is a place and time where employees are expected to be in order to deliver a service.

Line Item: A "Line Item" is a service, fee or discount and its associated information. An example of line items that might go into an appointment:

Name:                          Price: Commission: Time estimate   
Full Detail, Regular Size:        160       75       3.5 hours 
$10 Off Full Detail Coupon:       -10        0         0 hours 
Premium Detail:                   220      110       4.5 hours 
Derived totals(not a line item): $370     $185       8.0 hours

Invoice: An "Invoice" is a record of one or more line items that a customer has committed to pay for.

Payment: A "Payment" is a record of what payments have come in.

In a previous implementation of this application, life was simpler and I treated all four of these concepts as one table in a SQL database: "Appointment." One "Appointment" could have multiple line items, multiple payments, and one invoice. The invoice was just an e-mail or print out that was produced from the line items and customer record.

9 out of 10 times, this worked fine. When one customer made one appointment for one or a few vehicles and paid for it themselves, all was grand. But this system didn't work under a lot of conditions. For example:

  • When one customer made one appointment, but the appointment got rained out halfway through resulting in the detailer had to come back the next day, I needed two appointments, but only one line item, one invoice and one payment.
  • When a group of customers at an office all decided to have their cars done the same day in order to get a discount, I needed one appointment, but multiple invoices and multiple payments.
  • When one customer paid for two appointments with one check, I needed two appointments, but only one invoice and one payment.

I was able to handle all of these outliers by fudging things a little. For example, if a detailer had to come back the next day, i'd just make another appointment on the second day with a line item that said "Finish Up" and the cost would be $0. Or if I had one customer pay for two appointments with one check, I'd put split payment records in each appointment. The problem with this is that it creates a huge opportunity for data in-congruency. Data in-congruency can be a serious problem especially for cases involving financial information such as the third exmaple where the customer paid for two appointments with one check. Payments must be matched up directly with goods and services rendered in order to properly keep track of accounts receivable.

Proposed structure:

Below, is a normalized structure for organizing and storing this data. Perhaps because of my inexperience, I place a lot of emphasis on data normalization because it seems like a great way to avoid data incongruity errors. With this structure, changes to the data can be done with one operation without having to worry about updating other tables. Reads, however, can require multiple reads coupled with in-memory organization of data. I figure later on, if there are performance issues, I can add some denormalized fields to "Appointment" for faster querying while keeping the "safe" normalized structure intact. Denormalization could potentially slow down writes, but I was thinking that I might be able to make asynchronous calls to other resources or add to the task que so that the client does not have to wait for the extra writes that update the denormalized portions of the data.

Tables:

Appointment
 start_time
 etc...

Invoice
 due_date
 etc...

Payment
 invoice_Key_List
 amount_paid
 etc...

Line_Item
 appointment_Key_List
 invoice_Key
 name
 price
 etc...

The following is the series of queries and operations required to tie all four entities (tables) together for a given list of appointments. This would include information on what services were scheduled for each appointment, the total cost of each appointment and weather or not payment as been received for each appointment. This would be a common query when loading the calendar for appointment scheduling or for a manager to get an overall view of operations.

  • QUERY for the list of "Appointments" who's "start_time" field lies between the given range.
    • Add each key from the returned appointments into a List.
  • QUERY for all "Line_Items" who's appointment_key_List field includes any of the returns appointments
    • Add each invoice_key from all of the line items into a Set collection.
  • QUERY for all "Invoices" in the invoice ket set (this can be done in one asynchronous operation using app engine)
    • Add each key from the returned invoices into a List
  • QUERY for all "Payments" who's invoice_key_list field contains a key matching any of the returned invoices
  • Reorganize in memory so that each appointment reflects the line_items that are scheduled for it, the total price, total estimated time, and weather or not it has been paid for.

...As you can see, this operation requires 4 datastore queries as well as some in-memory organization (hopefully the in-memory will be pretty fast)

Can anyone comment on this design? This is the best I could come up with, but I suspect there might be better options or completely different designs that I'm not thinking of that might work better in general or specifically under GAE's (google app engine) strengths, weaknesses, and capabilities.

Thanks!

Usage clarification

Most applications are more read-intensive, some are more write intensive. Below, I describe a typical use-case and break down operations that the user would want to perform:

Manager gets a call from a customer:

  • Read - Manager loads the calendar and looks for a time that is available
  • Write - Manager queries customer for their information, I pictured this to be a succession of asynchronous reads as the manager enters each piece of information such as phone number, name, e-mail, address, etc... Or if necessary, perhaps one write at the end after the client application has gathered all of the information and it is then submitted.
  • Write - Manager takes down customer's credit card info and adds it to their record as a separate operation
  • Write - Manager charges credit card and verifies that the payment went through

Manager makes an outgoing phone call:

  • Read Manager loads the calendar
  • Read Manager loads the appointment for the customer he wants to call
  • Write Manager clicks "Call" button, a call is initiated and a new CallReacord entity is written
  • Read Call server responds to call request and reads CallRecord to find out how to handle the call
  • Write Call server writes updated information to the CallRecord
  • Write when call is closed, call server makes another request to the server to update the CallRecord resource (note: this request is not time-critical)

Accepted answer:: Both of the top two answers were very thoughtful and appreciated. I accepted the one with few votes in order to imperfectly equalize their exposure as much as possible.

Pistoleer answered 25/6, 2010 at 17:47 Comment(4)
Not directly related to your question, but is there a reason you're using the low-level API? It says in the docs that it's not intended to be used directly, only so that other libraries could be written on top of it. One such library, Objectify (code.google.com/p/objectify-appengine), looks pretty great, and might fit your needs better than using the bare-metal API.Drama
Yeah, its definitely debatable. My logic was that I didn't want to abstract away any of the datastore's capabilities. It was also a lot easier to pick up than it seemed like it would be.Pistoleer
I should also mention that Objectify is for the Java App Engine SDK; which language are you using? It may help people include code samples.Drama
I am using Java. I wrote a little ORM-type codebase to help facilitate moving objects in and out of the database. There is quite a bit of code though, I don't know if it would fit well inside a post.Pistoleer
A
9

You specified two specific "views" your website needs to provide:

  1. Scheduling an appointment. Your current scheme should work just fine for this - you'll just need to do the first query you mentioned.

  2. Overall view of operations. I'm not really sure what this entails, but if you need to do the string of four queries you mentioned above to get this, then your design could use some improvement. Details below.

Four datastore queries in and of itself isn't necessarily overboard. The problem in your case is that two of the queries are expensive and probably even impossible. I'll go through each query:

  1. Getting a list of appointments - no problem. This query will be able to scan an index to efficiently retrieve the appointments in the date range you specify.

  2. Get all line items for each of appointment from #1 - this is a problem. This query requires that you do an IN query. IN queries are transformed into N sub-queries behind the scenes - so you'll end up with one query per appointment key from #1! These will be executed in parallel so that isn't so bad. The main problem is that IN queries are limited to only a small list of values (up to just 30 values). If you have more than 30 appointment keys returned by #1 then this query will fail to execute!

  3. Get all invoices referenced by line items - no problem. You are correct that this query is cheap because you can simply fetch all of the relevant invoices directly by key. (Note: this query is still synchronous - I don't think asynchronous was the word you were looking for).

  4. Get all payments for all invoices returned by #3 - this is a problem. Like #2, this query will be an IN query and will fail if #3 returns even a moderate number of invoices which you need to fetch payments for.

If the number of items returned by #1 and #3 are small enough, then GAE will almost certainly be able to do this within the allowed limits. And that should be good enough for your personal needs - it sounds like you mostly need it to work, and don't need to it to scale to huge numbers of users (it won't).

Suggestions for improvement:

  • Denormalization! Try storing the keys for Line_Item, Invoice, and Payment entities relevant to a given appointment in lists on the appointment itself. Then you can eliminate your IN queries. Make sure these new ListProperty are not indexed to avoid problems with exploding indices

Other less specific ideas for improvement:

  • Depending on what your "overall view of operations" is going to show, you might be able to split up the retrieval of all this information. For example, perhaps you start by showing a list of appointments, and then when the manager wants more information about a particular appointment you go ahead and fetch the information relevant to that appointment. You could even do this via AJAX if you this interaction to take place on a single page.
  • Memcache is your friend - use it to cache the results of datastore queries (or even higher level results) so that you don't have to recompute it from scratch on every access.
Anoa answered 25/6, 2010 at 18:48 Comment(4)
Thanks for your response. I wasn't aware of the 30-value limit on IN queries. I suppose I could shard the query, but that would be nasty. Looks like I'll probably just put denormalized fields into the "Appointment" entities. I don't have any experience with maintaining denormalized data, are the any references that you would suggest?Pistoleer
Denormalization isn't as scary as it sounds. Whenever you create or delete a Line_Item, Invoice, or Payment just update the corresponding Appointment too. I wouldn't worry too much about doing this transactionally either - just create your Line_Item (etc.) and then update your Appointment (if you create multiple line items in a single request, then just update the relevant Appointment entity once). And do the reverse when deleting a Line_Item. If the second query experiences a transient failure, just push it off onto the Task Queue and it will eventually be applied.Anoa
Rather than storing LineItem in a list on Invoice, store a list of LineItem keys on Invoice. You can retrieve entities by key without N queries or the 30 item limit.Zebedee
I agree - store the keys for the related entities in the lists (that's what I was trying to say in my post).Anoa
D
7

As you've noticed, this design doesn't scale. It requires 4 (!!!) DB queries to render the page. That's 3 too many :)

The prevailing notion of working with the App Engine Datastore is that you want to do as much work as you possibly can when something is written, so that almost nothing needs to be done when something is retrieved and rendered. You presumably write the data very few times, compared to how many times it's rendered.

Normalization is similarly something that you seem to be striving for. The Datastore doesn't place any value in normalization -- it may mean less data incongruity, but it also means reading data is muuuuuch slower (4 reads?!!). Since your data is read much more often than it's written, optimize for reads, even if that means your data will occasionally be duplicated or out of sync for a short amount of time.

Instead of thinking about how the data looks when it's stored, think about how you want the data to look when it's displayed to the user. Store as close to that format as you can, even if that means literally storing pre-rendered HTML in the datastore. Reads will be lightning-fast, and that's a good thing.

So since you should optimize for reads, oftentimes your writes will grow to gigantic proportions. So gigantic that you can't fit it in the 30 second time limit for requests. Well, that's what the task queue is for. Store what you consider the "bare necessities" of your model in the datastore, then fire off a task queue to pull it back out, generate the HTML to be rendered, and put it in there in the background. This might mean your model is immediately ready to display until the task has finished with it, so you'll need a graceful degradation in this case, even if that means rendering it "the slow way" until the data is fully populated. Any further reads will be lightning-quick.

In summary, I don't have any specific advice directly related to your database -- that's dependent on what you want the data to look like when the user sees it.

What I can give you are some links to some super helpful videos about the datastore:

  • Brett Slatkin's 2008 and 2009 talks on building scalable, complex apps on App Engine, and a great one from this year about data pipelines (which isn't directly applicable I think, but really useful in general)
  • App Engine Under the Covers: How App Engine does what it does, behind the scenes
  • AppStats: a great way to see how many datastore reads you're performing, and some tips on reducing that number
Drama answered 25/6, 2010 at 18:32 Comment(5)
Wow, this answer got long in a hurry :) Sorry about that, hopefully it's not entirely useless!Drama
Well, a long answer to a long question - and besides, you had plenty of helpful things to point out. :)Anoa
Thanks for your response, it was very helpful. It wasn't too long, especially considering the length of the question. I added some more information on use cases to communicate a better feel for Read-Write ratios.Pistoleer
With the datastore failing a certain percentage of the time as well as going into read-only mode sporadically, how do you ensure data integrity of your denormalized data?Pistoleer
You'll have to figure that out based on your own needs. If the datastore is in read-only mode you can detect that and just not allow writes during that time. I don't know if transactions can be interrupted by read-only mode, but if not, then that might be a way to maintain consistency while the datastore degrades.Drama
D
2

Here are a few app-engine specific factors that I think you'll have to contend with:

  • When querying using an inequality, you can only use an inequality on one property. for example, if you are filtering on an appt date being between July 1st and July 4th, you couldn't also filter by price > 200

  • Transactions on app engine are a bit tricky compared to the SQL database you are probably used to. You can only do transactions on entities that are in the same "entity group".

Dion answered 25/6, 2010 at 18:55 Comment(1)
'Using cross-group transactions' - section in this link says we can use transactions for multiple entities also.Tarter

© 2022 - 2024 — McMap. All rights reserved.