Java 中的数据 "update"

Datomic "update" in Java

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

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

举个例子,假设我们有一个具有业务属性名称、电子邮件和 phone 的客户实体。所以在 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 schema 声明了相应的属性:customer/name, :customer/email, :customer/phone, etc.

有一个 "Edit customer" 表单显示要更改的用户的 3 个业务属性。假设我更改了姓名和电子邮件并保存了表格。

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

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

答案应该是:

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

(最后,旁注 - 不是问题本身的一部分。Datomic Java 文档中为什么没有解释 "editing an entity" 这样的核心用例,也没有官方示例展示如何以最好的方式解决这个问题?这种感觉 "Datomic Java API" 并没有得到真正的支持。在我看来 Java 和 Clojure 在不同的范例上工作,所以简单地移植一个 Clojure API 1:1到Java不构成JavaAPI呢。 我不应该能够稍微注释一下客户(比如 @Id@Version),然后调用 connection.persist(customer); 并完成它?我知道,可怕的 ORM 龙再次抬起它丑陋的头。但是,嘿,也许现在我会学习如何以更优雅的方式做到这一点。)

回答您的一些问题:

  1. 没有只交易发生变化的字段,Datomic会在交易运行时为你省略没有发生变化的属性。
  2. Datomic 不提供 class 映射层,而且可能永远不会。我不知道社区中开发了哪一种,这也不让我感到惊讶,因为出于通用性考虑,该社区倾向于支持面向数据的(而不是基于 class 的)架构。因此,您找不到将 Datomic 数据一般地转换为 POJO 的实用程序,例如 ORM 所提供的那样。
  3. 这并不意味着 Java 是这里的第二个 class 公民 - 但 Datomic 会向您施加压力(如果您要求创建者为了您自己的利益)使用数据结构,例如列表和映射而不是 POJO 来传达信息。这在 Clojure 中确实比在 Java 中更加地道。
  4. 我个人强烈建议为您的业务逻辑使用实体(即 datomic.Entity class 的实例)而不是 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;
    }
}