【问题标题】:Datomic "update" in JavaJava中的原子“更新”
【发布时间】:2016-03-31 22:23:50
【问题描述】:

我用 Java 做我的项目(后端)。我不想切换到 Clojure(反正还没有)。

Datomic 然而看起来很有趣并且它声明它有一个 Java API,但我仍然有几个未解决的问题,最重要的是这个。

举个例子,假设我们有一个具有业务属性名称、电子邮件和电话的客户实体。所以在Java中,我们有这样的东西:

public class Customer {
  private Long id;
  private String name;
  private String email;
  private String phone;
  private Long version; // ? - see 4. below
  // getters, setter, toString, hashCode, equals, business logic, etc.
}

Datomic 架构声明了相应的属性:customer/name、:customer/email、:customer/phone 等。

有一个“编辑客户”表单公开了要更改的用户的 3 个业务属性。假设我更改了姓名和电子邮件并保存表单。

现在,我应该怎么做才能将更改保存到 Datomic?如何建立交易?

Datomic 提供的示例过于简单,CompareAndSwap 示例最接近但根本没有帮助。我用谷歌搜索但无济于事。

答案应该是:

  1. 包含真正的 Java(不是 Clojure)代码,直到调用 connection.transact。
  2. 可重复使用/不需要复制和粘贴其他实体。
  3. 仅更新已更改的属性 (?) - 我知道我应该只处理值实际已更改的属性(正确吗?)。
  4. 正确解决多个用户的并发编辑问题,即用户不应覆盖彼此的工作。这通常通过乐观锁定来解决。那么如何在 Java 中的 Datomic 中进行乐观锁定呢?还是有其他策略?

(最后,附注 - 不是问题的一部分。Datomic Java 文档中没有解释“编辑实体”这样的核心用例,也没有官方示例说明如何处理这个问题最好的方法?这种感觉“Datomic Java API”并没有真正得到支持。在我看来,Java 和 Clojure 工作在不同的范式上,所以简单地将 Clojure API 1:1 移植到 Java 还不能构成 Java API。 我不应该对客户进行一点注释吗(例如 @Id@Version)然后调用 connection.persist(customer); 就这样结束了吗?我知道,可怕的ORM巨龙又抬起了丑陋的头。但是,嘿,也许现在我会以一种更优雅的方式学习如何做到这一点。)

【问题讨论】:

    标签: java datomic


    【解决方案1】:

    回答您的一些问题:

    1. 您没有必须只处理已更改的字段,Datomic 会在事务运行时为您删除未更改的属性。
    2. Datomic 不提供类映射层,而且可能永远不会。我不知道社区中已经开发了哪一个,这也不让我感到惊讶,因为为了通用性,这个社区倾向于支持面向数据(而不是基于类)的架构。因此,您将找不到将 Datomic 数据通用转换为 POJO 的实用程序,例如 ORM 提供的。
    3. 这并不意味着 Java 在这里完全是二等公民 - 但 Datomic 会迫使您(如果您要求创建者,为了您自己的利益)使用列表和地图等数据结构而不是 POJO 来传达信息。这在 Clojure 中确实比在 Java 中更惯用。
    4. 我个人强烈建议在您的业务逻辑中使用实体(即datomic.Entity 类的实例)而不是 POJO - 至少在编写映射代码之前尝试一下,看看这是否是一个问题。您失去一些静态保证 - 可能还有很多样板文件。尽管如此,下面的实现确实使用了 POJO。

    我在下面给了你我的实现。基本上,您将 Customer 对象转换为事务映射,并使用 :db.fn/cas 事务函数来获得您想要更新的并发保证。

    如果您是一位经验丰富的 Java 开发人员,那么这对您来说可能看起来很不优雅——我知道那种感觉。同样,这并不意味着您不能从 Java 中获得 Datomic 的好处。您是否可以遵守面向数据的 API 取决于您,而且这个问题并不是 Datomic 特有的 - 尽管 Datomic 倾向于将您推向面向数据的方向,例如通过实体。

    import datomic.*;
    
    import java.util.List;
    import java.util.Map;
    
    public class DatomicUpdateExample {
    
        // converts an Entity to a Customer POJO
        static Customer customerFromEntity(Entity e){
            if(e == null || (e.get(":customer/id") == null)){
                throw new IllegalArgumentException("What you gave me is not a Customer entity.");
            }
            Customer cust = new Customer();
            cust.setId((Long) e.get(":customer/id"));
            cust.setName((String) e.get(":customer/name"));
            cust.setEmail((String) e.get(":customer/email"));
            cust.setPhone((String) e.get(":customer/phone"));
            cust.setVersion((Long) e.get(":model/version"));
            return cust;
        }
    
        // finds a Customer by
        static Customer findCustomer(Database db, Object lookupRef){
            return customerFromEntity(db.entity(lookupRef));
        }
    
        static List txUpdateCustomer(Database db, Customer newCustData){
            long custId = newCustData.getId();
            Object custLookupRef = Util.list(":customer/id", custId);
            Customer oldCust = findCustomer(db, custLookupRef); // find old customer by id, using a lookup ref on the :customer.id field.
            long lastKnownVersion = oldCust.getVersion();
            long newVersion = lastKnownVersion + 1;
            return Util.list( // transaction data is a list
                    Util.map( // using a map is convenient for updates
                            ":db/id", Peer.tempid(":db.part/user"),
                            ":customer/id", newCustData.getId(), // because :customer/id is a db.unique/identity attribute, this will map will result in an update
                            ":customer/email", newCustData.getEmail(),
                            ":customer/name", newCustData.getName(),
                            ":customer/phone", newCustData.getPhone()
                    ),
                    // 'Compare And Swap': this clause will prevent the update from happening if other updates have occurred by the time the transaction is executed.
                    Util.list(":db.fn/cas", custLookupRef, ":model/version", lastKnownVersion, newVersion)
            );
        }
    
        static void updateCustomer(Connection conn, Customer newCustData){
            try {
                Map txResult = conn.transact(txUpdateCustomer(conn.db(), newCustData)).get();
            } catch (InterruptedException e) {
                // TODO deal with it
                e.printStackTrace();
            } catch (Exception e) {
                // if the CAS failed, this is where you'll know
                e.printStackTrace();
            }
        }
    }
    
    class Customer {
        private Long id;
        private String name;
        private String email;
        private String phone;
        private Long version;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    
        public Long getVersion() {
            return version;
        }
    
        public void setVersion(Long version) {
            this.version = version;
        }
    }
    

    【讨论】:

    • 您好 Valentin,感谢您的回答。我有两个问题:1)我不明白你为什么要在更新中创建一个新的 tempid。我在没有它的情况下进行了更新。 2) 如果版本不匹配,我无法使 db.fn/cas 抛出。我在这里更新了我的示例项目:github.com/ivos/datomic-java-sample CustomerServiceTest.update_Conflict() 测试失败。
    • 1) tempids 不会因为 upsert 行为而受到伤害,这就是我一直放置它们的原因。 2)您的测试不起作用,因为在您的 update() 方法中,以前的版本是根据数据库调用计算的,而不是根据您作为参数传递的 Customer(以及您伪造的版本)计算的。
    • 2) 对,我现在也得出了同样的结论,只是发现你已经在这里指出了这一点。所以,关于你自己的代码,lastKnownVersion 应该取自 newCustData,而不是 oldCust。
    • 我接受您的回答,因为您帮助我完成了乐观锁定工作。生成的支持基本创建、获取、更新和列表的通用 Datomic 存储库在这里:github.com/ivos/datomic-java-sample/blob/master/src/main/java/… 我所遇到的所有问题都以某种方式得到了解决。谢谢。
    猜你喜欢
    • 2017-03-13
    • 1970-01-01
    • 1970-01-01
    • 2012-07-16
    • 2014-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-20
    相关资源
    最近更新 更多