{"id":593,"date":"2012-01-14T17:04:56","date_gmt":"2012-01-14T16:04:56","guid":{"rendered":"http:\/\/doanduyhai.wordpress.com\/?p=593"},"modified":"2018-09-14T14:01:31","modified_gmt":"2018-09-14T14:01:31","slug":"jpahibernate-temporary-conversations-with-spring-aop","status":"publish","type":"post","link":"https:\/\/www.doanduyhai.com\/blog\/?p=593","title":{"rendered":"JPA\/Hibernate Temporary Conversations with Spring AOP"},"content":{"rendered":"<p>Today we discuss about temporary conversations in JPA, their usage, all usage warnings and a sample implementation using Spring AOP<\/p>\n<p>This article follows upon a previous one about global long conversation : <a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=278\" title=\"http:\/\/doanduyhai.wordpress.com\/2011\/11\/26\/hibernate-longextended-session\/\" target=\"_blank\">JPA\/Hibernate Long session<\/a><\/p>\n<p><!--more--><\/p>\n<h1>I Abstract<\/h1>\n<h3>A Definition<\/h3>\n<p>A temporary conversation is an unit of work from the end-user point of view whose lifetime is too short to deserve to be managed in the global conversation and whose concern is clearly distinct from the main business logic of the global conversation.<\/p>\n<p><strong>Wizard screens<\/strong> and <strong>windows popups<\/strong> are very good candidates to the temporary conversation pattern.<\/p>\n<p><a href=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/01\/temp_conversation.png\"><img loading=\"lazy\" src=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/01\/temp_conversation.png\" alt=\"\" title=\"Temp_Conversation\" width=\"630\" height=\"267\" class=\"aligncenter size-full wp-image-596\" srcset=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/01\/temp_conversation.png 732w, https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/01\/temp_conversation-300x127.png 300w\" sizes=\"(max-width: 630px) 100vw, 630px\" \/><\/a><br \/>\n&nbsp;<\/p>\n<h3>B Do&#8217;s &#038; don&#8217;t<\/h3>\n<p><strong>Do<\/strong>:<\/p>\n<ul>\n<li>Clearly demarcate temporary conversation boundaries from the application flow. One single entry &#038; exit point<\/li>\n<li>One single extended EntityManager per conversation to manage all loaded entities<\/li>\n<li>Commit\/RollBack operations only at the end of the conversation<\/li>\n<\/ul>\n<p><strong>Don&#8217;t<\/strong>:<\/p>\n<ul>\n<li>Mix many temporary conversations in one flow<\/li>\n<li>Have many JDBC transactions in one temporary conversation<\/li>\n<li>Abuse of temporary conversation<\/li>\n<\/ul>\n<p>&nbsp;<\/p>\n<h1>II Design<\/h1>\n<h2>A Class Diagram<\/h2>\n<p><a href=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/02\/conversationmanager_temporaryconversationannotationprocessor.png\"><img loading=\"lazy\" src=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/02\/conversationmanager_temporaryconversationannotationprocessor.png\" alt=\"\" title=\"ConversationManager_TemporaryConversationAnnotationProcessor\" width=\"630\" height=\"436\" class=\"aligncenter size-full wp-image-705\" srcset=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/02\/conversationmanager_temporaryconversationannotationprocessor.png 742w, https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/02\/conversationmanager_temporaryconversationannotationprocessor-300x208.png 300w\" sizes=\"(max-width: 630px) 100vw, 630px\" \/><\/a><\/p>\n<p> We re-use the same design as the one for global conversation. Though there are some differences:<\/p>\n<ul>\n<li>the <strong>ConversationFilter<\/strong> is not used<\/li>\n<li>we define a <strong>TemporaryConversationManager<\/strong>, extending the <strong>DefaultConversationManager<\/strong> and re-defining the <em>beginConversation()<\/em> &#038; <em>endConversation()<\/em> methods<\/li>\n<li>we define a <strong>TemporaryConversationAnnotationProcessor<\/strong> with additional methods : <em>beginTemporaryConversation()<\/em>, <em>endTemporaryConversation()<\/em> &#038; <em>interceptDaoExecution()<\/em><\/li>\n<\/ul>\n<p>Please note that we also re-use the <strong>HttpSessionRepository<\/strong> as implementation for <strong>ConversationRepository<\/strong> although it is not shown in the above diagram.<\/p>\n<p>&nbsp;<\/p>\n<h1>II Algorithm<\/h1>\n<ul>\n<li><u>Before the execution of any method annotated by<br \/>\n<strong>@BeginTemporaryConversation(conversationName=ConversationEnum.C1)<\/strong><\/u><\/p>\n<ul>\n<li><strong>ConversationAnnotationProcessor<\/strong>.<em>findEntityManagerFactoryFromContext(<strong>ConversationEnum.C1.emf()<\/strong>)<\/em><\/li>\n<li>Define <span style=\"color:#ff0000;font-weight:bold;\">token<\/span> = <strong>ConversationEnum.C1.<em>emf()<\/em><\/strong> + &#8220;-&#8221; + <strong>ConversationEnum.C1.<em>name()<\/em><\/strong><\/li>\n<li><strong>ConversationRepository<\/strong>.<em>registerEntityManager(<span style=\"color:#ff0000;font-weight:bold;\">token<\/span>,emf)<\/em><\/li>\n<\/ul>\n<\/li>\n<p>&nbsp;<\/p>\n<li><u>For each call to any method whose first argument is of type <strong>ConversationEnum<\/strong><\/u>\n<ul>\n<li>If the <strong>ConversationEnum<\/strong> parameter is null\n<ul>\n<li>Do nothing<\/li>\n<\/ul>\n<\/li>\n<li>Else\n<ul>\n<li>Save the Entity Manager attached to the Spring Thread Local having with search key = \t<strong>ConversationEnum.C1.<em>emf()<\/em><\/strong> (<strong>TransactionSynchronizationManager<\/strong>)<\/li>\n<\/ul>\n<\/li>\n<li><strong>ConversationAnnotationProcessor<\/strong>.<em>findEntityManagerFactoryFromContext(<strong>ConversationEnum.C1.emf()<\/strong>)<\/em><\/li>\n<li>Define <span style=\"color:#ff0000;font-weight:bold;\">token<\/span> = <strong>ConversationEnum.C1.<em>emf()<\/em><\/strong> + &#8220;-&#8221; + <strong>ConversationEnum.C1.<em>name()<\/em><\/strong><\/li>\n<li><strong>ConversationManager<\/strong>.<em>reattachEntityManager(<span style=\"color:#ff0000;font-weight:bold;\">token<\/span>,emf)<\/em><\/li>\n<li>Execute the method<\/li>\n<li><strong>ConversationManager<\/strong>.<em>detachEntityManager(<span style=\"color:#ff0000;font-weight:bold;\">token<\/span>)<\/em><\/li>\n<li>Rebind the save Entity Manager to the Spring Thread Local<\/li>\n<\/ul>\n<\/li>\n<p>&nbsp;<\/p>\n<li><u>After the execution of any method annotated by<br \/>\n<strong>@EndTemporaryConversation(conversationName=ConversationEnum.C1)<\/strong><\/u><\/p>\n<ul>\n<li>Define <span style=\"color:#ff0000;font-weight:bold;\">token<\/span> = <strong>ConversationEnum.C1.<em>emf()<\/em><\/strong> + &#8220;-&#8221; + <strong>ConversationEnum.C1.<em>name()<\/em><\/strong><\/li>\n<li><strong>ConversationRepository<\/strong>.<em>unregisterEntityManager(<span style=\"color:#ff0000;font-weight:bold;\">token<\/span>)<\/em><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p> &nbsp;<\/p>\n<h1>III Implementation<\/h1>\n<h3>A <em>ConversationEnum<\/em><\/h3>\n<p>We create a conversation enum to list all possible temporary conversations in the application. The advantages of this approach are:<\/p>\n<ol>\n<li>Have all the definition of temporary conversations at one place, refactoring will be easier<\/li>\n<li>A Java enum makes it easier to detect any typo in conversation naming<\/li>\n<li>Add extra properties to temporary conversation using enum structure<\/li>\n<\/ol>\n<pre class=\"brush: java; highlight: [8]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\npublic enum ConversationEnum\r\n{\r\n\tC1(&quot;myEntityManagerFactory&quot;),\r\n\tC2(&quot;myEntityManagerFactory&quot;),\r\n\tC3(&quot;myEntityManagerFactory&quot;),\r\n\tC4(&quot;myAnotherEntityManagerFactory&quot;);\r\n\r\n\tpublic final String emf;\r\n\r\n\tConversationEnum(String emf) {\r\n\t\tthis.emf = emf;\r\n\t}\r\n}\r\n<\/pre>\n<p>We define a public final property <strong>emf<\/strong> to indicates which <strong>EntityManagerFactory<\/strong> each temporary conversation is using. This information is mandatory for the annotation processor to look up and inject the right EntityManager for the conversation.<\/p>\n<h3>B @BeginTemporaryConversation<\/h3>\n<pre class=\"brush: java; highlight: [13,21]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n@Retention(RetentionPolicy.RUNTIME)\r\n@Target(ElementType.METHOD)\r\n@Documented\r\npublic @interface BeginTemporaryConversation\r\n{\r\n\t\/**\r\n\t * &lt;p&gt;\r\n\t * Name of the current conversation requiring a temporary Entity Manager. \r\n\t * This name is mandatory since it is used as search key to register the temporary \r\n\t * Entity Manager to a ThreadLocal\r\n\t * &lt;\/p&gt;\r\n\t *\/\r\n\tConversationEnum conversationName();\r\n\r\n}\r\n<\/pre>\n<p>The <strong>conversationName<\/strong> is mandatory because we need a reference to the ConversationEnum object to get the associated Entity Manager Factory.<\/p>\n<h3>C @EndTemporaryConversation<\/h3>\n<pre class=\"brush: java; highlight: [12,20]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n@Retention(RetentionPolicy.RUNTIME)\r\n@Target(ElementType.METHOD)\r\n@Documented\r\npublic @interface EndTemporaryConversation\r\n{\r\n\t\/**\r\n\t * &lt;p&gt;\r\n\t * Name of the current conversation requiring a temporary Entity Manager. \r\n\t * This name is mandatory since it is used as search key to register the temporary \r\n\t * Entity Manager to a ThreadLocal\r\n\t * &lt;\/p&gt;\r\n\t *\/\r\n\tConversationEnum conversationName();\r\n}\r\n<\/pre>\n<h3>D TemporaryConversationManager<\/h3>\n<pre class=\"brush: java; highlight: [1]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\npublic class TemporaryConversationManager extends DefaultConversationManager\r\n{\r\n\t@Override\r\n\tpublic void startConversation(Object token, EntityManagerFactory emf)\r\n\t{\r\n\t\tEntityManager em = emf.createEntityManager();\r\n\t\tthis.repository.registerEntityManager(token, em);\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void endConversation(Object token, EntityManagerFactory emf)\r\n\t{\r\n\t\tEntityManager em = this.repository.findEntityManager(token);\r\n\t\tif (em != null &amp;&amp; em.isOpen())\r\n\t\t{\r\n\t\t\tem.close();\r\n\t\t}\r\n\t\tthis.repository.unregisterEntityManager(token);\r\n\t}\r\n}\r\n<\/pre>\n<p>We re-define the <em>startConversation()<\/em> &#038; <em>endConversation()<\/em> from the superclass to remove the call to <em>reattachEntityManager()<\/em> &#038; <em>detachEntityManager()<\/em> respectively.<\/p>\n<p>&nbsp;<\/p>\n<h3>E TemporaryConversationAnnotationProcessor<\/h3>\n<pre class=\"brush: java; highlight: [3,20,21]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n@Aspect\r\npublic class TemporaryConversationAnnotationProcessor implements ConversationAnnotationProcessor,\r\nOrdered, ApplicationContextAware\r\n{\r\n\r\n\tprivate ConversationManager conversationManager;\r\n\r\n\tprivate ApplicationContext ctx;\r\n\r\n\tprivate int order;\r\n\r\n\t@Pointcut(&quot;execution(public * *(..)) &amp;&amp; @annotation(beginTempConversationAnn)&quot;)\r\n\tpublic void temporaryConversationBegin(BeginTemporaryConversation beginTempConversationAnn)\r\n\t{}\r\n\r\n\t@Pointcut(&quot;execution(public * *(..)) &amp;&amp; @annotation(endTemporaryConversationAnn)&quot;)\r\n\tpublic void temporaryConversationEnd(EndTemporaryConversation endTemporaryConversationAnn)\r\n\t{}\r\n\r\n\t@Pointcut(&quot;execution(* com.jpa.conversation*.dao..*()) &amp;&amp; args(conversation,..)&quot;)\r\n\tpublic void daoExecutionJoinPoint(ConversationEnum conversation)\r\n\t{}\r\n\r\n\t@Before(&quot;temporaryConversationBegin(beginTempConversationAnn)&quot;)\r\n\tpublic void triggerBeginTemporaryConversation(BeginTemporaryConversation beginTempConversationAnn)\r\n\t{\r\n\t\tEntityManagerFactory emf = this.findEntityManagerFactoryFromContext(beginTempConversationAnn.conversationName().emf);\r\n\t\tString token = beginTempConversationAnn.conversationName().emf + &quot;-&quot; + beginTempConversationAnn.conversationName().name();\r\n\t\tthis.conversationManager.startConversation(token, emf);\r\n\t}\r\n\r\n\t@After(&quot;temporaryConversationEnd(endTemporaryConversationAnn)&quot;)\r\n\tpublic void triggerEndTemporaryConversation(EndTemporaryConversation endTemporaryConversationAnn)\r\n\t{\r\n\t\tEntityManagerFactory emf = this.findEntityManagerFactoryFromContext(endTemporaryConversationAnn.conversationName().emf);\r\n\t\tString token = endTemporaryConversationAnn.conversationName().emf + &quot;-&quot; + endTemporaryConversationAnn.conversationName().name();\r\n\t\tthis.conversationManager.endConversation(token, emf);\r\n\t}\r\n\r\n\t@Around(&quot;daoExecutionJoinPoint(conversation)&quot;)\r\n\tpublic Object interceptDaoExecution(ProceedingJoinPoint pjp, ConversationEnum conversation)\r\n\t{\r\n\t\tObject retValue = null;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif (conversation == null)\r\n\t\t\t{\r\n\t\t\t\tretValue = pjp.proceed();\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tString token = conversation.emf + &quot;-&quot; + conversation.name();\r\n\t\t\t\tEntityManagerFactory emf = this.findEntityManagerFactoryFromContext(conversation.emf);\r\n\t\t\t\tEntityManagerHolder savedEmHolder = (EntityManagerHolder) TransactionSynchronizationManager.unbindResourceIfPossible(emf);\r\n\r\n\t\t\t\tthis.conversationManager.reattachEntityManager(token, emf);\r\n\r\n\t\t\t\tretValue = pjp.proceed();\r\n\r\n\t\t\t\tthis.conversationManager.detachEntityManager(emf);\r\n\r\n\t\t\t\tif (savedEmHolder != null)\r\n\t\t\t\t{\r\n\t\t\t\t\tTransactionSynchronizationManager.bindResource(emf, savedEmHolder);\r\n\t\t\t\t}\r\n\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch (Throwable throwable)\r\n\t\t{\r\n\r\n\t\t}\r\n\r\n\t\treturn retValue;\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void triggerBeginConversation(BeginConversation annotation)\r\n\t{}\r\n\r\n\t@Override\r\n\tpublic void triggerEndConversation(EndConversation annotation)\r\n\t{}\r\n\r\n\t@Override\r\n\tpublic EntityManagerFactory findEntityManagerFactoryFromContext(String emf)\r\n\t{\r\n\t\treturn (EntityManagerFactory) this.ctx.getBean(emf);\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void setConversationManager(ConversationManager conversationManager)\r\n\t{\r\n\t\tthis.conversationManager = conversationManager;\r\n\r\n\t}\r\n\r\n\t@Override\r\n\tpublic int getOrder()\r\n\t{\r\n\t\treturn this.order;\r\n\t}\r\n\r\n\tpublic void setOrder(int order)\r\n\t{\r\n\t\tthis.order = order;\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException\r\n\t{\r\n\t\tthis.ctx = applicationContext;\r\n\t}\r\n}\r\n<\/pre>\n<p>The implementation is quite straight-forward.<\/p>\n<blockquote><p>Please notice <strong>lines 20 &#038; 21<\/strong>. We define the around pointcut for <strong>interceptDaoExecution()<\/strong> by restricting the method execution to interception to method in the DAO layer only. Indeed it is useless to intercept a method call at Service or view layer. The interception should be done only in the class where the Entity Manager injected by Spring&#8217; <strong>@PersistenceConext<\/strong> is used<\/p><\/blockquote>\n<p><a href=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/01\/temporaryconversationannotationprocessor.png\"><img loading=\"lazy\" src=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/01\/temporaryconversationannotationprocessor.png\" alt=\"\" title=\"TemporaryConversationAnnotationProcessor\" width=\"630\" height=\"308\" class=\"aligncenter size-full wp-image-649\" srcset=\"https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/01\/temporaryconversationannotationprocessor.png 773w, https:\/\/www.doanduyhai.com\/blog\/wp-content\/uploads\/2012\/01\/temporaryconversationannotationprocessor-300x147.png 300w\" sizes=\"(max-width: 630px) 100vw, 630px\" \/><\/a><\/p>\n<p>&nbsp;<\/p>\n<h1>IV Usage<\/h1>\n<h3>A Starting a new temporary conversation<\/h3>\n<pre class=\"brush: java; highlight: [4]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n@BeginTemporaryConversation(conversationName = ConversationEnum.C1)\r\npublic void doGetUserDetails(User user) {\r\n\t...\r\n\tUserDetails details = myDao.loadUserDetails(ConversationEnum.C1, user);\r\n\t...\r\n}\r\n<\/pre>\n<p>In the above code, to start a new temporary conversation, we simply need to annotate it with the <strong><em>@BeginTemporaryConversation<\/em><\/strong> annotation, providing the conversation name and indicating whether the conversation should start a transaction or not.<\/p>\n<p>Please notice in the example that we pass the <strong>ConversationEnum<\/strong> as the <strong>first parameter<\/strong> down to the DAO layer. This parameter is mandatory so the <strong>TemporaryConversationAnnotationProcessor<\/strong> knows that we&#8217;re currently in a temporary conversation context. The <strong>ConversationEnum<\/strong> is also necessary to get the name of the related <strong>Entity Manager Factory<\/strong> in Spring context and thus create the temporary <strong>Entity Manager<\/strong>.<\/p>\n<h3>B Accessing the temporary Entity Manager in DAO layer<\/h3>\n<pre class=\"brush: java; highlight: [2,3,7]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\npublic class MyDao {\r\n\t@PersistenceContext(unitName=&quot;myPersistenceUnit &quot;)\r\n\tprivate  EntityManager em;\r\n\r\n\tpublic UserDetails loadUserDetails(ConversationEnum conversation, User user) {\r\n\t\t...\r\n\t\tem.createQuery(query);\r\n\t\t...\r\n\t} \r\n }\r\n<\/pre>\n<p>The Entity Manager <strong>em<\/strong> in use in the method <strong>loadUserDetails()<\/strong> is injected by Spring. However, since the method <strong>loadUserDetails()<\/strong> is intercepted by the <strong>TemporaryConversationAnnotationProcessor<\/strong>, the injected <strong>Entity Manager<\/strong> is the one created for the temporary conversation.<\/p>\n<h3>C Ending a temporary conversation<\/h3>\n<pre class=\"brush: java; highlight: [2,6]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n@EndTemporaryConversation(conversationName = ConversationEnum.C1)\r\n@Transactional(value = &quot;myTransactionManager1&quot;, propagation = Propagation.REQUIRED)  \r\npublic void doUpdateUserDetails(User user) \r\n{  \r\n\t...\r\n\t\/\/ Clear references\r\n\t...\r\n}\r\n<\/pre>\n<p>To end a conversation, we simply annotate the last method of the conversation with <strong><em>@EndTemporaryConversation<\/em><\/strong>. <\/p>\n<p>Please notice the use of Spring standard @Transactional annotation to create a JDBC transaction. Conversations and JDBC transactions are both cross-cuting concerns and completely uncorrelated.<\/p>\n<blockquote><p>We should not forget to clean all references to entities that were loaded in the temporary Entity Manager and now become detached. These references should be removed in all beans, from Service to View layer if necessary. Forgetting to do so may result in the famous &#8220;<em>Detached entities passed to persist\/delete<\/em>&#8221; Hibernate exception if these entities are re-used in another Entity Manager.<\/p><\/blockquote>\n<h3>D Cancelling a temporary conversation<\/h3>\n<pre class=\"brush: java; highlight: [5]; title: ; wrap-lines: false; notranslate\" title=\"\">\r\n@TemporaryConversationEnd(conversationName = ConversationEnum.C1)\r\npublic void doCancelUserDetailsUpdate(User user) \r\n{  \r\n\t...\r\n\t\/\/ Clear references\r\n\t...\r\n}\r\n<\/pre>\n<p>It is also possible to rollback a temporary conversation by removing Spring @Transactional annotation.  DML statements will not be committed to the database outside of a transactional context.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Today we discuss about temporary conversations in JPA, their usage, all usage warnings and a sample implementation using Spring AOP This article follows upon a previous one about global long conversation : JPA\/Hibernate Long session<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[20,7,16],"tags":[34,36,40],"_links":{"self":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/593"}],"collection":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=593"}],"version-history":[{"count":3,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/593\/revisions"}],"predecessor-version":[{"id":13536,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/593\/revisions\/13536"}],"wp:attachment":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=593"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=593"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=593"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}