Slide 1

Slide 1 text

Dropping ACID Schema Design for e-Commerce

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Why e-Commerce? • Data retention requirements make for complex schemas • Lots of existing examples, good and bad (Magento, Sylius, Spryker, etc.) • It’s catchy!

Slide 4

Slide 4 text

Databases

Slide 5

Slide 5 text

Databases Choose Your Poison • SQL-based vs. “NoSQL” • Tabular storage vs. Rich models • Performance vs. Durability • Owned vs. Cloud Platform

Slide 6

Slide 6 text

Databases The Trusted One • MySQL has been around for ages • LAMP, WAMP, MAMP packages make entry easy • Has caught up with other databases somewhat • Still owned by Oracle

Slide 7

Slide 7 text

Databases The Title Contender • PostgreSQL is being used more and more • Features like Custom Types are huge • Allows for creating rich, nested data structures • Allows querying those structures • Somewhat held back by MySQL compatibility in Libraries

Slide 8

Slide 8 text

Databases Webscale • MongoDB stores your data…and gives it back to you • Document model requires different approach to schema design • Lacks integration to important tools • Supporting it efficiently needs code changes

Slide 9

Slide 9 text

Databases Finding Common Ground • All allow inserting simple data • All support inserting rich data • All are relational • All support clustering • All support transactions • Only one is Web Scale

Slide 10

Slide 10 text

Relationship Advice

Slide 11

Slide 11 text

Relationship Advice References • Pointer to another entity • Referential integrity optional (MyISAM, MongoDB) • RDBMS is built around this concept • MongoDB has rudimental support

Slide 12

Slide 12 text

Relationship Advice The Case for References • References are supported in all databases • Implementations vary (e.g. DBRef in MongoDB) • Referential integrity ensures consistency • Reads across references possible in MongoDB, but different

Slide 13

Slide 13 text

Relationship Advice Embedded Data • Not just a pointer to another entity, but all data • Persistence is managed by the owning entity • Not useful if data is needed by itself

Slide 14

Slide 14 text

Relationship Advice Embed, don’t Reference • Embedding data groups entities together • Joining other entities no longer necessary • Limited support in MySQL (JSON types) • Interesting concepts in PostgreSQL (Custom Types) • MongoDB is built around this concept • This includes all flaws

Slide 15

Slide 15 text

Relationship Advice Why not both? • Hybrid relationships combine references and embedded data • Stores reference to original entity, but duplicates data • May require additional writes to update such data • Reads can be optimised to avoid joins • Only useful if libraries play along (e.g. proxy objects)

Slide 16

Slide 16 text

Model Your Data

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

Create Your Tables

Slide 21

Slide 21 text

A Simple Schema CREATE TABLE sylius_product_option (...); CREATE TABLE sylius_product_option_value (...); CREATE TABLE sylius_product (...); CREATE TABLE sylius_product_options (...); CREATE TABLE sylius_product_variant (...); CREATE TABLE sylius_product_variant_option_value (...); CREATE TABLE sylius_order (...); CREATE TABLE sylius_order_item (...); CREATE TABLE sylius_order_item_unit (...);

Slide 22

Slide 22 text

Normalise a Little More? CREATE TABLE product ( id INT PRIMARY KEY, title VARCHAR(255) NOT NULL ); CREATE TABLE product_description ( id INT PRIMARY KEY, description TEXT NOT NULL ); CREATE TABLE product_option ( id INT PRIMARY KEY, name VARCHAR(255) NOT NULL ); CREATE TABLE product_options ( product_id INT NOT NULL, product_option_id INT NOT NULL );

Slide 23

Slide 23 text

Normalise a Little Less? CREATE TABLE product_option ( id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, ... ); CREATE TABLE product ( id INT PRIMARY KEY, title VARCHAR(255) NOT NULL, ..., options TEXT NOT NULL ); INSERT INTO product VALUES (1, 'Shoe', ..., '1, 2, 3');

Slide 24

Slide 24 text

Normalise Your Data

Slide 25

Slide 25 text

Normal forms • Unnormalised data: “Just do whatever” • 1NF: “Domain of each attribute contains only atomic values” • 3NF: “No non-transitive dependencies” • … • 6NF: “Row contains the Primary Key, and at most one other attribute”

Slide 26

Slide 26 text

Pros of Normalisation • Atomic columns reduce complexity • Reads can be more efficient (reading less data is faster) • Atomic, non-conflicting updates are facilitated • Data can seem more organised

Slide 27

Slide 27 text

Cons of Normalisation • Data can seem disorganised • Queries may become more complex due to joins • Added complexity of writes to multiple tables

Slide 28

Slide 28 text

Write to the Database Spot the Problem INSERT INTO sylius_product VALUES (...); INSERT INTO sylius_product_options VALUES (...); INSERT INTO sylius_product_variant VALUES (...); INSERT INTO sylius_product_variant_option_value (...);

Slide 29

Slide 29 text

Have Some ACID

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

ACID Atomicity Consistency Isolation Durability

Slide 32

Slide 32 text

Write to the Database INSERT INTO sylius_product VALUES (...); INSERT INTO sylius_product_options VALUES (...); INSERT INTO sylius_product_variant VALUES (...); INSERT INTO sylius_product_variant_option_value (...); START TRANSACTION; COMMIT;

Slide 33

Slide 33 text

You Don’t NEED Transactions

Slide 34

Slide 34 text

You Need a Different Schema

Slide 35

Slide 35 text

Stop Using References

Slide 36

Slide 36 text

Address Data CREATE TABLE customer ( id INT PRIMARY KEY, ..., ); CREATE TABLE customer_address ( id INT PRIMARY KEY, customer_id INT NOT NULL, address TEXT NOT NULL, city TEXT NOT NULL, ... ); CREATE TABLE order ( id INT PRIMARY KEY, shipping_address INT NOT NULL, billing_address INT NOT NULL, ... );

Slide 37

Slide 37 text

Address Data Using Doctrine Embeddables CREATE TABLE order ( id INT PRIMARY KEY, shipping_address TEXT NOT NULL, shipping_address_city TEXT NOT NULL, ..., billing_address TEXT NOT NULL, billing_address_city TEXT NOT NULL, ... );

Slide 38

Slide 38 text

Address Data Using JSON/JSONb CREATE TABLE order ( id INT PRIMARY KEY, shipping_address JSON NOT NULL, billing_address JSON NOT NULL, ... ); INSERT INTO order VALUES ( 1, '{ "address": "", "postCode": "", "city": "", "country": "Germany" }', ... );

Slide 39

Slide 39 text

Address Data Using Custom Types CREATE TYPE address AS ( id INT, address TEXT, city TEXT, ... ); CREATE TABLE order ( id INT PRIMARY KEY, shipping_address address NOT NULL, billing_address address NOT NULL, ... ); INSERT INTO order VALUES ( 1, CAST(ROW('', ...)) AS address, ... );

Slide 40

Slide 40 text

Look at Your Queries

Slide 41

Slide 41 text

Potential Schema Issues • Frequent JOIN queries • Frequent projections to exclude certain columns • Frequent subqueries • Frequent writes to multiple tables at once

Slide 42

Slide 42 text

Update Cart Amount START TRANSACTION; INSERT INTO sylius_order_item_unit VALUES (...); UPDATE sylius_order_item SET quantity = 3, total = 1998 WHERE ...; UPDATE sylius_order SET items_total = 1998 WHERE ...; COMMIT;

Slide 43

Slide 43 text

Update Cart Amount A Little More Isolation START TRANSACTION; INSERT INTO sylius_order_item_unit VALUES (...); UPDATE sylius_order_item SET quantity = quantity + 1, total = total + 999 WHERE ...; UPDATE sylius_order SET items_total = items_total + 999 WHERE ...; COMMIT;

Slide 44

Slide 44 text

Look at Your Data

Slide 45

Slide 45 text

Cart Data { "id": 123, "customer": 45, "state": "cart", "items": [ { "variant": 1381, "price": 1499, "quantity": 2, "units": [ { "id": "1381-1", "shipped": false }, { "id": "1381-2", "shipped": false } ] } ], "items_total": 2998 }

Slide 46

Slide 46 text

Make it an Order { "id": 123, "customer": { "id": 45, "name": "Andreas Braun", "shippingAddress": { "address": "", "postCode": "", "city": "", "country": "Germany" }, "billingAddress": { "address": "", "postCode": "", "city": "", "country": "Germany" } }, "state": "new", "items": [ { "variant": 1381, "price": 1499, "quantity": 2, "units": [ { "id": "1381-1", "shipped": true }, { "id": "1381-2", "shipped": true } ] } ], "items_total": 2998, "payments": [ { "amount": 2998, "state": "completed", "payment_method": { "type": "credit_card", "details": { "cardHolder": "Andreas Braun", "creditCard": "xxxxxxxxxxxx1234", "expiration": "09/24", "gateway": "easy-payment-service", "identifier": "afae87a0" } }, "billingAddress": { "address": "", "postCode": "", "city": "", "country": "Germany" } } ], "shipments": [ { "units": ["1381-1", "1381-2"], "state": "registered", "shipment_method": { "type": "dhl", "trackingNumber": "12345678a" }, "shippingAddress": { "address": "", "postCode": "", "city": "", "country": "Germany" }, "history": [ { "date": "2019-11-21T14:23:04+01:00", "state": "registered" } ] } ] }

Slide 47

Slide 47 text

Make it an Order Customer Data { "customer": { "id": 45, "name": "Andreas Braun", "shippingAddress": { "address": "", "postCode": "", "city": "", "country": "Germany" }, "billingAddress": { "address": "", "postCode": "", "city": "", "country": "Germany" } } }

Slide 48

Slide 48 text

Make it an Order Payment Data { "payments": [ { "amount": 2998, "state": "completed", "payment_method": { "type": "credit_card", "details": { "cardHolder": "Andreas Braun", "creditCard": "xxxxxxxxxxxx1234", "expiration": "09/24", "gateway": "easy-payment-service", "identifier": "afae87a0" } }, "billingAddress": { ... } } ] }

Slide 49

Slide 49 text

Make it an Order Shipment Data { "shipments": [ { "units": ["1381-1", "1381-2"], "state": "registered", "shipment_method": { "type": "dhl", "trackingNumber": "12345678a" }, "shippingAddress": { ..., }, "history": [ { "date": "2019-11-21T14:23:04+01:00", "state": "registered" } ] } ] }

Slide 50

Slide 50 text

Use a Different Database

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

Shopping Cart Insert Initial Cart document db.order.insert({ _id: 123, customer: 45, state: "cart", items: [ { variant: 1381, price: 1499, total: 2998, quantity: 2, units: [ { id: "1381-1", shipped: true }, { id: "1381-2", shipped: true } ] } ], items_total: 2998 });

Slide 53

Slide 53 text

Update Cart Quantity Update Quantity db.order.updateOne( { _id: 123 }, { $inc: { 'items_total': 1499, 'items.$[matchingItem].quantity': 1, 'items.$[matchingItem].total': 1499 }, $push: { 'items.$[matchingItem].units': { id: “1381-3", shipped: false } } }, { arrayFilters: [ { 'matchingItem.variant': { $eq: 1381 } } ] } );

Slide 54

Slide 54 text

Finish Checkout db.order.updateOne( { _id: 123 }, { $set: { 'checkout_state': 'completed', 'state': 'awaiting_payment', 'customer': { 'name': 'Andreas Braun', ... } }, $push: { payments: { ... }, shipments: { ... } } } );

Slide 55

Slide 55 text

Get a Head Start With Doctrine

Slide 56

Slide 56 text

Order Object /** @final */ class Order { private ?string $id = null; private string $state = 'cart'; private ?Customer $customer = null; /** @var Collection */ private Collection $items; private int $total = 0; public function __construct() { $this->items = new ArrayCollection(); } }

Slide 57

Slide 57 text

Order Entity With Doctrine ORM /** * @ORM\Entity * @final */ class Order { /** @ORM\Id */ private ?string $id = null; /** @ORM\Column(type="string") */ private string $state = 'cart'; /** @ORM\ManyToOne(targetDocument=Customer::class) */ private ?Customer $customer = null; /** @ORM\OneToMany(targetDocument=OrderItem::class) */ private Collection $items; /** @ORM\Column(type="int") */ private int $total = 0; }

Slide 58

Slide 58 text

Order Document With Doctrine MongoDB ODM /** * @ODM\Document * @final */ class Order { /** @ODM\Id */ private ?string $id = null; /** @ODM\Field(type="string") */ private string $state = 'cart'; /** @ODM\EmbedOne(targetDocument=Customer::class) */ private ?Customer $customer = null; /** @ODM\EmbedMany(targetDocument=OrderItem::class) */ private Collection $items; /** @ODM\Field(type="int", strategy="increment") */ private int $total = 0; }

Slide 59

Slide 59 text

Embedded Documents With Doctrine MongoDB ODM /** @ODM\EmbeddedDocument */ final class OrderItem { /** @ODM\ReferenceOne(targetDocument=ProductVariant::class) */ private ProductVariant $variant; /** @ODM\Field(type="int") */ private int $price; /** @ODM\Field(type="int", strategy="increment") */ private int $quantity; /** @ODM\Field(type="int", strategy="increment") */ private int $total; /** @ODM\EmbedMany(targetDocument=OrderItemUnit::class) */ private Collection $units; }

Slide 60

Slide 60 text

Finish Checkout With Doctrine MongoDB ODM $order->setCheckoutState('completed'); $order->setState('awaiting_payment'); $order->setCustomer($customer); $order->addShipment($shipment); $order->addPayment($payment); $documentManager->persist($order); $documentManager->flush();

Slide 61

Slide 61 text

Finish Checkout Query from Doctrine MongoDB ODM db.order.updateOne( { _id: 123 }, { $set: { 'checkout_state': 'completed', 'state': 'awaiting_payment', 'customer': { 'name': 'Andreas Braun', ... } }, $push: { payments: { ... }, shipments: { ... } } } );

Slide 62

Slide 62 text

Update Cart Quantity With Doctrine MongoDB ODM $orderItem->updateQuantity(3); // OrderItemQuantityModifier creates/removes units $documentManager->persist($order); $documentManager->flush();

Slide 63

Slide 63 text

Update Cart Quantity Query from Doctrine MongoDB ODM db.order.updateOne( { _id: 123 }, { $inc: { 'items_total': 999, 'items.$[matchingItem].quantity': 1, 'items.$[matchingItem].total': 999 }, $push: { 'items.$[matchingItem].units': { id: “1381-3", shipped: false } } }, { arrayFilters: [ { 'matchingItem.variant': { $eq: 1381 } } ] } ); NOT YET

Slide 64

Slide 64 text

Think Outside The Box Table

Slide 65

Slide 65 text

Think Outside The Box Table

Slide 66

Slide 66 text

Thanks! @alcaeus github.com/alcaeus symfony-devs: @alcaeus #StayAtHome