Slide 1

Slide 1 text

Dropping ACID Schema Design for e-commerce

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

ERD Product

Slide 5

Slide 5 text

ERD Order

Slide 6

Slide 6 text

Product Entity /** @final */ class Product { private ?string $id = null; private string $title; private ?string $description = null; /** @var Collection */ private Collection $variants; /** @var Collection */ private Collection $options; public function __construct(string $title) { $this->title = $title; $this->variants = new ArrayCollection(); $this->options = new ArrayCollection(); } }

Slide 7

Slide 7 text

Order Entity /** @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 8

Slide 8 text

Design the Database Schema

Slide 9

Slide 9 text

Database Normalisation • Unnormalised: “Just do whatever” • 1NF: “Domain of each attribute contains only atomic values” • … • 6NF: “Row contains the Primary Key, and at most one other attribute”

Slide 10

Slide 10 text

ERD -> Tables 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 11

Slide 11 text

Create a Product $product = new Product(); $product->setTitle('Symfony T-Shirt'); $product->addProductOption($color); $variant = new ProductVariant($product); $variant->setProductOptionValue($color, $black); $manager->persist($product); $manager->persist($variant); $manager->flush();

Slide 12

Slide 12 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 (...);

Slide 13

Slide 13 text

ACID

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

ACID atomicity, consistency, isolation, durability

Slide 16

Slide 16 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 (...);

Slide 17

Slide 17 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 18

Slide 18 text

Finalise Payment START TRANSACTION; UPDATE sylius_payment SET state = 'completed' ...; UPDATE sylius_order SET payment_state = 'completed' ...; COMMIT;

Slide 19

Slide 19 text

You Don’t NEED Transactions

Slide 20

Slide 20 text

You Need a Different Database

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

You Need a Different Schema

Slide 23

Slide 23 text

Denormalise Your Schema

Slide 24

Slide 24 text

Cart { "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 25

Slide 25 text

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 26

Slide 26 text

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

Slide 27

Slide 27 text

Order - Payments { "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" } } ] }

Slide 28

Slide 28 text

Order - Shipments { "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 29

Slide 29 text

Relationship advice

Slide 30

Slide 30 text

References • A pointer to another entity • Referential integrity optional (MyISAM, MongoDB) • State of the art in an RDBMS

Slide 31

Slide 31 text

Embedded data • Not just a pointer to another entity, but all data • Update is done on the main entity • Not very useful if data is needed by itself

Slide 32

Slide 32 text

Choose Your Relationship • Reference data when it’s needed by itself • Embed data when it’s owned by another entity • Look at your queries

Slide 33

Slide 33 text

Create complex schemas

Slide 34

Slide 34 text

CLOB UPDATE sylius_order SET payments = 'a:1:{i:0;a:4:{s:6:"amount";i:2998;s:5:"state";...;}}}';

Slide 35

Slide 35 text

JSON/JSONb UPDATE sylius_order SET 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" } } ]';

Slide 36

Slide 36 text

Partial JSON Updates UPDATE sylius_order SET payments = JSON_SET( payments, '$[last].state', 'failed', ‘$[last].payment_method.details.error', '...' ) ;

Slide 37

Slide 37 text

Partial JSON Updates UPDATE sylius_order SET payments = JSON_ARRAY_APPEND( JSON_SET( payments, '$[last].state', 'failed', ‘$[last].payment_method.details.error', '...' ), CAST('{ "amount": 2998, "state": "pending", ... }' AS JSON) ) ;

Slide 38

Slide 38 text

Complex Types CREATE TYPE customer AS ( id INT, name VARCHAR(255), shippingAddress address, billingAddress address );

Slide 39

Slide 39 text

Update Complex Fields UPDATE sylius_order SET (customer).name = 'Andreas Braun';

Slide 40

Slide 40 text

Array Types CREATE TABLE sylius_order ( ..., payments payment[] );

Slide 41

Slide 41 text

Update Arrays UPDATE sylius_order SET (payments[0]).state = 'failed', payments = payments || ARRAY[CAST(ROW(123, ...) AS payment)] ;

Slide 42

Slide 42 text

Use MongoDB (yes, it’s ACID compliant)

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Atomic Updates db.order.updateOne( { _id: 123 }, { $set: { 'payments.$[failedPayment].state': 'failed', 'payments.$[failedPayment].details': { ... }, }, $push: { 'payments': { ... } } }, { arrayFilters: [ { 'failedPayment.id': { $eq: 1234 } } ] } );

Slide 46

Slide 46 text

Use Doctrine MongoDB ODM It’s like ORM, but for MongoDB

Slide 47

Slide 47 text

Use Doctrine MongoDB ODM It’s like ORM, but for MongoDB (and cooler)

Slide 48

Slide 48 text

Product in ODM /** * @ODM\Document * @final */ class Product { /** @ODM\Id */ private ?string $id = null; /** @ODM\Field(type="string") */ private string $title; /** @ODM\Field(type="string") */ private ?string $description = null; /** @ODM\ReferenceMany(targetDocument=ProductVariant::class) */ private Collection $variants; /** @ODM\ReferenceMany(targetDocument=ProductOption::class) */ private Collection $options; }

Slide 49

Slide 49 text

Order in 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 50

Slide 50 text

Embedded Documents /** @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\EmbedMany(targetDocument=OrderItemUnit::class) */ private Collection $units; }

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

More Relationship Advice

Slide 54

Slide 54 text

Hybrid Relationships • Keeps a reference to original entity • Stores some (or all) data in the relationship as well • Can be used in PostgreSQL or MongoDB

Slide 55

Slide 55 text

Using Complex Types CREATE TYPE customer AS ( id INT, name VARCHAR(255), shippingAddress address, billingAddress address );

Slide 56

Slide 56 text

Using DBRef in MongoDB { "id": 123, "customer": { "$ref": "customer", "$id": 45 } }

Slide 57

Slide 57 text

Using DBRef in MongoDB , "name": "Andreas Braun", "shippingAddress": { "address": "", "postCode": "", "city": "", "country": "Germany" }, "billingAddress": { "address": "", "postCode": "", "city": "", "country": "Germany" } { "id": 123, "customer": { "$ref": "customer", "$id": 45 } }

Slide 58

Slide 58 text

Think outside the box table

Slide 59

Slide 59 text

Think outside the box table

Slide 60

Slide 60 text

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