Mô hình hóa miền đối tượng và lưu giữ lâu bền đối tượng cho các kho lưu trữ dữ liệu không quan hệ

Andrew Glover, Tác giả và Nhà phát triển, Beacon50

Qua loạt bài này, độc giả nhận thấy rằng các kho lưu trữ dữ liệu NoSQL đã thúc đẩy sự bùng nổ về đổi mới trong thế giới Java™ trong vài năm qua. Ngoài các kho lưu trữ dữ liệu (như CouchDB, MongoDB và Bigtable), chúng ta đã bắt đầu thấy xuất hiện các công cụ mở rộng tính tiện ích của chúng. Các thư viện ánh xạ kiểu ORM dẫn đầu trào lưu này nhờ chỉ ra một trong những thách thức nguy hiểm của NoSQL: làm thế nào để ánh xạ có hiệu quả các đối tượng Java cũ (là tài sản chung của các kho lưu trữ dữ liệu không lược đồ) và làm cho các đối tượng đó trở nên hữu dụng, giống như những gì Hibernate đã làm đối với các kho lưu trữ dữ liệu quan hệ.

SimpleJPA là một ví dụ của loại này: một thư viện lưu giữ lâu bền để cho phép các đối tượng có chú giải JPA làm việc trơn tru với SimpleDB của Amazon. Trong một vài chuyên mục trước tôi đã giới thiệu SimpleJPA, nhưng cũng đã lưu ý rằng mặc dù nó dựa trên JPA, nó không thực hiện toàn bộ đặc tả của JPA. Điều này là do thực tế là JPA dành để làm việc với các cơ sở dữ liệu quan hệ, mà SimpleDB (và vì thế cả trình trợ giúp nhỏ của nó là SimpleJPA) đã tránh. Các dự án khác thậm chí không cố bắt chước toàn bộ đặc tả của JPA: chúng chỉ mượn những gì chúng muốn từ JPA. Một dự án như vậy — Objectify-Appengine — là chủ đề của chuyên mục tháng này.

Objectify-Appengine: Một thư viện ánh xạ đối tượng không quan hệ
Objectify-Appengine hoặc Objectify, là một thư viện kiểu như-ORM để đơn giản hóa sự lưu giữ lâu bền của dữ liệu trong Bigtable và vì thế cả trong GAE nữa. Là một tầng ánh xạ, Objectify tự chèn chính mình, bằng cách dùng một API đẹp đẽ, giữa các POJO (Đối tượng Java cũ đơn giản) của bạn và thiết bị nặng của Google. Bạn sử dụng một tập hợp con quen thuộc của các chú giải JPA (mặc dù Objectify không thực hiện toàn bộ đặc tả), cùng với một số ít chú giải vòng đời, để lưu giữ lâu bền và lấy ra dữ liệu dưới dạng các đối tượng Java. Về bản chất, Objectify là một Hibernate gọn nhẹ hơn được thiết kế chỉ dành cho Bigtable của Google.

Kiểu như-ORM?
Ánh xạ đối tượng quan hệ là cách phổ biến nhất để vượt qua cái gọi là không khớp trở kháng giữa các mô hình dữ liệu hướng đối tượng và các cơ sở dữ liệu quan hệ (xem phần Tài nguyên). Trong thế giới không quan hệ, chẳng có sự không khớp trở kháng nào, nên Objectify không thực sự là một thư viện ORM, nó giống như thư viện ONRM (ánh xạ đối tượng không quan hệ) hơn. “Kiểu như-ORM” là viết tắt tiện lợi cho những ai trong chúng ta đang mệt mỏi vì từ viết tắt.
Objectify tương tự như Hibernate ở chỗ nó cho phép bạn ánh xạ và sử dụng các POJO dựa vào Bigtable, mà bạn xem Bigtable này như là một sự trừu tượng hóa trong GAE. Ngoài một tập hợp con của các chú giải JPA, Objectify sử dụng các chú giải riêng của mình để giải quyết các tính năng độc đáo của kho dữ liệu GAE. Objectify cũng cho phép các mối quan hệ và trưng ra một giao diện truy vấn để hỗ trợ các khái niệm GAE về lọc và sắp xếp.
Trong các phần tiếp theo, chúng ta sẽ phát triển một ứng dụng ví dụ để cho phép bạn tự mình thử ánh xạ và lưu giữ lâu bền dữ liệu với Objectify, bằng cách sử dụng Bigtable của Google để lưu trữ dữ liệu ứng dụng. Trong nửa thứ hai của bài này, chúng ta sẽ sử dụng dữ liệu của mình trong một ứng dụng web của GAE.

Bức tranh tổng thể, Bigtable
Tôi sẽ cho miền “races và runners” tạm nghỉ và chúng ta cũng có thể bỏ qua các vé đỗ xe. Thay vào đó, chúng ta sẽ khai phá Twitter — một miền ứng dụng quen thuộc khác đối với những ai đã đọc bài giới thiệu về MongoDB hồi tháng trước. Lần này, chúng ta sẽ điều tra không chỉ những ai đã chuyển tiếp tin nhắn cho chúng ta (hoặc tôi, hoặc bạn) trên Twitter, mà còn tìm ra ai là người chuyển tiếp tin nhắn hàng đầu của chúng ta có ảnh hưởng nhất.
Đối với ứng dụng này, chúng ta sẽ cần tạo ra hai lớp miền đối tượng: Retweet và User. Đối tượng Retweet rõ ràng đại diện cho một dữ liệu chuyển tiếp tin nhắn từ một tài khoản Twitter. Đối tượng User đại diện cho người dùng Twitter mà chúng ta sẽ khai phá dữ liệu tài khoản của người đó. (Lưu ý rằng đối tượng User này là khác với đối tượng User của GAE). Mỗi đối tượng Retweet có một mối quan hệ với một đối tượng User.

Về Bigtable
Bigtable là một kho dữ liệu NoSQL theo hướng column (cột) để có thể truy cập thông qua GAE. Thay vì các lược đồ mà bạn thường thấy trong một cơ sở dữ liệu quan hệ, về cơ bản Bigtable là một ánh xạ lưu giữ lâu bền có phân tán lớn — một ánh xạ cho phép các truy vấn dựa trên các khoá và các thuộc tính của các giá trị dữ liệu bên dưới. Bigtable với GAE rất giống như SimpleDB với Amazon Web Services (Các dịch vụ Web của Amazon).
Objectify sử dụng API Entity mức thấp của Google để ánh xạ một cách trực giác các đối tượng miền đến kho dữ liệu GAE. Tôi đã giới thiệu API Entity trong một bài viết trước đây (xem phần Tài nguyên), vì vậy tôi sẽ không thảo luận nhiều ở đây. Những điểm chính mà bạn cần biết là trong API Entity các tên miền trở thành kiểu kind — nghĩa là theo logic User sẽ ánh xạ đến một User kind (thể loại người dùng) — giống như một bảng nếu nói theo thuật ngữ quan hệ. (Với một sự tương đồng gần hơn, hãy nghĩ một kind giống như là một ánh xạ đang chứa các khóa và các giá trị). Rồi các thuộc tính miền về cơ bản là các tên cột theo thuật ngữ quan hệ và các giá trị thuộc tính là các giá trị cột. Không giống như SimpleDB của Amazon, kho dữ liệu GAE hỗ trợ một tập các kiểu dữ liệu phong phú bao gồm các BLOB – Đối tượng nhị phân lớn (xem phần Tài nguyên) và đủ mọi con số, ngày tháng và danh sách.

Định nghĩa lớp trong Objectify
Đối tượng User cũng khá cơ bản: chỉ có một tên và hai thuộc tính liên quan đến việc thực hiện OAuth của Twitter, mà chúng ta sẽ sử dụng việc thực hiện OAuth này cho cách tiếp cận trực giác đến việc cấp phép. Thay vì lưu trữ một mật khẩu của người dùng, những người dùng trong một mô hình OAuth lưu trữ các thẻ xác thực, đại diện cho giấy phép của người dùng để hành động nhân danh họ. OAuth hoạt động giống như một thẻ tín dụng, nhưng sử dụng dữ liệu xác thực làm thông tin trao đổi. Thay vì cung cấp cho mỗi trang web tên người dùng và mật khẩu của bạn, bạn cung cấp cho các trang web giấy phép để truy cập thông tin đó. (OAuth tương tự như OpenID — nhưng khác nhau; xem phần Tài nguyên để tìm hiểu thêm).

Liệt kê 1. Khởi đầu của một đối tượng User

import javax.persistence.Id;

public class User {
 @Id
 private String name;	
 private String token;
 private String tokenSecret;

 public User() {
  super();	
 }

 public User(String name, String token, String tokenSecret) {
  super();
  this.name = name;
  this.token = token;
  this.tokenSecret = tokenSecret;	
 }

 public String getName() {
  return name;
 }

 //...
}

Như bạn có thể thấy trong Liệt kê 1, mã đặc trưng của lớp User chính là @Id. @Id là JDO tiêu chuẩn, mà bạn có thể khai báo nó từ import (nhập khẩu). Kho dữ liệu GAE cho phép các mã định danh (ID) hay các khóa có thể hoặc là các String (chuỗi ký tự) hoặc là các số Long/long. Trong Liệt kê 1, tôi đã chỉ rõ tên tài khoản của Twitter là khóa. Tôi cũng đã tạo ra một hàm dựng, nhận tất cả ba thuộc tính, làm cho việc tạo các cá thể mới dễ dàng. Lưu ý rằng thực tế tôi không phải định nghĩa các getter và các setter cho đối tượng này để dùng trong Objectify (mặc dù tôi sẽ cần chúng nếu tôi muốn truy cập hoặc thiết lập các thuộc tính bằng lập trình!).
Khi đối tượng User được lưu giữ lâu bền vào kho dữ liệu bên dưới, nó sẽ là một User kind. Thực thể này sẽ có một khóa được đặt tên là name và hai thuộc tính khác: token và tokenSecret, tất cả đều là các String. Khá dễ dàng, phải không?
Quyền hạn của Người dùng
Tiếp theo, tôi sẽ thêm một chút hành vi vào lớp miền User của tôi. Tôi sẽ tạo ra một phương thức lớp để cho phép các đối tượng User tự tìm thấy mình theo tên.

Liệt kê 2. Tìm các User theo tên

//inside User.java... 
 private static Objectify getService() {
  return ObjectifyService.begin();
 }

 public static User findByName(String name){
  Objectify service = getService();
  return service.get(User.class, name);
 }

Một số thứ sẽ diễn ra trong User mới được tạo ra trongLiệt kê 2. Để sử dụng Objectify, hãy lấy một cá thể của Objectify để xử lý tất cả các hoạt động kiểu-CRUD. Bạn có thể nghĩ về lớp Objectify đại khái giống với lớp SessionFactory của Hibernate.
Lớp Objectify có một API đơn giản. Để tìm một thực thể riêng lẻ theo khóa của nó, bạn chỉ cần gọi phương thức get, nhận đầu vào là một kiểu lớp và khóa. Như vậy, trong Liệt kê 2, tôi ban hành cuộc gọi phương thức get với lớp User bên dưới và tên bạn muốn. Cũng lưu ý rằng các lỗi ngoại lệ của Objectify không được kiểm tra — có nghĩa là tôi không phải lo lắng về việc bắt giữ lại một gói các kiểu Exception (lỗi ngoại lệ). Điều này không có nghĩa là không xảy ra các ngoại lệ; thực chất, chỉ là ta không phải xử lý chúng lúc biên dịch mà thôi. Ví dụ, phương thức get sẽ đưa ra một NotFoundException nếu không thể định vị được User kind. (Objectify cũng cung cấp một phương thức find, để trả về null để thay cho việc đưa ra lỗi ngoại lệ).
Tiếp theo là hành vi của cá thể: Tôi muốn các cá thể User của mình hỗ trợ khả năng liệt kê tất cả các đối tượng retweet sắp xếp theo thứ tự ảnh hưởng, có nghĩa là tôi cần thêm vào một phương thức khác. Nhưng trước tiên tôi sẽ mô hình hóa đối tượng Retweet của mình.

Có bao nhiêu đối tượng retweet?
Đối tượng Retweet, như bạn có thể đoán được, biểu diễn một việc chuyển tiếp tin nhắn của Twitter. Đối tượng này sẽ có một số các thuộc tính, bao gồm cả mối quan hệ quay lại đối tượng User sở hữu nó.
Tôi đã đề cập rằng một mã định danh hoặc khóa trong kho dữ liệu GAE phải là một String hoặc là một số Long/long. Các khóa trong kho dữ liệu GAE cũng là duy nhất, giống như trong một cơ sở dữ liệu truyền thống. Đó là lý do tại sao khóa của đối tượng User chính là tên của một tài khoản Twitter, vốn đã là duy nhất rồi. Khóa trên đối tượng Retweet trong Liệt kê 3 sẽ là một tổ hợp của tweet id và người dùng đã chuyển tiếp lại nó. (Twitter không cho phép tạo và gửi cùng một văn bản hai lần, vì thế bây giờ khóa này có lý).

Liệt kê 3. Định nghĩa đối tượng Retweet

import javax.persistence.Id;
import com.googlecode.objectify.Key;

public class Retweet {
 @Id
 private String id;
 private String userName;
 private Long tweetId;
 private Date date;
 private String tweet;
 private Long influence;
 private Key<User> owner;

 public Retweet() {
  super();
 }

 public Retweet(String userName, Long tweetId, Date date, String tweet,
   Long influence) {
  super();
  this.id = tweetId.toString() + userName;
  this.userName = userName;
  this.tweetId = tweetId;
  this.date = date;
  this.tweet = tweet;
  this.influence = influence;
 }

 public void setOwner(User owner) {
  this.owner = new Key<User>(User.class, owner.getName());
 }
 //...
}

Lưu ý rằng khóa id trong Liệt kê 3 là một String; nó kết hợp tweetId và userName. Phương thức setOwner được hiển thị trong Liệt kê 3 sẽ có ý nghĩa hơn khi tôi giải thích các mối quan hệ.

Mô hình hóa các mối quan hệ
Các đối tượng Retweet và các đối tượng User trong ứng dụng này có một mối quan hệ; có nghĩa là, mỗi đối tượng User giữ một bộ sưu tập logic về các đối tượng Retweet và mỗi đối tượng Retweet giữ một liên kết trực tiếp quay về đối tượng User của nó. Xem lại Liệt kê 3 và bạn có thể nhận thấy một cái gì đó không bình thường: Một đối tượng Retweet có một đối tượng Key của kiểu User.
Việc Objectify sử dụng các Key , chứ không phải là các tham chiếu đối tượng, phản ánh kho dữ liệu không truyền thống của GAE trong số nhiều thứ khác thiếu tính toàn vẹn tham chiếu.
Mối quan hệ giữa hai đối tượng thực sự chỉ cần một kết nối cứng vào đối tượng Retweet. Đó là lý do tại sao một cá thể của đối tượng Retweet nắm giữ một Key trực tiếp đến một cá thể User. Do đó, một cá thể User không thực sự phải lưu giữ lâu bền các Key của đối tượng Retweet ở phía mình — một cá thể User chỉ cần truy vấn các đối tượng retweet để tìm những đối tượng retweet nào có liên kết quay về chính nó.
Tuy nhiên, để làm cho sự tương tác giữa các đối tượng trực giác hơn, trong Liệt kê 4, tôi đã thêm cho User một vài phương thức nhận đầu vào là đối tượng Retweet. Những phương thức này gắn chặt mối quan hệ giữa hai đối tượng: bây giờ đối tượng: User trực tiếp thiết lập quyền sở hữu của nó trong một đối tượng Retweet.

Liệt kê 4. Thêm các đối tượng Retweet cho một đối tượng User

public void addRetweet(Retweet retweet){
 retweet.setOwner(this);
 Objectify service = getService();
 service.put(retweet);
}

public void addRetweets(List<Retweet> retweets){
 for(Retweet retweet: retweets){
  retweet.setOwner(this);
 }

 Objectify service = getService();
 service.put(retweets);
}

Trong Liệt kê 4, tôi đã thêm hai phương thức mới cho đối tượng miền User. Một phương thức làm việc với một bộ sưu tập các đối tượng Retweet, trong khi phương thức kia chỉ làm việc với một cá thể. Bạn sẽ nhận thấy rằng tham chiếu tới service đã được định nghĩa ở trên trong Liệt kê 2 và phương thức put của nó đã được nạp chồng lên để làm việc với cả các cá thể riêng lẻ và các List (Danh sách). Mối quan hệ trong trường hợp này cũng được đối tượng chủ sở hữu xử lý— cá thể User tự thêm mình vào đối tượng Retweet. Vì vậy các đối tượng Retweet được tạo ra riêng biệt, nhưng một khi chúng được thêm vào một cá thể của User, chúng chính thức được đính kèm.

Khai phá Twitter
Bước tiếp theo của tôi là thêm một phương thức kiểu như-trình dò tìm vào đối tượng User. Phương thức này sẽ cho phép tôi liệt kê tất cả các đối tượng Retweet sở hữu sắp xếp theo thứ tự ảnh hưởng — có nghĩa là, từ một tài khoản sở hữu ban đầu đến các tài khoản đã chuyển tiếp nó. Tôi sẽ theo vết từ tài khoản có nhiều người đi theo nhất đến tài khoản có ít người đi theo nhất.

Liệt kê 5. Các đối tượng Retweet sắp xếp theo ảnh hưởng

public List<Retweet> listAllRetweetsByInfluence(){
 Objectify service = getService();
 return service.query(Retweet.class).filter("owner", this).order("-influence").list();
}

Mã trong Liệt kê 5 lưu trú trong đối tượng User. Nó trả về một List của các đối tượng Retweet được sắp xếp theo thuộc tính influence (ảnh hưởng) của chúng. Thuộc tính này là một số nguyên. Dấu “-” trong trường hợp này cho biết rằng tôi muốn các đối tượng Retweet được sắp xếp theo thứ tự giảm dần, từ cao nhất đến thấp nhất. Lưu ý mã truy vấn của Objectify: cá thể service hỗ trợ lọc theo thuộc tính (trong trường hợp này là owner – chủ sở hữu) và thậm chí còn hỗ trợ sắp xếp các kết quả. Cũng lưu ý vẫn tiếp tục mẫu các trường hợp ngoại lệ không được kiểm tra để giữ cho mã này ngắn gọn.
Truy vấn nhiều thuộc tính
Kho dữ liệu GAE sử dụng một chỉ mục cho bất kỳ truy vấn nào. Điều này giúp đọc nhanh hơn vì các thuộc tính đơn lẻ trong một thực thể được lập chỉ mục tự động. Nhưng nếu rốt cuộc bạn phải truy vấn theo nhiều thuộc tính (như tôi đã làm trong Liệt kê 5, truy vấn theo owner rồi theo influence), bạn phải cung cấp một tệp datastore-index.xml cho GAE. Việc này cung cấp cho GAE cảnh báo trước về một truy vấn đến. Liệt kê 6 là chỉ mục tùy chỉnh để có thể truy vấn nhiều thuộc tính:

Liệt kê 6. Định nghĩa một chỉ mục tùy chỉnh cho kho dữ liệu GAE

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="true">
 <datastore-index kind="Retweet" ancestor="false">
  <property name="owner" direction="asc" />
  <property name="influence" direction="desc" />
 </datastore-index>
</datastore-indexes>


Lưu giữ lâu bền
Cuối cùng nhưng không kém quan trọng, tôi cần phải thêm một số khả năng để lưu giữ lâu bền các đối tượng miền của mình. Bạn có thể thấy rằng có một luồng công việc ngầm cho mối quan hệ giữa các đối tượng User và Retweet. Cụ thể, tôi cần có một cá thể User được tạo ra (và được lưu vào kho dữ liệu GAE) trước khi tôi có thể thêm các đối tượng Retweet liên quan một cách logic.
Trong Liệt kê 7, tôi thêm một phương thức save vào đối tượng User, nhưng lưu ý rằng tôi không cần thêm phương thức đó vào đối tượng Retweet. Các đối tượng Retweet được lưu trữ tự động khi tôi thêm chúng vào một cá thể User — do tôi thực hiện thông qua các phương thức addRetweet và addRetweets (lưu ý các cuộc gọi đến service.put trong Liệt kê 4).

Liệt kê 7. Lưu trữ các User

public void save(){
 Objectify service = getService();
 service.put(this);
}

Hãy xem mã đó ngắn gọn như thế nào? Đó là API Objectify đang làm việc.

Đăng ký các lớp miền
Đến lúc tôi sẵn sàng để kéo ứng dụng khai phá Twitter của tôi lại với nhau, bao gồm một số kết nối với API Servlets. Tôi sẽ sử dụng các servlet để xử lý đăng nhập vào Twitter, kéo dữ liệu chuyển tiếp tin nhắn và cuối cùng hiển thị một báo cáo đúng cách. Tuy nhiên, tạm thời bây giờ tôi sẽ gác lại việc đó, để dành cho trí tưởng tượng của bạn và tập trung vào một yêu cầu cuối cùng để làm việc với Objectify: đó là đăng ký các lớp miền theo cách thủ công.
Objectify không tự nạp các lớp miền — có nghĩa là nó không quét đường dẫn lớp (classpath) của bạn để tìm các thực thể. Bạn phải nói rõ cho Objectify biết từ trước, các lớp nào là lớp đặc biệt, để sau này bạn sẽ có thể truy cập và sử dụng chúng thông qua API Objectify. Đối tượng ObjectifyService cho phép bạn đăng ký các lớp miền, tất nhiên bạn cần làm việc đó trước khi thử gọi các hành vi kiểu như CRUD của chúng. May mắn thay, vì tôi đang viết một ứng dụng web đơn giản để triển khai trên GAE, tôi có thể sử dụng API Servlet để đăng ký hai lớp của tôi trong một cá thể ServletContextListener.
Các cá thể ServletContextListener có hai phương thức, một phương thức được gọi khi một bối cảnh được tạo ra, một phương thức được gọi khi một bối cảnh bị hủy bỏ. Các bối cảnh được tạo ra khi bạn lần đầu tiên khởi chạy một ứng dụng web, nên việc này sẽ hoạt động tốt.

Liệt kê 8. Đăng ký các đối tượng miền

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import com.googlecode.objectify.ObjectifyService;

public class ContextInitializer implements ServletContextListener {

 public void contextDestroyed(ServletContextEvent arg) {}

 public void contextInitialized(ServletContextEvent arg) {
  ObjectifyService.register(Retweet.class);
  ObjectifyService.register(User.class);
 }
}

Liệt kê 8 cho thấy một cách thực hiện đơn giản của ServletContextListener, trong đó tôi đăng ký hai lớp miền Objectify của mình là User và Retweet. Theo API Servlet, các cá thể ServletContextListener được đăng ký trong một tệp web.xml. Khi ứng dụng của tôi khởi động trên các máy chủ của Google, mã trong Liệt kê 8 sẽ được gọi ra. Tất cả các servlet trong tương lai có sử dụng các đối tượng miền của tôi sẽ làm việc tốt và không cần bỏ thêm công sức gì nữa.

Kết luận cho Phần 1
Vào lúc này, chúng ta đã viết chi tiết một vài lớp và đã định nghĩa các mối quan hệ của chúng và các khả năng kiểu-CRUD, tất cả đều sử dụng Objectify-AppEngine. Bạn có thể đã thấy một vài điều về API Objectify khi chúng ta đã thực hiện thông qua ứng dụng ví dụ mẫu — chẳng hạn như có thực tế là nó cắt bớt nhiều đoạn dài dòng trong mã Java bình thường. Nó cũng sử dụng một số chú giải JPA tiêu chuẩn, do đó làm thông suốt tuyến đường dành cho các nhà phát triển đã quen làm việc với các framework JPA nâng cao như Hibernate. Nhìn chung, API Objectify làm cho việc mô hình hóa miền với GAE dễ dàng hơn và trực giác hơn, đó là một sự thúc đẩy năng suất của nhà phát triển.
Trong phần hai của bài này, chúng ta sẽ đưa ứng dụng miền của mình lên mức tiếp theo, nối nó với OAuth, API Twitter (thông qua Twitter4J) và Ajax-plus-JSON. Tất cả điều này sẽ phức tạp hơn một chút vì thực tế là chúng ta đang triển khai trên Google App Engine, với một số hạn chế khi hiện thực. Nhưng về mặt ưu điểm, chúng ta cuối cùng sẽ nhận được một ứng dụng web thực sự có khả năng mở rộng quy mô, dựa trên đám mây. Chúng ta sẽ khám phá những sự đánh đổi này vào phần tới, khi chúng ta bắt đầu chuẩn bị ứng dụng ví dụ mẫu để triển khai trên GAE.

Nguồn: ibm.com