CHAPTER 7

image

MongoDB e-Commerce Database Model

The market for open source e-commerce software keeps on growing every year. For proof, just look at the many popular platforms that are used today as starting points for a variety of e-commerce applications. For example, Magento, Zen Cart, and Spree all provide database schemas ready for storing and querying categories, products, orders, inventories, and so on. Despite the differences among these platforms, they all have something in common: they provide a SQL database.

For NoSQL stores, the e-commerce software market is a challenge, with most NoSQL stores considered inappropriate for e-commerce. MongoDB, however, is robust and flexible, with features like support for rich data models, indexed queries, atomic operations, and replicated writes that prompt us to ask: is MongoDB suitable for e-commerce applications? Well, this question waits for an authoritative answer, which will probably emerge after both the enthusiasm and misconceptions regarding MongoDB’s suitability for e-commerce application begin to wane, and things start to calm down.

It’s generally agreed that MongoDB is fast, reduces the number of tables and associations by using documents (which are conceptually simpler than tables), and provides flexible schemas. But it has some drawbacks that center around transactions, consistency, and durability. SQL databases, in contrast, provide safety, but they’re not that fast, have rigid schemas, need dozens of tables (associations), and can slow development progress (sometimes we need to write complex queries). Nevertheless, it seems that “safety” is the operative word, since no e-seller (e-retailer) wants to lose an order or money because of database inconsistency.

Still, “a full-featured, developer-centric e-commerce platform that makes custom code easy, with powerful templates & expressive syntax”, named Forward (http://getfwd.com/) is ready to show everybody that MongoDB is more than suitable for e-commerce applications. And so, on a smaller scale, I’ll try to sustain this affirmation by developing an e-commerce data model using MongoDB, and using it in an enterprise application based on Hibernate OGM via JPA and Hibernate Search/Apache Lucene.

In this chapter, I’ll look at converting (or adapting) a specific SQL schema for e-commerce applications to a MongoDB schema. In Figure 7-1, you can see a database schema for a medium-complexity e-commerce application; most of the tables are self-explanatory in an e-commerce context. The main tables are categories, products, orders, and users.

9781430257943_Fig07-01.jpg

Figure 7-1. SQL e-commerce database schema

The main goal is to develop a MongoDB database schema similar to the one in Figure 7-1. By similar, I mean that we want to reproduce the main functionality (the same query capabilities), not the same tables, associations and fields. Moreover, we will write the corresponding JPA entities for it. We’re going to use Hibernate OGM via JPA, so we’ll need JPA annotations. And we’ll be using Hibernate Search and Apache Lucene for querying, so we’ll need Hibernate Search-specific annotations for indexing data in Lucene.

Even if you’re not an e-retailer, you’re probably very familiar with many e-commerce terms from the client perspective, especially categories, products, promotions, orders, shopping carts, purchase orders, payment, shipping addresses and so on. Such terms are well-known to every Internet user, so I won’t try to explain them here.

MongoDB E-commerce Database Architecture

In Figure 7-2, you can see the MongoDB e-commerce database architecture I propose, which I named eshop_db. The diagram contains the MongoDB collections, their associations, and the corresponding JPA entities (but not the fields).

9781430257943_Fig07-02.jpg

Figure 7-2. MongoDB E-commerce database schema

Model the Categories Collection (categories_c)

The categories_c collection corresponds to the categories table.

Sorting the products by categories is a common capability on most e-commerce sites. Very likely, the SQL table specific to categories stores the name of each category and a one-to-many (or, sometimes, a many-to-many) lazy association to the table responsible for storing products. The  idea is to load category names very quickly (without their products), since they appear on the first page of the e-commerce web site. The products can be loaded later, after the user chooses a category. But though this works in the case of SQL, in MongoDB you need to be very careful with associations, since they may start transactions. Our aim is to avoid transactions as much as possible, so I didn’t define any association in the categories_c collection.

I created the categories collection (categories_c) with the structure shown in Figure 7-3. As you can see, each document stores an identifier and the category name:

9781430257943_Fig07-03.jpg

Figure 7-3. Document sample from the categories_c collection

The JPA entity for this collection is shown in Listing 7-1.

Listing 7-1.  The JPA Entity for categories_c

1       package eshop.entities;
2
3       import java.io.Serializable;
4       import javax.persistence.Column;
5       import javax.persistence.Entity;
6       import javax.persistence.GeneratedValue;
7       import javax.persistence.Id;
8       import javax.persistence.Table;
9       import org.hibernate.annotations.GenericGenerator;
10      import org.hibernate.search.annotations.Analyze;
11      import org.hibernate.search.annotations.DocumentId;
12      import org.hibernate.search.annotations.Field;
13      import org.hibernate.search.annotations.Index;
14      import org.hibernate.search.annotations.Indexed;
15      import org.hibernate.search.annotations.Store;
16
17      @Entity
18      @Indexed
19      @Table(name = "categories_c")
20      public class Categories implements Serializable {
21
22          private static final long serialVersionUID = 1L;
23          @DocumentId
24          @Id
25          @GeneratedValue(generator = "uuid")
26          @GenericGenerator(name = "uuid", strategy = "uuid2")
27          private String id;
28          @Column(name = "category_name")
29          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.YES)
30          private String category;
31
32          public String getId() {
33              return id;
34          }
35
36          public void setId(String id) {
37              this.id = id;
38          }
39
40          public String getCategory() {
41              return category;
42          }
43
44          public void setCategory(String category) {
45              this.category = category;
46          }
47
48          @Override
49          public int hashCode() {
50              int hash = 0;
51              hash += (id != null ? id.hashCode() : 0);
52              return hash;
53          }
54
55          @Override
56          public boolean equals(Object object) {
57              if (!(object instanceof Categories)) {
58                  return false;
59              }
60              Categories other = (Categories) object;
61              if ((this.id == null && other.id != null) || (this.id != null &&
                  !this.id.equals(other.id))) {
62                  return false;
63              }
64              return true;
65          }
66
67          @Override
68          public String toString() {
69              return "eshop.entities.Categories[ id=" + id + " ]";
70          }
71      }
72

Notice that line 29 specifies that the category id (id field) and category name (category_name field) should be searchable with Lucene and disables analyzers. We don’t need analyzers because we search the category as is (not by the words it contains), and we’ll sort the categories by name (Lucene doesn’t let you analyze fields used for sorting operations). Moreover, category names are stored in the Lucene index. This consumes space in the index, but not a considerable amount, since you won’t want so many categories as to cause concern. This allows us to take advantage of projection (notice that the ids are automatically stored). Using projection allows us, in the future, to add more searchable, non-lazy fields to this collection, such as category code, category description, and so on, but still extract only the categories names. Of course, this is just an approach (not a rule) specific to Lucene. If you choose to use JP-QL queries (when Hibernate OGM provides support for such queries), things will be different.

Model The Products Collection (products_c)

The products_c collection corresponds to the products and productoptions tables.

In the collection dedicated to products (products_c), the document for each product stores two kinds of information: general data, such as SKU, name, price, description and so on; and the kind of data that in a relational model usually needs additional tables, such as a product’s gallery and a product’s options (for example, colors, sizes, types, and so on). Instead of using additional tables and associations, I’m going to store each product’s gallery and options in embedded collections. This makes sense, because these physical details are unique features of the product. Moreover, the products_c collection is the owner side of the unidirectional many-to-one association with the categories_c collection, so it stores the foreign keys of the corresponding categories.

In Figure 7-4, you can see such a document sample.

9781430257943_Fig07-04.jpg

Figure 7-4. Sample document from products_c collection

Each product will be represented by such a document. The colors and sizes embedded collections will be visible only for products that have these options.

The JPA entity for this collection is shown in Listing 7-2.

Listing 7-2.  The JPA entity for products_c

1        package eshop.entities;
2
3        import java.io.Serializable;
4        import java.util.ArrayList;
5        import java.util.List;
6        import javax.persistence.Column;
7        import javax.persistence.ElementCollection;
8        import javax.persistence.Entity;
9        import javax.persistence.FetchType;
10       import javax.persistence.GeneratedValue;
11       import javax.persistence.Id;
12       import javax.persistence.ManyToOne;
13       import javax.persistence.Table;
14       import org.hibernate.annotations.GenericGenerator;
15       import org.hibernate.search.annotations.Analyze;
16       import org.hibernate.search.annotations.DocumentId;
17       import org.hibernate.search.annotations.Field;
18       import org.hibernate.search.annotations.Index;
19       import org.hibernate.search.annotations.Indexed;
20       import org.hibernate.search.annotations.IndexedEmbedded;
21       import org.hibernate.search.annotations.NumericField;
22       import org.hibernate.search.annotations.Store;
23
24       @Entity
25       @Indexed
26       @Table(name = "products_c")
27       public class Products implements Serializable {
28
29           private static final long serialVersionUID = 1L;
30           @DocumentId
31           @Id
32           @GeneratedValue(generator = "uuid")
33           @GenericGenerator(name = "uuid", strategy = "uuid2")
34           private String id;
35           @Column(name = "product_sku")
36           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
37           private String sku;
38           @Column(name = "product_name")
39           @Field(index = Index.YES, analyze = Analyze.YES, store = Store.NO)
40           private String product;
41           @Column(name = "product_price")
42           @NumericField
43           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
44           private double  price;
45           @Column(name = "product_old_price")
46           @NumericField
47           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
48           private double old_price;
49           @Column(name = "product_description")
50           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
51           private String description;
52           @IndexedEmbedded
53           @ManyToOne(fetch = FetchType.LAZY)
54           private Categories category;
55           @IndexedEmbedded
56           @ElementCollection(targetClass = java.lang.String.class,
                              fetch = FetchType.EAGER)
57           @Column(name = "product_gallery")
58           private List<String> gallery = new ArrayList<String>();
59           @IndexedEmbedded
60           @ElementCollection(targetClass = java.lang.String.class,
                              fetch = FetchType.EAGER)
61           @Column(name = "product_colors")
62           private List<String> colors = new ArrayList<String>();
63           @IndexedEmbedded
64           @ElementCollection(targetClass = java.lang.String.class,
                              fetch = FetchType.EAGER)
65           @Column(name = "product_sizes")
66           private List<String> sizes = new ArrayList<String>();
67
68           public String getId() {
69               return id;
70           }
71
72           public void setId(String id) {
73               this.id = id;
74           }
75
76           public List<String> getGallery() {
77               return gallery;
78           }
79
80           public void setGallery(List<String> gallery) {
81               this.gallery = gallery;
82           }
83
84           public double getPrice() {
85               return price;
86           }
87
88           public void setPrice(double price) {
89               this.price = price;
90           }
91
92           public double getOld_price() {
93               return old_price;
94           }
95
96           public void setOld_price(double old_price) {
97               this.old_price = old_price;
98           }
99
100          public String getProduct() {
101              return product;
102          }
103
104          public void setProduct(String product) {
105              this.product = product;
106          }
107
108          public String getSku() {
109              return sku;
110          }
111
112          public void setSku(String sku) {
113              this.sku = sku;
114          }
115
116          public String getDescription() {
117              return description;
118          }
119
120          public List<String> getColors() {
121              return colors;
122          }
123
124          public void setColors(List<String> colors) {
125              this.colors = colors;
126          }
127
128          public List<String> getSizes() {
129              return sizes;
130          }
131
132          public void setSizes(List<String> sizes) {
133              this.sizes = sizes;
134          }
135
136          public void setDescription(String description) {
137              this.description = description;
138          }
139
140          public Categories getCategory() {
141              return category;
142          }
143
144          public void setCategory(Categories category) {
145              this.category = category;
146          }
147
148          @Override
149          public int hashCode() {
150              int hash = 0;
151              hash += (id != null ? id.hashCode() : 0);
152              return hash;
153          }
154
155          @Override
156          public boolean equals(Object object) {
157              if (!(object instanceof Products)) {
158                  return false;
159              }
160              Products other = (Products) object;
161              if ((this.id == null && other.id != null) || (this.id != null &&
                    !this.id.equals(other.id))) {
162                  return false;
163              }
164              return true;
165          }
166
167          @Override
168          public String toString() {
169              return "eshop.entities.Products[ id=" + id + " ]";
170          }
171      }

Let’s take a closer look at some of the main lines of code.

In line 39, the field that corresponds to the product name (product_name) is prepared for Lucene. The part we want to note is analyze = Analyze.YES, which tells Lucene to use the default analyzer for this field. Instead of searching for a product by name (which is usually composed of several words), we can search for it by any of the words its name contains. This helps us easily implement a “search by product name” facility.

As you can see, in lines 42 and 48 the product prices (product_price and product_old_price) are numerical values (doubles). It makes sense to store them as numbers instead of strings so you can perform range queries and calculations, like subtotals, totals, currency conversions and so on. You can tell Lucene that a field represents numerical values by annotating it with @NumericField. When a property is indexed as a numeric field, it enables efficient range querying, and sorting is faster than doing the same query on standard @Field properties.

Lines 52-54 define a unidirectional, many-to-one association between the categories_c and products_c collections. For Lucene, this association should be marked as @IndexedEmbedded, which is used to index associated entities as part of the owning entity. Probably I’ve said this before, but it’s a good moment to point out again that Lucene is not aware of associations, which is why it needs the @IndexedEmbedded and @ContainedIn annotations. Without these annotations, associations like @ManyToMany, @*ToOne, @Embedded, and @ElementCollection will not be indexed and, therefore, will not be searchable. Associations let you easily write Lucene queries similar to SQL queries that contain the WHERE clause, of the type: select all products from a category where the category field equals something (which in JP-QL is usually a join).

Lines 55-66 define the product’s options and gallery of images. For this example, we used the most common options, color and size, but you can add more. Instead of placing them into another table and creating another association, I prefer to store them using @ElementCollection. When a product doesn’t have color or size, it’s just skipped. MongoDB documents allow a flexible structure, so when an option isn’t specified, the corresponding collection will not be present in document. As a final observation, we’re loading the options and gallery using the eager mechanism, because we want to load and display each product with its gallery and options. If you want to load the products in two phases: first a brief overview of the products and then, by user request, the options, use the lazy mechanism instead.

Model the Customers Collection (customers_c)

The customers_c collection corresponds to the users table.

For users (potential customers), we need a separate collection for storing personal data; we name this collection customers_c. Personal data includes information such as name, surname, e-mail address, password, addresses and so on (obviously, you can add more fields). When a user logs into the system, you can easily indentify him by e-mail address and password and load his profile. His orders are not loaded in the same query as his profile. They are loaded lazily only when an explicit request is performed; this allows us to load only the requested orders, not all. Usually, a customer checks just his most recent order status and rarely wants to view an obsolete order. Many e-commerce sites don’t provide access to obsolete orders, only to the most recent one.

Each document (entry) in the customers_c collection looks like what’s shown in Figure 7-5.

9781430257943_Fig07-05.jpg

Figure 7-5. Sample document from the customers_c collection

Notice that the customer’s addresses are stored as embedded documents; this lets us provide multiple addresses without additional tables, using fast queries and lazy loading.

The JPA entity for this collection is shown in Listing 7-3.

Listing 7-3.  The JPA Entity for customers_c

1        package eshop.entities;
2
3        import eshop.embedded.Addresses;
4        import java.io.Serializable;
5        import java.util.Date;
6        import javax.persistence.Basic;
7        import javax.persistence.Column;
8        import javax.persistence.Embedded;
9        import javax.persistence.Entity;
10       import javax.persistence.FetchType;
11       import javax.persistence.GeneratedValue;
12       import javax.persistence.Id;
13       import javax.persistence.Table;
14       import javax.persistence.Temporal;
15       import org.hibernate.annotations.GenericGenerator;
16       import org.hibernate.search.annotations.Analyze;
17       import org.hibernate.search.annotations.DateBridge;
18       import org.hibernate.search.annotations.DocumentId;
19       import org.hibernate.search.annotations.Field;
20       import org.hibernate.search.annotations.Index;
21       import org.hibernate.search.annotations.Indexed;
22       import org.hibernate.search.annotations.IndexedEmbedded;
23       import org.hibernate.search.annotations.Resolution;
24       import org.hibernate.search.annotations.Store;
25
26       @Entity
27       @Indexed
28       @Table(name = "customers_c")
29       public class Customers implements Serializable {
30
31           private static final long serialVersionUID = 1L;
32           @DocumentId
33           @Id
34           @GeneratedValue(generator = "uuid")
35           @GenericGenerator(name = "uuid", strategy = "uuid2")
36           private String id;
37           @Column(name = "customer_email")
38           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
39           private String email;
40           @Column(name = "customer_password")
41           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
42           private String password;
43           @Column(name = "customer_name")
44           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
45           private String name;
46           @Column(name = "customer_surname")
47           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
48           private String surname;
49           @DateBridge(resolution = Resolution.DAY)
50           @Temporal(javax.persistence.TemporalType.DATE)
51           @Column(name = "customer_registration")
52           private Date registration;
53           @Embedded
54           @IndexedEmbedded
55           @Basic(fetch = FetchType.LAZY)
56           private Addresses customer_address_1;
57           @Embedded
58           @IndexedEmbedded
59           @Basic(fetch = FetchType.LAZY)
60           private Addresses customer_address_2;
61
62           public String getId() {
63               return id;
64           }
65
66           public void setId(String id) {
67               this.id = id;
68           }
69
70           public String getEmail() {
71               return email;
72           }
73
74           public void setEmail(String email) {
75               this.email = email;
76           }
77
78           public String getPassword() {
79               return password;
80           }
81
82           public void setPassword(String password) {
83               this.password = password;
84           }
85
86           public String getName() {
87               return name;
88           }
89
90           public void setName(String name) {
91               this.name = name;
92           }
93
94           public String getSurname() {
95               return surname;
96           }
97
98           public void setSurname(String surname) {
99               this.surname = surname;
100          }
101
102          public Date getRegistration() {
103              return registration;
104          }
105
106          public void setRegistration(Date registration) {
107              this.registration = registration;
108          }
109
110          public Addresses getCustomer_address_1() {
111              return customer_address_1;
112          }
113
114          public void setCustomer_address_1(Addresses customer_address_1) {
115              this.customer_address_1 = customer_address_1;
116          }
117
118          public Addresses getCustomer_address_2() {
119              return customer_address_2;
120          }
121
122          public void setCustomer_address_2(Addresses customer_address_2) {
123              this.customer_address_2 = customer_address_2;
124          }
125
126          @Override
127          public int hashCode() {
128              int hash = 0;
129              hash += (id != null ? id.hashCode() : 0);
130              return hash;
131          }
132
133          @Override
134          public boolean equals(Object object) {
135              if (!(object instanceof Customers)) {
136                  return false;
137              }
138              Customers other = (Customers) object;
139              if ((this.id == null && other.id != null) || (this.id != null &&
                    !this.id.equals(other.id))) {
140                  return false;
141              }
142              return true;
143          }
144
145          @Override
146          public String toString() {
147              return "eshop.entities.Customers[ id=" + id + " ]";
148          }
149      }
150

There are important aspects of this code that deserve explanation.

The code in lines 53-60 is pretty interesting. As you can see, the same embeddable object type appears twice in the same entity (the embeddable object maps the address coordinates, city, zip, street and so on in a class named Addresses). If you’ve used this technique with SQL and JPA providers such as EclipseLink or Hibernate, you know you had to set at least one of the columns explicitly, because the column name default will not work. In this case, generic JPA fixes the issue with the @AttributeOverride annotation (see www.docs.oracle.com/javaee/6/api/javax/persistence/AttributeOverride.html). In NoSQL and Hibernate OGM, however, you don’t need to use this adjustment to column names.

The embeddable class representing an address is shown in Listing 7-4.

Listing 7-4.  The Embeddable Addresses Class

1       package eshop.embedded;
2
3       import java.io.Serializable;
4       import javax.persistence.Embeddable;
5       import org.hibernate.search.annotations.Analyze;
6       import org.hibernate.search.annotations.Field;
7       import org.hibernate.search.annotations.Index;
8       import org.hibernate.search.annotations.Store;
9
10      @Embeddable
11      public class Addresses implements Serializable {
12
13          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
14          private String city;
15          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
16          private String state;
17          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
18          private String street;
19          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
20          private String number;
21          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
22          private String zip;
23          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
24          private String country;
25          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
26          private String phone;
27          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
28          private String fax;
29
30          public String getCity() {
31              return city;
32          }
33
34          public void setCity(String city) {
35              this.city = city;
36          }
37
38          public String getNumber() {
39              return number;
40          }
41
42          public void setNumber(String number) {
43              this.number = number;
44          }
45
46          public String getState() {
47              return state;
48          }
49
50          public void setState(String state) {
51              this.state = state;
52          }
53
54          public String getStreet() {
55              return street;
56          }
57
58          public void setStreet(String street) {
59              this.street = street;
60          }
61
62          public String getZip() {
63              return zip;
64          }
65
66          public void setZip(String zip) {
67              this.zip = zip;
68          }
69
70          public String getCountry() {
71              return country;
72          }
73
74          public void setCountry(String country) {
75              this.country = country;
76          }
77
78          public String getPhone() {
79              return phone;
80          }
81
82          public void setPhone(String phone) {
83              this.phone = phone;
84          }
85
86          public String getFax() {
87              return fax;
88          }
89
90          public void setFax(String fax) {
91              this.fax = fax;
92          }
93      }

Model The Orders Collection (orders_c)

The orders_c collection corresponds to the orders and details tables.

The orders are stored in a separate collection, named orders_c. For each order, we store status (an order can pass through multiple statuses, such as PURCHASED, SHIPPED, CANCELED and so on); subtotal (this represents the order value in money); order creation date; shipping address; and the order’s products. You can add more fields, such as an order identifier (#nnnn, for example), an order friendly name, an order expiration date, and so on.

The shipping address is represented by an embedded document and the order’s products are stored as an embedded collection. Therefore, we don’t need supplementary collections or associations, the queries are very easy to perform, and we can load the shipping address and the order’s products either lazily or eagerly, depending on how we implement the web site GUI.

In this collection, we need to store the foreign keys that indicate the customers who purchased the orders. For this I defined a unidirectional many-to-one association between orders and customers.

I haven’t yet said anything about the current shopping cart—the order hasn’t been submitted yet. The shopping cart can support multiple content modifications in a single (or multiple) session(s) of a customer, adding new products, deleting others, clearing the cart, modifying a product’s quantity, and so forth. It’s not useful to reflect all of these modifications in the database, since each requires at least one query for updating the “conversation” between customer and shopping cart. For this, you can take a programmatic approach, storing the shopping cart in a customer session, or in a view scope or conversational scope. You can also use cookies, or any specific design pattern that can help implement this task. The idea is to modify the database only when an order is actually placed.

Of course, if your data is highly critical or you need to persist over multiple sessions (for example, if the user might come back after a week), then it’s a good idea to persist the shopping cart to the database using a separate collection or as a document inside the orders_c collection. After all, a shopping cart is just an order that has not been placed, so it can be stored like a normal order with a status of, perhaps, unpurchased. If you decide to persist the shopping cart, be careful to correctly synchronize it with the inventory. This is mandatory for preventing “overselling;” the application must move items from inventory to the cart and back to again in some cases, for instance if the user drops one or more products or even abandons the whole purchase. Taking a product from inventory and moving it to the cart (or the reverse) is an operation specific to transactions, so you have to deal with rollback issues. Obviously, if you don’t have an inventory, things are much simpler.

In Figure 7-6, you can see a document sample for an order.

9781430257943_Fig07-06.jpg

Figure 7-6. Sample document from the orders_c collection

By convention, when a product does not have color or size, we store a flag like “Unavailable”.

The JPA entity for this collection is shown in Listing 7-5:

Listing 7-5.  The JPA Entity for orders_c

1        package eshop.entities;
2
3        import eshop.embedded.Addresses;
4        import eshop.embedded.CartProducts;
5        import java.io.Serializable;
6        import java.util.ArrayList;
7        import java.util.Date;
8        import java.util.List;
9        import javax.persistence.AttributeOverride;
10       import javax.persistence.AttributeOverrides;
11       import javax.persistence.Basic;
12       import javax.persistence.Column;
13       import javax.persistence.ElementCollection;
14       import javax.persistence.Embedded;
15       import javax.persistence.Entity;
16       import javax.persistence.FetchType;
17       import javax.persistence.GeneratedValue;
18       import javax.persistence.Id;
19       import javax.persistence.ManyToOne;
20       import javax.persistence.Table;
21       import javax.persistence.Temporal;
22       import org.hibernate.annotations.GenericGenerator;
23       import org.hibernate.search.annotations.Analyze;
24       import org.hibernate.search.annotations.DateBridge;
25       import org.hibernate.search.annotations.DocumentId;
26       import org.hibernate.search.annotations.Field;
27       import org.hibernate.search.annotations.Index;
28       import org.hibernate.search.annotations.Indexed;
29       import org.hibernate.search.annotations.IndexedEmbedded;
30       import org.hibernate.search.annotations.NumericField;
31       import org.hibernate.search.annotations.Resolution;
32       import org.hibernate.search.annotations.Store;
33
34       @Entity
35       @Indexed
36       @Table(name = "orders_c")
37       public class Orders implements Serializable {
38
39           private static final long serialVersionUID = 1L;
40           @DocumentId
41           @Id
42           @GeneratedValue(generator = "uuid")
43           @GenericGenerator(name = "uuid", strategy = "uuid2")
44           private String id;
45           @Column(name = "order_status")
46           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
47           private String status;
48           @Column(name = "order_subtotal")
49           @NumericField
50           @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
51           private double subtotal;
52           @DateBridge(resolution = Resolution.HOUR)
53           @Temporal(javax.persistence.TemporalType.DATE)
54           private Date orderdate;
55           @Embedded
56           @IndexedEmbedded
57           @Basic(fetch = FetchType.EAGER)
58           private Addresses shipping_address;
59           @IndexedEmbedded
60           @ElementCollection(targetClass = eshop.embedded.CartProducts.class,
61           fetch = FetchType.EAGER)
62           @AttributeOverrides({
63               @AttributeOverride(name = "sku",
64               column =
65               @Column(name = "product_sku")),
66               @AttributeOverride(name = "name",
67               column =
68               @Column(name = "product_name")),
69               @AttributeOverride(name = "price",
70               column =
71               @Column(name = "product_price")),
72               @AttributeOverride(name = "color",
73               column =
74               @Column(name = "product_color")),
75               @AttributeOverride(name = "size",
76               column =
77               @Column(name = "product_size")),
78               @AttributeOverride(name = "quantity",
79               column =
80               @Column(name = "product_quantity")),
81               @AttributeOverride(name = "uin",
82               column =
83               @Column(name = "unique_identification_number")),})
84           private List<CartProducts> cart = new ArrayList<CartProducts>(0);
85           @IndexedEmbedded
86           @ManyToOne(fetch = FetchType.LAZY)
87           private Customers customer;
88
89           public String getId() {
90               return id;
91           }
92
93           public void setId(String id) {
94               this.id = id;
95           }
96
97           public String getStatus() {
98               return status;
99           }
100
101          public void setStatus(String status) {
102              this.status = status;
103          }
104
105          public Addresses getShipping_address() {
106              return shipping_address;
107          }
108
109          public void setShipping_address(Addresses shipping_address) {
110              this.shipping_address = shipping_address;
111          }
112
113          public List<CartProducts> getCart() {
114              return cart;
115          }
116
117          public void setCart(List<CartProducts> cart) {
118              this.cart = cart;
119          }
120
121          public Customers getCustomer() {
122              return customer;
123          }
124
125          public void setCustomer(Customers customer) {
126              this.customer = customer;
127          }
128
129          @Override
130          public int hashCode() {
131              int hash = 0;
132              hash += (id != null ? id.hashCode() : 0);
133              return hash;
134          }
135
136          public double getSubtotal() {
137              return subtotal;
138          }
139
140          public void setSubtotal(double subtotal) {
141              this.subtotal = subtotal;
142          }
143
144          public Date getOrderdate() {
145              return orderdate;
146          }
147
148          public void setOrderdate(Date orderdate) {
149              this.orderdate = orderdate;
150          }
151
152          @Override
153          public boolean equals(Object object) {
154              if (!(object instanceof Orders)) {
155                  return false;
156              }
157              Orders other = (Orders) object;
158              if ((this.id == null && other.id != null) || (this.id != null &&
                    !this.id.equals(other.id))) {
159                  return false;
160              }
161              return true;
162          }
163
164          @Override
165          public String toString() {
166              return "eshop.entities.Orders[ id=" + id + " ]";
167          }
168      }

Let’s discuss the main lines of code for this entity.

Lines 55-58 represent the mapping of the shipping address. As you can see, I prefer to use an embedded document for each order. I loaded it eagerly, but lazy loading is also an option, depending on what you want to display when you load an order.

From the Lucene perspective, I need the @IndexedEmbedded annotation, because I want to index this embeddable class as part of the owning entity. The Addresses embeddable class (annotated with @Embeddable) is shown above in Listing 7-4.

In lines 59-84, an element-collection (mapped in MongoDB as an embedded collection) stores an order’s products. The type of the element-collection is an embeddable class. The main thing to notice here is that I’ve used the @AttributeOverrides annotation; if we don’t override the columns names of the embeddable collection, they default to something like cart.collection&&element.price. This is not very friendly, so @AttributeOverrides can be very useful in such cases.

This embeddable class is named CartProducts and is shown in Listing 7-6.

Listing 7-6.  The Embeddable CartProducts Class

1       package eshop.embedded;
2
3       import java.io.Serializable;
4       import javax.persistence.Embeddable;
5       import org.hibernate.search.annotations.Analyze;
6       import org.hibernate.search.annotations.Field;
7       import org.hibernate.search.annotations.Index;
8       import org.hibernate.search.annotations.NumericField;
9       import org.hibernate.search.annotations.Store;
10
11      @Embeddable
12      public class CartProducts implements Serializable {
13
14          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
15          private String sku;
16          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
17          private String name;
18          @NumericField
19          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
20          private double price;
21          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
22          private String color;
23          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
24          private String size;
25          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
26          private String quantity;
27          @Field(index = Index.YES, analyze = Analyze.NO, store = Store.NO)
28          private String uin;
29
30          public String getSku() {
31              return sku;
32          }
33
34          public void setSku(String sku) {
35              this.sku = sku;
36          }
37
38          public String getName() {
39              return name;
40          }
41
42          public void setName(String name) {
43              this.name = name;
44          }
45
46          public double getPrice() {
47              return price;
48          }
49
50          public void setPrice(double price) {
51              this.price = price;
52          }
53
54          public String getColor() {
55              return color;
56          }
57
58          public void setColor(String color) {
59              this.color = color;
60          }
61
62          public String getSize() {
63              return size;
64          }
65
66          public void setSize(String size) {
67              this.size = size;
68          }
69
70          public String getQuantity() {
71              return quantity;
72          }
73
74          public void setQuantity(String quantity) {
75              this.quantity = quantity;
76          }
77
78          public String getUin() {
79              return uin;
80          }
81
82          public void setUin(String uin) {
83              this.uin = uin;
84          }
85      }
86

From the Lucene perspective, we need the @IndexedEmbedded annotation because we want to index this embeddable collection in the entity owner index.

Lines 85-87 define the unidirectional association between the orders_c and customers_c collections. For Lucene, this association should be marked as @IndexedEmbedded, which is used to index associated entities as part of the owning entity. This association allows us to easily write Lucene queries similar to SQL queries that contain the WHERE clause, of the type: select all orders from an order where the customer field equals something (which, in JP-QL, is usually a join).

Model The Inventory Collection (inventory_c)

This collection doesn’t have a corresponding table in Figure 7-1. Not all e-commerce sites need inventory management. But, for those that do, MongoDB provides a few solutions. One solution is to store a separate document for each physical product in the warehouse. This will prevent concurrent access to data, since every document will have a unique lock on that product. In this approach, we rely on the fact that MongoDB supports atomic operations on individual documents. For cases where the warehouse doesn’t contain too many products (and this depends on your definition of “too many”), this approach will work quite well.

Another approach is to store a document for a group of identical products and use a field in this document to represent the number of products. In this case, you need to deal with the situation of multiple users updating this field, by extracting or returning a product from the same group (there’s also an administrator who occasionally repopulates the inventory). I choose this approach and deal with concurrent updates by using optimistic locking. If you need to lock a document for your exclusive use until you’ve finished with it, use pessimistic locking, but be carefully to avoid (or deal with) deadlocks. In general, optimistic locking is good when you don’t expect imminent collisions but, since the transaction is aborted (not rolled back), you need to pay the price and deal with it somehow. On the other hand, pessimistic locking is used when a collision is anticipated, and it’s used when collisions are imminent. It can be pretty tricky to decide which locking option to choose, but here’s a rule of thumb: use pessimistic locking if you have to guarantee the integrity of important data, like banking data, and use optimistic locking for everything else.

The MongoDB collection for storing inventory is named inventory_c. For each group of identical products, I’ve created a composite key from the product SKU and the color and size. Besides the id, each document contains a numeric field for storing the number of available products, named inventory. The version field is used for optimistic locking. See Figure 7-7.

9781430257943_Fig07-07.jpg

Figure 7-7. Sample document from the customers_c collection showing the inventory field

The JPA entity for inventory_c is shown in Listing 7-7.

Listing 7-7.  The JPA Entity for inventory_c

1       package eshop.entities;
2
3       import java.io.Serializable;
4       import javax.persistence.Column;
5       import javax.persistence.Entity;
6       import javax.persistence.Id;
7       import javax.persistence.IdClass;
8       import javax.persistence.Table;
9       import javax.persistence.Version;
10
11      @Entity
12      @IdClass(eshop.embedded.InventoryPK.class)
13      @Table(name = "inventory_c")
14      public class Inventory implements Serializable {
15
16          private static final long serialVersionUID = 1L;
17          @Id
18          private String sku;
19          @Id
20          private String sku_color;
21          @Id
22          private String sku_size;
23          @Version
24          private Long version;
25          @Column(name = "inventory")
26          private int inventory;
27
28          public int getInventory() {
29              return inventory;
30          }
31
32          public void setInventory(int inventory) {
33              this.inventory = inventory;
34          }
35
36          public String getSku() {
37              return sku;
38          }
39
40          public void setSku(String sku) {
41              this.sku = sku;
42          }
43
44          public String getSku_color() {
45              return sku_color;
46          }
47
48          public void setSku_color(String sku_color) {
49              this.sku_color = sku_color;
50          }
51
52          public String getSku_size() {
53              return sku_size;
54          }
55
56          public void setSku_size(String sku_size) {
57              this.sku_size = sku_size;
58          }
59
60          public Long getVersion() {
61              return version;
62          }
63
64          protected void setVersion(Long version) {
65              this.version = version;
66          }
67
68          @Override
69          public int hashCode() {
70              int hash = 7;
71              hash = 13 * hash + (this.sku != null ? this.sku.hashCode() : 0);
72              return hash;
73          }
74
75          @Override
76          public boolean equals(Object obj) {
77              if (obj == null) {
78                  return false;
79              }
80              if (getClass() != obj.getClass()) {
81                  return false;
82              }
83              final Inventory other = (Inventory) obj;
84              if ((this.sku == null) ? (other.sku != null) :
                   !this.sku.equals(other.sku)) {
85                  return false;
86              }
87              return true;
88          }
89      }

And the composite key class is:

1       package eshop.embedded;
2
3       import java.io.Serializable;
4
5       public class InventoryPK implements Serializable{
6
7           private String sku;
8           private String sku_color;
9           private String sku_size;
10
11          public InventoryPK(){
12          }
13
14          public InventoryPK(String sku, String sku_color, String sku_size) {
15              this.sku = sku;
16              this.sku_color = sku_color;
17              this.sku_size = sku_size;
18          }
19
20          @Override
21          public int hashCode() {
22              int hash = 7;
23              hash = 83 * hash + (this.sku != null ? this.sku.hashCode() : 0);
24              hash = 83 * hash + (this.sku_color != null ?
               this.sku_color.hashCode() : 0);
25              hash = 83 * hash + (this.sku_size != null ?
               this.sku_size.hashCode() : 0);
26              return hash;
27          }
28
29          @Override
30          public boolean equals(Object obj) {
31              if (obj == null) {
32                  return false;
33              }
34              if (getClass() != obj.getClass()) {
35                  return false;
36              }
37              final InventoryPK other = (InventoryPK) obj;
38              if ((this.sku == null) ? (other.sku != null) :
                   !this.sku.equals(other.sku)) {
39                  return false;
40              }
41              if ((this.sku_color == null) ? (other.sku_color != null) :
                   !this.sku_color.equals(other.sku_color)) {
42                  return false;
43              }
44              if ((this.sku_size == null) ? (other.sku_size != null) :
                   !this.sku_size.equals(other.sku_size)) {
45                  return false;
46              }
47              return true;
48          }
49      }

Summary

In this chapter, you saw my proposal for a MongoDB e-commerce database. Of course, this is just a sketch that, obviously, is open for improvement. I presented the proposed architecture and the database collections, and we’ve created the necessary entities and embeddable classes. In the next chapter, we’ll continue to develop an enterprise application based on this database architecture.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset