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 示例最接近但根本没有帮助。我用谷歌搜索但无济于事。
答案应该是:
- 包含真正的 Java(非 Clojure)代码直至调用 connection.transact.
- 可重复使用/不需要复制和粘贴其他实体。
- 只更新已更改的属性(?)- 我知道我应该只处理值实际已更改的属性(正确吗?)。
- 正确解决多人同时编辑的问题,即用户不应该覆盖彼此的工作。这通常通过乐观锁定来解决。那么如何在 Java 的 Datomic 中进行乐观锁定呢?或者有其他策略吗?
(最后,旁注 - 不是问题本身的一部分。Datomic Java 文档中为什么没有解释 "editing an entity" 这样的核心用例,也没有官方示例展示如何以最好的方式解决这个问题?这种感觉 "Datomic Java API" 并没有得到真正的支持。在我看来 Java 和 Clojure 在不同的范例上工作,所以简单地移植一个 Clojure API 1:1到Java不构成JavaAPI呢。
我不应该能够稍微注释一下客户(比如 @Id 和 @Version),然后调用 connection.persist(customer); 并完成它?我知道,可怕的 ORM 龙再次抬起它丑陋的头。但是,嘿,也许现在我会学习如何以更优雅的方式做到这一点。)
回答您的一些问题:
- 你没有只交易发生变化的字段,Datomic会在交易运行时为你省略没有发生变化的属性。
- Datomic 不提供 class 映射层,而且可能永远不会。我不知道社区中开发了哪一种,这也不让我感到惊讶,因为出于通用性考虑,该社区倾向于支持面向数据的(而不是基于 class 的)架构。因此,您找不到将 Datomic 数据一般地转换为 POJO 的实用程序,例如 ORM 所提供的那样。
- 这并不意味着 Java 是这里的第二个 class 公民 - 但 Datomic 会向您施加压力(如果您要求创建者为了您自己的利益)使用数据结构,例如列表和映射而不是 POJO 来传达信息。这在 Clojure 中确实比在 Java 中更加地道。
- 我个人强烈建议为您的业务逻辑使用实体(即
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;
}
}
我在 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 示例最接近但根本没有帮助。我用谷歌搜索但无济于事。
答案应该是:
- 包含真正的 Java(非 Clojure)代码直至调用 connection.transact.
- 可重复使用/不需要复制和粘贴其他实体。
- 只更新已更改的属性(?)- 我知道我应该只处理值实际已更改的属性(正确吗?)。
- 正确解决多人同时编辑的问题,即用户不应该覆盖彼此的工作。这通常通过乐观锁定来解决。那么如何在 Java 的 Datomic 中进行乐观锁定呢?或者有其他策略吗?
(最后,旁注 - 不是问题本身的一部分。Datomic Java 文档中为什么没有解释 "editing an entity" 这样的核心用例,也没有官方示例展示如何以最好的方式解决这个问题?这种感觉 "Datomic Java API" 并没有得到真正的支持。在我看来 Java 和 Clojure 在不同的范例上工作,所以简单地移植一个 Clojure API 1:1到Java不构成JavaAPI呢。 我不应该能够稍微注释一下客户(比如 @Id 和 @Version),然后调用 connection.persist(customer); 并完成它?我知道,可怕的 ORM 龙再次抬起它丑陋的头。但是,嘿,也许现在我会学习如何以更优雅的方式做到这一点。)
回答您的一些问题:
- 你没有只交易发生变化的字段,Datomic会在交易运行时为你省略没有发生变化的属性。
- Datomic 不提供 class 映射层,而且可能永远不会。我不知道社区中开发了哪一种,这也不让我感到惊讶,因为出于通用性考虑,该社区倾向于支持面向数据的(而不是基于 class 的)架构。因此,您找不到将 Datomic 数据一般地转换为 POJO 的实用程序,例如 ORM 所提供的那样。
- 这并不意味着 Java 是这里的第二个 class 公民 - 但 Datomic 会向您施加压力(如果您要求创建者为了您自己的利益)使用数据结构,例如列表和映射而不是 POJO 来传达信息。这在 Clojure 中确实比在 Java 中更加地道。
- 我个人强烈建议为您的业务逻辑使用实体(即
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;
}
}