Phát triển Java 2.0: NoSQL

Kho dữ liệu NoSQL cũng giống như Bigtable và CouchDB là đều chuyển lên trọng tâm trong thời đại Web 2.0 bởi vì chúng có thể giải quyết các vấn đề mở rộng trên một quy mô lớn. Google và Facebook là hai trong số những tên tuổi lớn đã sử dụng NoSQL, và kể cả chúng tôi nữa. Kho dữ liệu Schemaless về cơ bản khác với cơ sở dữ liệu quan hệ truyền thống, nhưng việc tận dụng chúng dễ dàng hơn bạn nghĩ, đặc biệt là nếu bạn bắt đầu với mô hình miền domain chứ không phải là một quan hệ.

pdf27 trang | Chia sẻ: lylyngoc | Lượt xem: 1442 | Lượt tải: 1download
Bạn đang xem trước 20 trang tài liệu Phát triển Java 2.0: NoSQL, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
Phát triển Java 2.0: NoSQL ô hình hóa dữ liệu Schemaless với Bigtable và Gaelyk của Groovy Kho dữ liệu NoSQL cũng giống như Bigtable và CouchDB là đều chuyển lên trọng tâm trong thời đại Web 2.0 bởi vì chúng có thể giải quyết các vấn đề mở rộng trên một quy mô lớn. Google và Facebook là hai trong số những tên tuổi lớn đã sử dụng NoSQL, và kể cả chúng tôi nữa. Kho dữ liệu Schemaless về cơ bản khác với cơ sở dữ liệu quan hệ truyền thống, nhưng việc tận dụng chúng dễ dàng hơn bạn nghĩ, đặc biệt là nếu bạn bắt đầu với mô hình miền domain chứ không phải là một quan hệ. Cơ sở dữ liệu quan hệ đã thống trị lưu trữ dữ liệu hơn 30 năm, nhưng việc cơ sở dữ liệu schemaless (hay NoSQL) ngày càng phổ biến đã cho thấy rằng điều này đang dần thay đổi. Trong khi các hệ quản trị cơ sở dữ liệu (RDBMS) cung cấp một nền tảng vững chắc để lưu trữ dữ liệu trong kiến trúc client-server truyền thống, nhưng nó không dễ dàng (hay tốn ít chi phí) để mở rộng. Trong thời đại mà các ứng dụng web có thể co giãn được như Facebook, Twitter thì điều này quả thực là một hạn chế đáng tiếc. Trong khi lựa chọn cơ sở dữ liệu quan hệ trước đó (bạn còn nhớ về cơ sở dữ liệu hướng đối tượng chứ?) đã thất bại trong việc giải quyết các vấn đề cấp bách, thì các cơ sở dữ liệu NoSQL như Bigtable của Google hay SimpleDB của Amazon đã xuất hiện như một câu trả lời cho nhu cầu co giãn cao của các ứng dụng Web. Về bản chất, NoSQL có thể là giải pháp cho các vấn đề khó khăn — một trong các quá trình tiến hóa của ứng dụng Web không hơn không kém, nó cũng giống như việc tiến hóa tất yếu lên Web 2.0 vậy. NoSQL: Một lối tư duy mới? Khi các nhà phát triển bàn về cơ sở dữ liệu không-quan-hệ hay NoSQL, thì điều đầu tiên mà họ thường nhắc đến là việc phải thay đổi lối tư duy. Theo tôi, thực ra điều đó còn tùy vào cách tiếp cận ban đầu của bạn về mô hình hóa dữ liệu. Nếu bạn đã quen với việc thiết kế ứng dụng theo cách mô hình hóa cấu trúc cơ sở dữ liệu trước (có nghĩa là bạn phải tìm ra các bảng và các mối quan hệ của chúng trước), sau đó việc mô hình hóa dữ liệu với kho lưu trữ schemaless như Bigtable sẽ yêu cầu bạn phải xem xét lại cách làm. Tuy nhiên, nếu bạn bắt đầu thiết kế ứng dụng với mô hình miền domain thì sẽ cảm thấy phù hợp hơn với cấu trúc schemaless của Bigtable. Kho dữ liệu không-quan-hệ không có các bảng hay khóa chính, hay thậm chí là các khóa ngoại (mặc dù cả hai loại khóa này đều hiện diện trong một dạng nới lỏng). Vì thế bạn sẽ thất vọng nếu cố gắng áp dụng mô hình hóa quan hệ như là nền tảng cho mô hình hóa dữ liệu trong cơ sở dữ liệu NoSQL. Bắt đầu từ một mô hình miền domain giúp đơn giản hóa nhiều thứ; trong thực tế, tôi đã thấy sự linh hoạt của cấu trúc schemaless dưới mô hình miền domain chính là khả năng làm mới. Sự phức tạp của việc chuyển từ một mô hình dữ liệu quan hệ đến một mô hình dữ liệu schemaless phụ thuộc vào cách tiếp cận của bạn: đó là bạn bắt đầu từ một quan hệ hay một thiết kế dựa trên miền domain. Khi bạn di chuyển sang kho dữ liệu như CouchDB hoặc Bigtable, bạn sẽ thật sự loại bỏ được những hạn chế của nền tảng lâu đời như Hibernate (ít nhất là từ bây giờ). Mặt khác, hiệu ứng green-pasture sẽ giúp bạn tự xây dựng nó. Và trong bài này, bạn sẽ tìm hiểu sâu về kho dữ liệu schemaless. Các thực thể và mối quan hệ Một kho dữ liệu schemaless cung cấp cho bạn sự linh hoạt để thiết kế mô hình miền domain với các đối tượng đầu tiên (một thứ gì đó mới hơn, các framework như Grails tạo thuận lợi một cách tự động). Công việc của bạn là tiến về phía trước sau đó map miền domain của bạn với kho dữ liệu, điều này rất đễ dàng đối với Google App Engine. Trong bài "Phát triển Java 2.0: Gaelyk cho Google App Engine," tôi đã giới thiệu về Gaelyk, một framework dựa trên Groovy giúp làm việc với kho dữ liệu của Google. Phần lớn bài viết tập trung vào việc thúc đẩy các đối tượng Google Entity. Ví dụ sau đây (trích từ bài viết đó) cho thấy cách các thực thể đối tượng làm việc trong Gaelyk. Liệt kê 1. Gán đối tượng vào thực thể def ticket = new Entity("ticket") ticket.officer = params.officer ticket.license = params.plate ticket.issuseDate = offensedate ticket.location = params.location ticket.notes = params.notes ticket.offense = params.offense Thiết kế bởi đối tượng Các mô hình đối tượng trong thiết kế cơ sở dữ liệu cho thấy trong khuôn khổ ứng dụng Web hiện đại như Grails và Ruby on Rails, nhấn mạnh việc thiết kế mô hình đối tượng và xử lý việc tạo giản đồ cơ sở dữ liệu cho bạn. Phương pháp này phản đối các tiến trình dai dẳng, nhưng ta sẽ dễ dàng thấy nó trở nên tẻ nhạt nếu bạn sử dụng nhiếu thực thể vé — ví dụ, nếu bạn đã tạo ra (hay tìm kiếm) chúng trong các servlet khác nhau. Một servlet (hay Groovlet) xử lý các nhiệm vụ của bạn có thể loại bỏ đi một số gánh nặng. Một lựa chọn tự nhiên hơn — như tôi sẽ chứng minh — sẽ là một mô hình đối tượng Ticket. Trở lại với Race (đường đua) Thay vì tạo lại các ticket ví dụ từ phần giới thiệu Gaelyk, tôi sẽ giữ mọi thứ mới mẻ và sử dụng một chủ đề xuyên suốt bài này để xây dựng một ứng dụng chứng minh các kỹ thuật mà tôi đã thảo luận.. Sơ đồ nhiều-nhiều ở Hình 1 cho thấy, một Race có nhiều Runners và một Runner có thể thuộc về nhiều Races. Figure 1. Race and runners Nếu tôi được sử dụng một cấu trúc bảng quan hệ để thiết kế mối quan hệ này thì tôi cần ít nhất ba bảng: bảng thứ ba chính là bảng nối liên kết một mối quan hệ nhiều- nhiều. Tôi rất vui vì không bị ràng buộc với mô hình dữ liệu quan hệ. Thay vào đó, tôi sẽ sử dụng Gaelyk (và mã Groovy) để lập bản đồ quan hệ nhiều-nhiều này để trừu tượng Bigtable của Google cho Google App Engine. Thực tế là Gaelyk cho phép một Entity được đối xử như một Map để giúp quá mình trở nên đơn giản. Mở rộng quy mô với Shards Sharding là một dạng của phân vùng sao chép cấu trúc bảng qua các nút nhưng phân chia hợp lý dữ liệu của chúng. Ví dụ, một nút có thể có tất cả các dữ liệu liên quan đến tài khoản cư trú ở Mỹ và một nút khác cho tất cả các tài khoản cư trú ở châu Âu. Những thách thức của Shards xảy ra khi các nút có mối quan hệ — đó là, cross-shard joins. Đó là một vấn đề khó giải quyết và trong nhiều trường hợp không được hỗ trợ. (Xem mục Tài nguyên để thấy đường dẫn đến bài thảo luận của tôi với Google's Max Ross về sharding và thách thức khả năng mở rộng với cơ sở dữ liệu quan hệ.) Một trong những nét đẹp của kho lưu trữ schemaless là tôi không cần phải biết những gì sắp xảy ra; điều đó có nghĩa tôi có thể thay đổi dễ dàng hơn với một lược đồ (schema) cơ sở dữ liệu quan hệ. (Lưu ý rằng tôi không ngụ ý bạn không thể thay đổi lược đồ, tôi chỉ nói rằng sự thay đổi đó sẽ được xem xét dễ dàng hơn.) Tôi sẽ không định nghĩa các thuộc tính của đối tượng miền domain — Tôi trì hoãn việc đó là để Groovy tự xử lý động (về bản chất, để làm các đối tượng của tôi ủy quyền cho các đối tượng Entity của Google). Thay vào đó, tôi sẽ dành nhiều thời gian để tìm ra cách mà tôi muốn tìm các đối tượng và xử lý các mối quan hệ. Đó là NoSQL và các framework khác nhau tận dụng kho dữ liệu schemaless chưa được tích hợp. Mô hình lớp cơ sở Tôi sẽ bắt đầu bằng việc tạo một lớp cơ sở chứa một thể hiện của một đối tượng Entity. Sau đó, tôi sẽ cho phép các lớp con có thuộc tính dynamic được thêm vào tương ứng với các thể hiện Entity thông qua phương thức setProperty. setProperty được gọi cho bất kỳ setter thuộc tính nào mà không thật sự tồn tại trong một đối tượng. (Nếu bạn thấy điều này có vẻ lạ thì đừng lo, bạn sẽ hiểu khi bắt tay vào thực hành.) Liệt kê 2 là đoạn mã mở đầu trong ứng dụng của tôi tại thể hiện Model: Liệt kê 2. Một lớp mô hình cơ sở đơn giản package com.b50.nosql import com.google.appengine.api.datastore.DatastoreServiceFactory import com.google.appengine.api.datastore.Entity abstract class Model { def entity static def datastore = DatastoreServiceFactory.datastoreService public Model(){ super() } public Model(params){ this.@entity = new Entity(this.getClass().simpleName) params.each{ key, val -> this.setProperty key, val } } def getProperty(String name) { if(name.equals("id")){ return entity.key.id }else{ return entity."${name}" } } void setProperty(String name, value) { entity."${name}" = value } def save(){ this.entity.save() } } Hãy lưu ý cách mà lớp trừu tượng định nghĩa một hàm khởi tạo lấy Map của các thuộc tính — Tôi hoàn toàn có thể thêm vào nhiều hàm khởi tạo sau. Thiết lập này khá tiện dụng cho các framework Web, thường sử dụng để đón các tham số được gửi từ form. Gaelyk và Grails đưa các thông số vào một đối tượng gọi là params. Hàm khởi tạo lặp lại trên Map này và gọi phương thức setProperty cho mỗi cặp key/value. Hãy nhìn vào phương thức setProperty ra thấy rằng khóa (key) được thiết lập vào thuộc tính name của entity, trong khi đó giá trị (value) tương ứng chính là giá trị của entity's. Các thủ thuật Groovy Như tôi đã đề cập, tính động của Groovy cho phép tôi bắt giữ các lời gọi phương thức đến các thuộc tính không tồn tại thông qua các phương thức get và setProperty. Vì vậy, các lớp con của Model trong Liệt kê 2 không cần phải tự định nghĩa các thuộc tính — chúng đơn giản chỉ cần ủy thác các lời gọi thuộc tính đến đối tượng entity. Mã trong Liệt kê 2 chỉ ra nhiều điểm giá trị của Groovy. Trước tiên, tôi có thể bỏ qua các phương thức truy xuất của một thuộc tính bằng cách thêm @ vào trước thuộc tính. Tôi phải làm điều này đối với các đối tượng entity trong hàm khởi tạo, hay có thể gọi phương thức setProperty. Gọi setProperty tại thời điểm này rõ ràng là sẽ phá vỡ mô hình, như biến entity trong phương thức setProperty có thể có giá trị null. Thứ hai, lời gọi this.getClass().simpleName trong hàm khởi tạo sẽ thiết lập "loại" của entity— thuộc tính simpleName sẽ mang tên của một lớp con mà không có tên package phía trước (lưu ý rằng simpleName thực sự gọi đến phương thức getSimpleName, nhưng Groovy cho phép tôi truy cập vào thuộc tính mà không cần gọi phương thức JavaBeans-esque tương ứng.) Cuối cùng, nếu gọi đến thuộc tính id (là key của đối tượng), thì phương thức getProperty đủ thông minh để yêu cầu key cho id của nó. Trong Google App Engine, các thuộc tính key của entities được tự động sinh ra. Lớp con: Race Bạn có thể định nghĩa dễ dàng một lớp con Race như Liệt kê 3: Liệt kê 3. Lớp con Race package com.b50.nosql class Race extends Model { public Race(params){ super(params) } } Khi một lớp con được khởi tạo với danh sách các tham số (có nghĩa là một Map chứa các cặp key/value) thì một entity tương ứng được tạo ra trong bộ nhớ. Để lưu giữ nó, tôi chỉ cần phương thức save. Liệt kê 4. Tạo một thể hiện Race và lưu nó vào kho dữ liệu GAE import com.b50.nosql.Runner def iparams = [:] def formatter = new SimpleDateFormat("MM/dd/yyyy") def rdate = formatter.parse("04/17/2010") iparams["name"] = "Charlottesville Marathon" iparams["date"] = rdate iparams["distance"] = 26.2 as double def race = new Race(iparams) race.save() Trong Liệt kê 4 chính là Groovlet, một Map (được gọi là iparams) được tạo ra với ba thuộc tính — name (tên), date (ngày tháng), và distance (chiều dài) của race (đường đua). (Lưu ý rằng trong Groovy, một Map rỗng được tạo bằng cú pháp [:].) Vậy là một thể hiện mới của Race đã được tạo và lưu vào kho dữ liệu thông qua phương thức save. Tôi có thể kiểm tra lại kho lưu trữ thông qua giao diện điều khiển Google App Engine để đảm bảo rằng dữ liệu của tôi đang ở đó, như trong Hình 2: Figure 2. Viewing the newly created Race Các phương thức tìm kiếm dành cho các Entities đã tồn tại Bây giờ tôi đã có một Entity, thật hữu ích khi tôi có thể truy lại nó; sau đó, có thể thêm vào một phương thức "finder". Trong trường hợp này, tôi sẽ tạo nó như một phương thức trong lớp (dạng static) và cho phép Races có thể được tìm kiếm theo tên (tức là tôi tìm kiếm dựa vào thuộc tính name). Tôi hoàn toàn có thể thêm vào các phương thức tìm kiếm đối với các thuộc tính khác. Tôi cũng quy ước rằng nếu tên phương thức nào không có từ all thì xem như nó chỉ tìm một thể hiện. Nếu phương thức nào có từ all (như phương thức findAllByName) thì nó có thể trả về một Collection, hay List, hay nhiều thể hiện. Liệt kê 5 cho ta thấy phương thức tìm kiếm findByName: Liệt kê 5. Một phương thức tìm kiếm đơn giản theo tên của Entity static def findByName(name){ def query = new Query(Race.class.simpleName) query.addFilter("name", Query.FilterOperator.EQUAL, name) def preparedQuery = this.datastore.prepare(query) if(preparedQuery.countEntities() > 1){ return new Race(preparedQuery.asList(withLimit(1))[0]) }else{ return new Race(preparedQuery.asSingleEntity()) } } Phương thức tìm kiếm đơn giản này sử dụng các truy vấn Query và PreparedQuery của Google App Engine để tìm các entity có loại là "Race," có tên giống (hay chính xác) với từ khóa nhập vào. Nếu tìm ra nhiều Race thỏa điều kiện này, phương thức sẽ trả về giá trị đầu tiên trong danh sách, theo thông số giới hạn trang là 1 (withLimit(1)). Phương thức findAllByName cũng tương tự vậy nhưng sẽ có thêm 1 tham số với ý nghĩa rằng bạn muốn hiển thị bao nhiêu kết quả?, như trong Liệt kê 6: Liệt kê 6. Tìm tất cả name (tên) static def findAllByName(name, pagination=10){ def query = new Query(Race.class.getSimpleName()) query.addFilter("name", Query.FilterOperator.EQUAL, name) def preparedQuery = this.datastore.prepare(query) def entities = preparedQuery.asList(withLimit(pagination as int)) return entities.collect { new Race(it as Entity) } } Cũng giống như các phương thức tìm kiếm đã được định nghĩa, phương thức findAllByName tìm kiếm các thể hiện Race theo tên, nhưng nó trả về tất cảRaces. Phương thức collect (tập hợp) của Groovy khá hấp dẫn, bằng cách này: nó cho phép tôi thả vào vòng lặp tương ứng tạo ra các thể hiện Race. Lưu ý, Groovy cũng cho phép giá trị mặc định cho tham số của phương thức; do đó, nếu tôi không nhập vào giá trị thứ hai thì pagination sẽ có giá trị mặc định là 10. Liệt kê 7. Tiến hành tìm kiếm def nrace = Race.findByName("Charlottesville Marathon") assert nrace.distance == 26.2 def races = Race.findAllByName("Charlottesville Marathon") assert races.class == ArrayList.class Các phương thức tìm kiếm ở Liệt kê 7 hoạt động như mong đợi: phương thức findByName trả về một thể hiện trong khi phương thức findAllByName trả về một tập hợp (giả sử có nhiều hơn "Charlottesville Marathon"). Đối tượng Runner (vận động viên) cũng không có nhiều khác biệt Bây giờ tôi có thể thoải mái tạo và tìm kiếm các thể hiện của Race, tôi tạo một đối tượng Runner. Quá trình tạo cũng giống như khi tạo thể hiện Race; và tôi chỉ cần mở rộng Model, như trong Liệt kê 8: Liệt kê 8. Dễ dàng tạo một Runner package com.b50.nosql class Runner extends Model{ public Runner(params){ super(params) } } Nhìn vào Liệt kê 8, tôi có cảm giác rằng chúng ta gần tới đích rồi. Tôi còn có thể tạo ra mối liên kết giữa các runners và races. Và tất nhiên, tôi sẽ mô hình hóa mối quan hệ của chúng là nhiều-nhiều bởi vì các vận động viên (runners) có thể chạy trên nhiều đường đua (races). Mô hình hóa miền domain không cần lược đồ Sự trừu tượng của Google App Engine trên Bigtable không phải là một mô hình hướng đối tượng; nghĩa là tôi không thể lưu lại các mối quan hệ nhưng tôi có thể chia sẻ các key (khóa). Do đó, để mô hình hóa mối quan hệ giữa Races và Runners, tôi sẽ lưu trữ một danh sách các key Runner bên trong mỗi thể hiện Race, và ngược lại. Tôi sẽ phải nói thêm một chút logic, tuy nhiên, tôi muốn kết quả API thật tự nhiên — nên tôi không muốn yêu cầu một Race cho một danh sách key Runner, mà chỉ yêu cầu một danh sách Runners mà thôi. Cũng may là việc này không khó. Trong Liệt kê 9, tôi đã thêm vào thể hiện Race hai phương thức. Khi một đối tượng Runner được gửi vào phương thức addRunner, thì id tương ứng của nó được thêm vào danh sách Collection những thuộc tính ids của các runners trong entity (thực thể). Nếu tồn tại một danh sách collection các runners, key của đối tượng Runner mới sẽ được thêm vào danh sách đó; nếu không thì một danh sách Collection mới sẽ được tạo và key của Runner (chính là thuộc tính id trong entity) cũng được thêm vào danh sách. Liệt kê 9. Thêm vào và gọi ra các runners def addRunner(runner){ if(this.@entity.runners){ this.@entity.runners << runner.id }else{ this.@entity.runners = [runner.id] } } def getRunners(){ return this.@entity.runners.collect { new Runner( this.getEntity(Runner.class.simpleName, it) ) } } Khi phương thức getRunners trong Liệt kê 9 được gọi, một danh sách các đối tượng Runner được tạo từ danh sách ids. Do đó, một phương thức mới (getEntity) được định nghĩa trong lớp Model, như trong Liệt kê 10: Liệt kê 10. Tạo một entity từ một id def getEntity(entityType, id){ def key = KeyFactory.createKey(entityType, id) return this.@datastore.get(key) } Phương thức getEntity sử dụng lớp KeyFactory của Google để tạo key cơ bản phục vụ cho việc tìm kiếm các entity riêng lẻ bên trong kho dữ liệu. Cuối cùng, một hàm khởi tạo mới được định nghĩa để tiếp nhận một loại entity, như trong Liệt kê 11: Liệt kê 11. Một hàm khởi tạo mới được thêm vào public Model(Entity entity){ this.@entity = entity } Như bạn thấy trong các Liệt kê 9, 10, và 11, và mô hình đối tượng của Hình 1, tôi có thể thêm một Runner vào bất kỳ Race nào, và tôi cũng có thể lấy danh sách các đối tượng Runner từ bất kỳ Race nào. Trong Liệt kê 12, tôi tạo ra một liên kết tương tự bên phía Runner của biểu thức. Liệt kê 12 hiển thị các phương thức của lớp Runner. Liệt kê 12. Các Runners và Races của chúng def addRace(race){ if(this.@entity.races){ this.@entity.races << race.id }else{ this.@entity.races = [race.id] } } def getRaces(){ return this.@entity.races.collect { new Race( this.getEntity(Race.class.simpleName, it) ) } } Bằng cách này, tôi đã quản lý mô hình hai đối tượng miền domain với một kho dữ liệu schemaless. Kết thúc race với các runners Bây giờ những gì tôi cần phải làm là tạo ra một đối tượng Runner và thêm nó vào một Race. Nếu tôi muốn mối quan hệ là hai chiều, giống như mô hình đối tượng mà tôi đã chỉ ra ở Hình 1, và tôi cũng có thể thêm đối tượng Race vào Runner, như thể hiện ở Liệt kê 13: Liệt kê 13. Các Runners với Races của chúng def runner = new Runner([fname:"Chris", lname:"Smith", date:34]) runner.save() race.addRunner(runner) race.save() runner.addRace(race) runner.save() Sau khi thêm mới một Runner vào race và hàm save của Race, thì kho dữ liệu được cập nhật với một danh sách các ID như thể hiện ở Hình 3: Hình 3. Xem thuộc tính mới của các Runners trong một Race Bằng cách kiểm tra chặt chẽ các dữ liệu trong Google App Engine, bây giờ bạn có thể thấy một thực thể Race có một danh sách các Runners, như được hiển thị ở Hình 4. Hình 4. Xem danh sách các Runners mới Tương tự, trước khi thêm một Race vào một đối tượng Runner mới thì thuộc tính không tồn tại, như thể hiện ở Hình 5. Hình 5. Một Runner không có Race Tuy nhiên, sau khi liên kết một Race với một Runner, kho dữ liệu sẽ bổ sung một danh sách mới các id của race. Hình 6. Một Runner ra khỏi Race Sự linh hoạt của kho dữ liệu schemaless là khả năng làm mới — các thuộc tính sẽ tự động được thêm vào nơi lưu trữ theo yêu cầu. Là một nhà phát triển, tôi không có nhu cầu cập nhật hay thay đổi giản đồ schema, càng không có nhu cầu triển khai nó! Ưu và nhược điểm của NoSQL Tất nhiên mô hình hóa dữ liệu schemaless cũng có cả ưu và nhược điểm. Một lợi thế của ứng dụng Back to the Races (Trở lại đường đua) là nó khá linh hoạt. Nếu tôi quyết định
Tài liệu liên quan