2010-08-19 23 views
5

Tôi gặp một chút rắc rối trong Google App Engine để đảm bảo rằng dữ liệu của tôi là chính xác khi sử dụng mối quan hệ tổ tiên không có tên chính.Làm cách nào để đảm bảo tính toàn vẹn dữ liệu cho các đối tượng trong công cụ ứng dụng của Google mà không sử dụng tên khóa?

Để tôi giải thích thêm một chút: Tôi có một thực thể mẹ loại và tôi muốn tạo một thực thể con mục. Tôi muốn tạo một hàm lấy tên danh mục và tên mục và tạo cả hai thực thể nếu chúng không tồn tại. Ban đầu tôi đã tạo một giao dịch và tạo cả giao dịch trong trường hợp cần thiết bằng cách sử dụng tên khóa và điều này đã hoạt động tốt. Tuy nhiên, tôi nhận ra rằng tôi không muốn sử dụng tên là chìa khóa vì nó có thể cần phải thay đổi, và tôi đã cố gắng trong phạm vi giao dịch của tôi để làm điều này:

def add_item_txn(category_name, item_name): 
    category_query = db.GqlQuery("SELECT * FROM Category WHERE name=:category_name", category_name=category_name) 
category = category_query.get() 
if not category: 
    category = Category(name=category_name, count=0) 

item_query = db.GqlQuery("SELECT * FROM Item WHERE name=:name AND ANCESTOR IS :category", name=item_name, category=category) 
item_results = item_query.fetch(1) 
if len(item_results) == 0: 
    item = Item(parent=category, name=name) 

db.run_in_transaction(add_item_txn, "foo", "bar") 

Những gì tôi thấy khi tôi cố gắng chạy này là App Engine từ chối điều này vì nó sẽ không cho phép bạn chạy truy vấn trong một giao dịch: Only ancestor queries are allowed inside transactions.

Nhìn vào example Google cung cấp cho về cách giải quyết vấn đề này:

def decrement(key, amount=1): 
    counter = db.get(key) 
    counter.count -= amount 
    if counter.count < 0: # don't let the counter go negative 
     raise db.Rollback() 
    db.put(counter) 

q = db.GqlQuery("SELECT * FROM Counter WHERE name = :1", "foo") 
counter = q.get() 
db.run_in_transaction(decrement, counter.key(), amount=5) 

Tôi đã cố gắng để di chuyển của tôi lấy chủng loại để trước khi giao dịch:

def add_item_txn(category_key, item_name): 
    category = category_key.get() 
    item_query = db.GqlQuery("SELECT * FROM Item WHERE name=:name AND ANCESTOR IS :category", name=item_name, category=category) 
    item_results = item_query.fetch(1) 
    if len(item_results) == 0: 
     item = Item(parent=category, name=name) 

category_query = db.GqlQuery("SELECT * FROM Category WHERE name=:category_name", category_name="foo") 
category = category_query.get() 
if not category: 
    category = Category(name=category_name, count=0) 
db.run_in_transaction(add_item_txn, category.key(), "bar") 

này dường như làm việc, nhưng tôi tìm thấy khi tôi chạy điều này với một số yêu cầu mà tôi đã tạo các danh mục trùng lặp, điều này có ý nghĩa, vì danh mục được truy vấn bên ngoài giao dịch và nhiều yêu cầu có thể tạo nhiều danh mục.

Có ai có ý tưởng nào về cách tôi có thể tạo các danh mục này đúng cách không? Tôi đã cố gắng để tạo danh mục thành một giao dịch, nhưng đã nhận được lỗi về truy vấn tổ tiên chỉ một lần nữa.

Cảm ơn!

Simon

+0

Bạn đang cố gắng tránh sử dụng tên khóa cho Danh mục? Cách tiêu chuẩn để đảm bảo tính duy nhất là sử dụng db.get_or_insert (parent, key_name). – mahmoud

+0

Vâng, lý do đó tôi tránh điều đó là vì tên danh mục có thể thay đổi và nếu tôi đổi tên nhưng có khóa là tên cũ, tôi sợ mọi thứ sẽ rất khó hiểu ... – Simon

Trả lời

2

Đây là cách tiếp cận để giải quyết vấn đề của bạn. Nó không phải là một cách tiếp cận lý tưởng trong nhiều cách, và tôi chân thành hy vọng rằng một người nào đó khác AppEnginer sẽ đưa ra một giải pháp neater hơn tôi có. Nếu không, hãy thử.

Cách tiếp cận của tôi sử dụng chiến lược sau: nó tạo ra các thực thể hoạt động như bí danh cho các thực thể Danh mục. Tên của Danh mục có thể thay đổi, nhưng thực thể bí danh sẽ giữ lại khóa của nó và chúng tôi có thể sử dụng các phần tử của khóa bí danh để tạo một tên khóa cho các thực thể Danh mục của bạn, vì vậy chúng tôi có thể tra cứu Danh mục theo tên của nó, nhưng lưu trữ của nó được tách rời khỏi tên của nó.

Bí danh tất cả được lưu trữ trong một nhóm thực thể duy nhất và cho phép chúng tôi sử dụng truy vấn tổ tiên thân thiện với giao dịch, vì vậy chúng tôi có thể tra cứu hoặc tạo Danh mục mà không có nguy cơ tạo nhiều bản sao.

Khi tôi muốn tra cứu hoặc tạo danh mục và mục, tôi có thể sử dụng tên khóa của danh mục để tạo khóa trong chương trình một cách có lập trình và chúng tôi được phép nhận thực thể qua khóa của nó bên trong giao dịch.

class CategoryAliasRoot(db.Model): 
    count = db.IntegerProperty() 
    # Not actually used in current code; just here to avoid having an empty 
    # model definition. 

    __singleton_keyname = "categoryaliasroot" 

    @classmethod 
    def get_instance(cls): 
      # get_or_insert is inherently transactional; no chance of 
      # getting two of these objects. 
     return cls.get_or_insert(cls.__singleton_keyname, count=0) 

class CategoryAlias(db.Model): 
    alias = db.StringProperty() 

    @classmethod 
    def get_or_create(cls, category_alias): 
     alias_root = CategoryAliasRoot.get_instance() 
     def txn(): 
      existing_alias = cls.all().ancestor(alias_root).filter('alias = ', category_alias).get() 
      if existing_alias is None: 
       existing_alias = CategoryAlias(parent=alias_root, alias=category_alias) 
       existing_alias.put() 

      return existing_alias 

     return db.run_in_transaction(txn) 

    def keyname_for_category(self): 
     return "category_" + self.key().id 

    def rename(self, new_name): 
     self.alias = new_name 
     self.put() 

class Category(db.Model): 
    pass 

class Item(db.Model): 
    name = db.StringProperty() 

def get_or_create_item(category_name, item_name): 

    def txn(category_keyname): 
     category_key = Key.from_path('Category', category_keyname) 

     existing_category = db.get(category_key) 
     if existing_category is None: 
      existing_category = Category(key_name=category_keyname) 
      existing_category.put() 

     existing_item = Item.all().ancestor(existing_category).filter('name = ', item_name).get() 
     if existing_item is None: 
      existing_item = Item(parent=existing_category, name=item_name) 
      existing_item.put() 

     return existing_item 

    cat_alias = CategoryAlias.get_or_create(category_name) 
    return db.run_in_transaction(txn, cat_alias.keyname_for_category()) 

Hiện tượng trống: Tôi chưa thử nghiệm mã này. Rõ ràng, bạn sẽ cần phải thay đổi nó để phù hợp với mô hình thực tế của bạn, nhưng tôi nghĩ rằng các nguyên tắc mà nó sử dụng là âm thanh.

CẬP NHẬT: Simon, trong nhận xét của bạn, bạn chủ yếu có ý tưởng đúng; mặc dù, có một sự tinh tế quan trọng mà bạn không nên bỏ lỡ. Bạn sẽ nhận thấy rằng các thực thể Category không phải là con của root giả. Họ không chia sẻ một phụ huynh, và họ chính họ là các thực thể gốc trong các nhóm thực thể riêng của họ. Nếu các thực thể Category tất cả đều có cùng một parent, điều đó sẽ làm cho một nhóm thực thể khổng lồ, và bạn sẽ có một cơn ác mộng hiệu suất bởi vì mỗi nhóm thực thể chỉ có thể có một giao dịch chạy trên nó một lúc.

Thay vào đó, các thực thể CategoryAlias ​​là con của thực thể gốc không có thật. Điều đó cho phép tôi truy vấn bên trong một giao dịch, nhưng nhóm thực thể không nhận được quá lớn bởi vì các Item thuộc về mỗi Category không được gắn vào CategoryAlias.

Ngoài ra, dữ liệu trong thực thể CategoryAlias ​​có thể thay đổi mà không thay đổi khóa của entitie. Vì vậy, tôi có thể thay đổi tên được lưu trữ trong CategoryAlias ​​mà không làm mất khả năng của tôi để phù hợp với thực thể đó với cùng Danh mục.

+0

Adam, Cảm ơn giải pháp - được nhiều người đánh giá cao. Tôi đọc qua nó một vài lần, và khi tôi hiểu điều bạn đang làm ở đây là tạo ra một thực thể gốc giả để đảm bảo rằng khi danh mục được lấy nó được lấy từ một nhóm thực thể (và do đó được cho phép trong một giao dịch) - đó là đúng? cảm ơn! -simon – Simon

+0

@Simon, tôi đã chỉnh sửa câu trả lời của tôi theo phản hồi của bạn. –

+0

Sự khác biệt rất lớn ở đó: thực thể cha mẹ khác biệt với danh mục, cảm ơn câu trả lời của bạn! – Simon

0

Một vài điều cần lưu ý (tôi nghĩ rằng họ có lẽ chỉ là lỗi chính tả) -

  1. Dòng đầu tiên của cuộc gọi phương thức giao dịch của bạn nhận được() trên một phím - đây là không phải là một chức năng được ghi lại. Tuy nhiên, bạn không cần phải có đối tượng danh mục thực tế trong hàm - khóa là đủ ở cả hai nơi bạn đang sử dụng thực thể danh mục.

  2. Bạn dường như không gọi put() trên một trong hai danh mục hoặc mục (nhưng vì bạn nói bạn đang nhận dữ liệu trong kho dữ liệu, tôi cho rằng bạn đã bỏ qua điều này một cách ngắn gọn?)

theo như một giải pháp đi - bạn có thể cố gắng thêm một giá trị trong memcache với hết hạn hợp lý -

if memcache.add("category.%s" % category_name, True, 60): create_category(...) 

này ít nhất dừng bạn tạo bội. Nó vẫn còn một chút khó khăn để biết phải làm gì nếu truy vấn không trả về thể loại, nhưng bạn không thể lấy khóa từ memcache. Điều này có nghĩa là danh mục đang trong quá trình tạo.

Nếu yêu cầu gốc xuất phát từ hàng đợi công việc, thì chỉ cần ném một ngoại lệ để tác vụ được chạy lại.

Nếu không, bạn có thể chờ một chút và truy vấn lại, mặc dù điều này hơi tinh tế.

Nếu yêu cầu đến từ người dùng, thì bạn có thể cho họ biết đã có xung đột và thử lại.

+0

cảm ơn vì đã đánh bắt lỗi chính tả của tôi - thay vì sao chép chính xác chức năng của tôi trong ngắn gọn, tôi đã chỉnh sửa nó rất nhiều và thực hiện những điều sau: ( Đây là một giải pháp thú vị, memcache có đảm bảo không va chạm không? – Simon

+0

memcache.add() đảm bảo nguyên tử - tức là bạn chỉ có thể thêm khóa một lần Tất nhiên với mã ở trên, mục nhập hết hạn vào những năm 60 và bạn có thể lấy lại khóa. http://code.google.com/appengine/docs/python/memcache/clientclass.html # Client_add – hawkett

Các vấn đề liên quan