{"id":215,"date":"2023-11-15T09:20:48","date_gmt":"2023-11-15T17:20:48","guid":{"rendered":"https:\/\/blog.kineteque.com\/?p=215"},"modified":"2023-11-15T09:20:48","modified_gmt":"2023-11-15T17:20:48","slug":"what-i-would-change-if-i-saw-your-database","status":"publish","type":"post","link":"https:\/\/blog.kineteque.com\/?p=215","title":{"rendered":"What I would change if I saw your database"},"content":{"rendered":"<p>This post briefly covers the history of databases, transaction fundamentals, and then proceeds to explain what I would change if I saw your database. It is highly-opinionated, and based on decades of experience in working with relational databases.<\/p>\n<h1>History<\/h1>\n<p>The term relational database was Invented by E. F. Codd at IBM in 1970 in his paper &#8220;A Relational Model of Data for Large Shared Data Banks.&#8221; The first commercially available RDBMS was Oracle, released in 1979 by Relational Software, now Oracle Corporation. Currently, the most popular relational databases are: Oracle, MySql, Microsoft SQL Server, PostgreSQL, DB2, SQLite, Sybase. They all use a variant of SQL. My personal favorite database is PostreSQL. It is free, open-source, and most faithfully implements ANSI-SQL.<\/p>\n<h1>Relational Databases Today<\/h1>\n<p>The relational database can provide the right persistence solution for most business needs. They are extraordinarily fast and reliable. They can manage petabyte scale data. There are plenty of tools written for them. They are simple to use. Right or wrong, they are also one of the most common enterprise integration patterns.<\/p>\n<p><!--more--><\/p>\n<h1>The Advent of NoSQL<\/h1>\n<p>What about NoSQL?<\/p>\n<p>NoSQL solutions provide a viable alternative persistence solution, depending on your needs.<\/p>\n<p>Many NoSQL solutions are distributed by design, thus horizontally scalable. They often compromise consistency (CAP theorem) in favor of availability, partition tolerance, and speed.<\/p>\n<p>Examples of the different types of NoSQL solutions are:<\/p>\n<ul>\n<li>Key Value Store (Riak, Memcached)<\/li>\n<li>Data-Structure Store (Redis)<\/li>\n<li>Big Table or Column Store (Cassandra, HBase)<\/li>\n<li>Document Store (Couchbase, Lucene, MongoDB)<\/li>\n<\/ul>\n<p>Ironically, most NoSQL solutions have an SQL like interface.<\/p>\n<h1>When to use RDBMS<\/h1>\n<p>The RDBMS is the right choice for a very wide variety of use cases, such as when:<\/p>\n<ul>\n<li>Data is inherently relational.<\/li>\n<li>Data is tabular.<\/li>\n<li>Data is user-generated (as opposed to machine generated).<\/li>\n<li>You need a standard interface such as SQL used by business people or multiple applications.<\/li>\n<li>Your application fits into the 99.9% of the applications that can perform and scale perfectly with a RDBMS*.<\/li>\n<li>Your application truly needs ACID.<\/li>\n<\/ul>\n<h1>Fundamentals<\/h1>\n<h2>Transactions<\/h2>\n<p>A transaction is a group of SQL statements that are a logical unit of work attributed to one connection.\u00a0Most RDBS support transactions.<\/p>\n<p>It has four fundamental properties:<\/p>\n<p>Atomic Consistent Isolation Durable. ACID.<\/p>\n<p>&#8220;Atomic&#8221; describes how after the transaction finishes the changes that each statement made seem as though were completed as one statement.<\/p>\n<p>&#8220;Consistent&#8221; describes how the RDBMS will have valid data after the transaction.<\/p>\n<p>&#8220;Isolation&#8221; describes the rules of how one transaction can change the values in another transaction happening at the same time.<\/p>\n<p>&#8220;Durable&#8221; describes that persistent nature of an RDBMS. Once the transaction has been committed, the update will be persisted, even if there is a system failure.<\/p>\n<h2>Transaction Isolation Levels<\/h2>\n\n<table id=\"tablepress-2\" class=\"tablepress tablepress-id-2\">\n<thead>\n<tr class=\"row-1\">\n\t<th class=\"column-1\">Isolation Level<\/th><th class=\"column-2\">Phantom Reads<\/th><th class=\"column-3\">Nonrepeatable Reads<\/th><th class=\"column-4\">Dirty Reads<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"row-striping row-hover\">\n<tr class=\"row-2\">\n\t<td class=\"column-1\">READ_UNCOMMITTED<\/td><td class=\"column-2\">yes<\/td><td class=\"column-3\">yes<\/td><td class=\"column-4\">yes<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">READ_COMMITTED<\/td><td class=\"column-2\">yes<\/td><td class=\"column-3\">yes<\/td><td class=\"column-4\">no<\/td>\n<\/tr>\n<tr class=\"row-4\">\n\t<td class=\"column-1\">REPEATABLE_READS<\/td><td class=\"column-2\">yes<\/td><td class=\"column-3\">no<\/td><td class=\"column-4\">no<\/td>\n<\/tr>\n<tr class=\"row-5\">\n\t<td class=\"column-1\">SERIALIZABLE<\/td><td class=\"column-2\">no<\/td><td class=\"column-3\">no<\/td><td class=\"column-4\">no<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<!-- #tablepress-2 from cache -->\n\n<table id=\"tablepress-3\" class=\"tablepress tablepress-id-3\">\n<tbody class=\"row-striping row-hover\">\n<tr class=\"row-1\">\n\t<td class=\"column-1\">Phantom Read<\/td><td class=\"column-2\">T1 does query. T2 inserts rows. T1 does same query and finds new row.<br \/>\n<\/td>\n<\/tr>\n<tr class=\"row-2\">\n\t<td class=\"column-1\">Nonrepeatable Read<br \/>\n<\/td><td class=\"column-2\">T1 reads a row. T2 updates the same row. T1 reads same row again.<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">Dirty Read<\/td><td class=\"column-2\">T1 updates a row, but without commit. T2 reads updated row. T1 does rollback.<br \/>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<!-- #tablepress-3 from cache -->\n<p>I encourage you to experiment with isolation levels to understand the difference. Experiment by creating two connections to a database. T1 is connection one. T2 is connection two. Please note that not all databases support all isolation levels.<\/p>\n<h1>What I would Change<\/h1>\n<h2>Use a Migration Tool<\/h2>\n<p>Introduce migrations to manage your database development and management process. My favorite choice is Liquibase, however, there are also Active Record Migrations, and Liquibase.<\/p>\n<p>When possible, prepare are rollback plan for each migration. Also, remember that is simpler to add a column to a production system than to remove one.<\/p>\n<p>Introduce only manageable incremental migrations per sprint.<\/p>\n<p>Below is an example of how to run liquibase through docker-compose.<\/p>\n<pre class=\"lang:yaml decode:true\">  liquibase.db:\n    image: liquibase\/liquibase:4.18\n    command: --defaultsFile=\/liquibase\/config\/liquibase.properties update\n    depends_on:\n      db:\n        condition: service_healthy\n    volumes:\n      - .\/conf\/liquibase\/liquibase.properties:\/liquibase\/config\/liquibase.properties\n      - .\/microservice\/src\/main\/resources\/db\/changelog:\/liquibase\/changelog<\/pre>\n<h2>Continuously Document Your Schema<\/h2>\n<p>Introduce a utility that publishes your schema entity relationship diagram after every database migration.<\/p>\n<p>You&#8217;ll be surprised how many dialogues this creates up during design, problem solving sessions, agile grooming, planning poker, and many other scenarios.<\/p>\n<p>Below is an example of how to run schemaspy through docker-compose.<\/p>\n<pre class=\"lang:yaml decode:true\">  schemaspy.db:\n    image: schemaspy\/schemaspy:6.2.2\n    volumes:\n      - schemaspy.db:\/output:rw\n      - .\/conf\/schemaspy\/schemaspy.properties:\/schemaspy.properties\n    command: SCHEMASPY_OUTPUT=\/schemaspy\/db \/usr\/local\/bin\/schemaspy -schemas public\n    depends_on:\n      liquibase.db:\n        condition: service_completed_successfully\n\nvolumes:\n  schemaspy.db: {}<\/pre>\n<h2>Introduce Coding Conventions<\/h2>\n<ol>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">TABLE names are plural.<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">COLUMN names are singular<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Each TABLE has:\u00a0 id INTEGER PRIMARY KEY,.<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Index each PRIMARY KEY and FOREIGN KEY columns.<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">FOREIGN KEY columns are named with the pattern singular_table_name_id.<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Use COMMENT data-definition language.<\/span><\/li>\n<li style=\"font-weight: 400;\" aria-level=\"1\"><span style=\"font-weight: 400;\">Favor NOT NULL since null columns are normalization columns candidates.<\/span><\/li>\n<\/ol>\n<p><span style=\"font-weight: 400;\">Identifiers should use the following suffices.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">_PK Primary Key<br \/>\n<\/span><span style=\"font-weight: 400;\">_CK Check Constraint<br \/>\n<\/span><span style=\"font-weight: 400;\">_UK Unique Key Constraint<br \/>\n<\/span><span style=\"font-weight: 400;\">_V View<br \/>\n<\/span><span style=\"font-weight: 400;\">_MV Materialized View<br \/>\n<\/span><span style=\"font-weight: 400;\">_FK Foreign Key<br \/>\n<\/span><span style=\"font-weight: 400;\">_X Index<br \/>\n<\/span><span style=\"font-weight: 400;\">_UX Unique Index<br \/>\n<\/span><span style=\"font-weight: 400;\">_FX Function Based Index<br \/>\n<\/span><span style=\"font-weight: 400;\">_SEQ Sequences<br \/>\n<\/span><span style=\"font-weight: 400;\">_TRG Triggers<br \/>\n<\/span><span style=\"font-weight: 400;\">_PKG Packages and Package Bodies<\/span><\/p>\n\n<table id=\"tablepress-4\" class=\"tablepress tablepress-id-4\">\n<thead>\n<tr class=\"row-1\">\n\t<th class=\"column-1\">Constraint Type<\/th><th class=\"column-2\">Naming Convention<\/th>\n<\/tr>\n<\/thead>\n<tbody class=\"row-hover\">\n<tr class=\"row-2\">\n\t<td class=\"column-1\">Foreign Key<br \/>\n<\/td><td class=\"column-2\">REFERENCING_TABLE#REFERENCED_TABLE#REFERENCED_COLUMN_NAME_FK<\/td>\n<\/tr>\n<tr class=\"row-3\">\n\t<td class=\"column-1\">Unique<\/td><td class=\"column-2\">TABLE_NAME#COLUMN_NAME_UX<br \/>\n<\/td>\n<\/tr>\n<tr class=\"row-4\">\n\t<td class=\"column-1\">Check<\/td><td class=\"column-2\">TABLE_NAME#COLUMN_NAME_CK<br \/>\n<\/td>\n<\/tr>\n<tr class=\"row-5\">\n\t<td class=\"column-1\">Not Null<\/td><td class=\"column-2\">TABLE_NAME#COLUMN_NAME_NN<br \/>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<!-- #tablepress-4 from cache -->\n<p>Not everybody will agree on these conventions, and you might have different conventions in mind. However, it is better to stick to one convention than to have no conventions because this will lead to higher maintainability.<\/p>\n<h2>Always Start with 3rd Normal Form<\/h2>\n<p>When starting your database design, start with and favor 3rd Normal Form (3NF). Deviate from that only for optimization purposes, after you&#8217;ve already tried 3NF. Do NOT prematurely optimize away from 3NF.<\/p>\n<h2>Use Foreign Key Constraints<\/h2>\n<p><span style=\"font-weight: 400;\">Favor Foreign Key Constraints for applications still in development, or for &#8220;small&#8221; applications.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Although Foreign Key Constraints protect the integrity of the database, they are <\/span><span style=\"font-weight: 400;\">not necessary<\/span><span style=\"font-weight: 400;\"> in a bug-free application that will never violate Foreign Key Constraints. They affect performance.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Foreign Key constraints make it very difficult to manage or refactor large &#8220;enterprise&#8221; databases with thousands of tables.<\/span><\/p>\n<h2>Only Use Numeric Id Primary Keys<\/h2>\n<p>Favor auto-generated integer PRIMARY KEY columns that are populated by a SEQUENCE.<\/p>\n<p>Use the name &#8220;id&#8221; for the PRIMARY KEY column. It is the most popular convention, and it will allow your database to more easily integrate with other frameworks such as Ruby on Rails.<\/p>\n<p>Do NOT use a COMPOSITE PRIMARY KEY, instead use a UNIQUE INDEX.<\/p>\n<h2>Use Compact Data-Types<\/h2>\n<p>Use a proper data-type. <em>Do not make everything type &#8220;text&#8221;!<\/em><\/p>\n<p>Use the smallest byte sized data-type appropriate for the given column. This will help performance in the long run on the server and on the client.<\/p>\n<p>3rd Normal Form will allow for a more compact data types since it removes redundant data (i.e. an association will show an integer id, which is more compact, instead of text).<\/p>\n<h2>Design for Concurrency<\/h2>\n<p>Design a schema that is concurrently safe with multiple clients or processes.<\/p>\n<ul>\n<li>Avoid row contention.<\/li>\n<li>Favor row inserts to sum amounts.<\/li>\n<li>Use optimistic locking with row version numbers.<\/li>\n<\/ul>\n<h2>Design for Growth<\/h2>\n<p>Depending on your needs, you may need to also architect for growth. It&#8217;s easy to scale up a database (better processors, more ram). However, that also has its limits, and you may need to use a distributed design that may incorporate the following.<\/p>\n<ul>\n<li>Caching<\/li>\n<li>Partitioning<\/li>\n<li>Sharding<\/li>\n<li>Replication<\/li>\n<li>An intelligent driver or client<\/li>\n<\/ul>\n<p>MySql has very nice sharding and partitioning capabilities. The MySQL NDB cluster can automatically partitions tables across nodes, and queries will access the correct shards. Sharding is transparent to the application. \u00a0Unlike other databases, users can perform JOIN operations, use ACID-guarantees, when perform queries and transactions across shards (source <a href=\"https:\/\/www.mysql.com\/products\/cluster\/scalability.html\">MySQL NDB Cluster: Scalability<\/a>).<\/p>\n<h2>Introduce Auditing<\/h2>\n<p>Introduce auditing into your design.<\/p>\n<p>Each DML statement can be audited with a version number, user id, update time, and create time that will show current values on the target table, and the history in a history table.<\/p>\n<p>This will also help with optimistic locking, future integrations with other system, and with trouble shooting.<\/p>\n<p>Below is an example audit trigger that I use with PostgreSQL.<\/p>\n<pre class=\"lang:plsql decode:true\">-- Create trigger that does two things: 1) it updates the version and updated_at fields in the case of an\n-- UPDATE. 2) on every UPDATE, and DELETE operation it adds a record to the _H audit table.\nCREATE OR REPLACE FUNCTION audit_update_trigger() RETURNS TRIGGER LANGUAGE plpgsql AS\n'BEGIN\n    IF (TG_OP = ''UPDATE'') AND (NEW.last_updated_by &gt; 0) THEN\n        NEW.version := OLD.version + 1;\n        NEW.updated_at := CURRENT_TIMESTAMP;\n        EXECUTE format(''INSERT INTO audit.%I_h SELECT nextval(''''audit.%I_h_id_seq''''), %L, now(), ($1).*'', TG_TABLE_NAME, TG_TABLE_NAME, TG_OP) USING OLD;\n        RETURN NEW;\n    ELSEIF (TG_OP = ''DELETE'') THEN\n        EXECUTE format(''INSERT INTO audit.%I_h SELECT nextval(''''audit.%I_h_id_seq''''), %L, now(), ($1).*'', TG_TABLE_NAME, TG_TABLE_NAME, TG_OP) USING OLD;\n        RETURN OLD;\n    END IF;\n\n    RETURN NEW;\nEND;';\n\nCOMMENT ON FUNCTION audit_update_trigger() IS 'Trigger that does two things: 1) it updates the version and updated_at fields of a record in the case of an UPDATE 2) on every UPDATE, and DELETE operation it adds a record to the _H audit table.'<\/pre>\n<h2>Use Prepared Statements for ALL Production Code<\/h2>\n<p>They are optimized on the server, and thus perform faster.<\/p>\n<p>They are cached on the server, but you can also cache them on the client.<\/p>\n<p>They are secure, and prevent SQL injection attacks.<\/p>\n<h2>Use Batch Updates<\/h2>\n<p>JDBC Batch Update allowed me to support 7 billion impressions per day on MySpaces&#8217;s Ad Server. That&#8217;s a throughput of 81,000 impressions per second!<\/p>\n<p>Batch Update allowed me to perform 15 million inserts across two indexed tables in 7 minutes. Without batch update, the same code would take 15 hours!<\/p>\n<p>Additionally, streaming frameworks such as Apache Flink&#8217;s JDBC Connector also uses JDBC Batch to perform its upserts.<\/p>\n<p>The Java code below shows how to perform JDBC batch updates.<\/p>\n<pre class=\"lang:java decode:true \">con.setAutoCommit(false);\ntry ( \n  PreparedStatement seq = con.prepareStatement(\"SELECT nextval('products_seq') FROM generate_series(1, 1000)\"); PreparedStatement insert = con.prepareStatement(\"INSERT INTO products(id, name) VALUES(?, ?)\");\n) {\n  int b = 0;  int c = 0;\n  for (int i = 0; i &lt; 1000; i++) {\n     final ResultSet products_seq = seq.executeQuery();\n     while (products_seq.next()) {\n        long product_id = products_seq.getLong(1);        insert.setLong(1, product_id);\n        insert.setString(2, \"T-Shirt #\" + product_id);        insert.addBatch(); b++;\n        if (b % 100 == 0) { insert.executeBatch(); c++; }\n        if (c % 10 == 0) { con.commit(); }\n     }\n  }\n  if (b % 100 != 0) { insert.executeBatch(); }\n  if (c % 10 != 0) { con.commit(); }\n}\n<\/pre>\n<h2>Eliminate all N+1 Queries from Production!<\/h2>\n<p>Your associate programmers are just happy to make the application functional.<\/p>\n<p>Your senior programmers should replace N + 1 with join queries! (or another variant if join won&#8217;t work).<\/p>\n<p>The code below shows an example of the N + 1 query problem misuse.<\/p>\n<pre class=\"lang:java decode:true\">try (\n  Connection con = buildConnection(); \n  PreparedStatement p = con.prepareStatement(\"SELECT id, name FROM products\");\n) {   \n  ResultSet prs = p.executeQuery();\n  while(prs.next()) { \n    PreparedStatement o = con.prepareStatement(\"SELECT id, name FROM options WHERE product_id = ?\");\n    o.setLong(1, prs.getLong(1));  \n    ResultSet ors = o.executeQuery();\n    while(ors.next()) { \n      long option_id = ors.getLong(1); \n        \/\/remaining business logic \u2026\n    }\n  }\n}\n<\/pre>\n<p>Instead, use an SQL join to bring in the data in one query instead of N + 1 queries.<\/p>\n<pre class=\"lang:pgsql decode:true \">SELECT \n   p.id AS product_id, \n   p.name AS product_name,\n   o.id AS option_id,\n   o.name AS option_name\nFROM\n   products p\nLEFT JOIN\n   options on o.id = p.product_id<\/pre>\n<h2>Introduce Production Query Audits<\/h2>\n<p>Your development process should collect all queries used in production code. This allows experts to optimize the query with an <em><strong>EXPLAIN<\/strong><\/em>\u00a0or\u00a0<em><strong>EXPLAIN ANALYZE<\/strong><\/em>\u00a0plan to introduce indices on the right columns or refactor the query.<\/p>\n<h2>Introduce Performance Monitoring<\/h2>\n<p>Focus on variance.<\/p>\n<p>If you vertically scaled, then focus on Top-N worst performing queries.<\/p>\n<p>If you horizontally scaled, then focus on the Top-P worst performing hosts.<\/p>\n<h2>Additional Tips<\/h2>\n<ol>\n<li>Consider query paging when querying large tables.<\/li>\n<li>Distributed caches can sometimes increase performance.<\/li>\n<li>Use connection pooling appropriately.<\/li>\n<li>Know your database frameworks.\u00a0<span style=\"font-weight: 400;\">You probably only need to know three persistence frameworks to survive as a Java Developer, you must learn then and master them:<\/span>\n<ol>\n<li>JDBC<\/li>\n<li>JPA<\/li>\n<li>Spring JDBC<\/li>\n<\/ol>\n<\/li>\n<\/ol>\n<h3>Spring JDBC In a Nutshell<\/h3>\n<p>Although JDBC is very versatile, it is quite low-level when compared to the simplicity that Spring JDBC provides on top it.<\/p>\n<p>Spring JDBC will:<\/p>\n<ol>\n<li>Provide full-control over the production queries.<\/li>\n<li>Provide a very simple interface for result set extraction.<\/li>\n<li>Provide transactional control through annotations and Aspect Oriented Programming.<\/li>\n<li>Provide named query parameters!<\/li>\n<\/ol>\n<h3>JPA in a Nutshell<\/h3>\n<p>JPA is the standard ORM for Java. It does all things expected of ORM, and more.<\/p>\n<ol>\n<li>Named Queries are optimized and utilize prepared statements.<\/li>\n<li>Developers have less control over the generated queries, so more care must be taken to avoid the N+1 query problem.<\/li>\n<li>JPA provides the cliche table mapping features expected of an ORM framework, but it can also map ANY query to ANY class with the @ConstructorResult annotation.<\/li>\n<\/ol>\n<h1>References<\/h1>\n<h2>Source Code<\/h2>\n<p><a href=\"https:\/\/github.com\/minmay\/improving-your-relational-database-architecture\">https:\/\/github.com\/minmay\/improving-your-relational-database-architecture<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post briefly covers the history of databases, transaction fundamentals, and then proceeds to explain what I would change if I saw your database. It is highly-opinionated, and based on decades of experience in working with relational databases. History The term relational database was Invented by E. F. Codd at IBM in 1970 in his [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,5],"tags":[],"class_list":["post-215","post","type-post","status-publish","format-standard","hentry","category-architecture","category-database-design"],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=\/wp\/v2\/posts\/215","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=215"}],"version-history":[{"count":0,"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=\/wp\/v2\/posts\/215\/revisions"}],"wp:attachment":[{"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=215"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=215"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.kineteque.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=215"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}