Reputation: 123
I do my projects (backends) in Java. I don't feel like switching to Clojure (not yet anyway).
Datomic however looks interesting and it declares it has a Java API, but I still do have a couple of open issues, the most important being this.
For the sake of an example, say we have a Customer entity with business attributes name, email and phone. So in Java, we have something like:
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.
}
The Datomic schema declares corresponding attributes :customer/name, :customer/email, :customer/phone, etc.
There is an "Edit customer" form exposing the 3 business attributes for the user to be changed. Say I change the name and email and save the form.
Now, what exactly am I supposed to do to save the change into Datomic? How do I build the transaction?
The examples provided with Datomic are way too simplistic, the CompareAndSwap example coming closest but not really helpful at all. I did my googling but to no avail.
The answer should:
(Finally, a side-note - not part of the question proper. How come such a core use case as "editing an entity" is not explained in Datomic Java docs nor is there an official example showing how to approach this in the best way? This sort of feels the "Datomic Java API" is not really supported. It seems to me Java and Clojure work on different paradigms, so simply porting a Clojure API 1:1 to Java does not constitute a Java API yet. Shouldn't I be able to annotate Customer a bit (like @Id and @Version) and then just call connection.persist(customer); and be done with it? I know, the dreaded ORM dragon raising its ugly head again. But hey, maybe now I will learn how to do this in a much more elegant way.)
Upvotes: 3
Views: 870
Reputation: 6051
To answer some of your questions:
datomic.Entity
class) instead of POJOs for your business logic - at least give it a try and see if that's an issue before writing mapping code. You will lose some static guarantees - and probably a lot of boilerplate. Nonetheless, the implementation below does use POJOs.I have given you my implementation below. Basically, you convert your Customer object into a transaction map, and use the :db.fn/cas
transaction function to get the concurrency guarantees you wanted for update.
If you're a seasoned Java developer, this will probably look very not elegant to you - I know that feeling. Again, this doesn't mean that you can't reap the benefits of Datomic from Java. Whether you can abide by a data-oriented API or not is up to you, and this problem is not specific to Datomic - although Datomic tends to press you towards data-orientation, for example through Entities.
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;
}
}
Upvotes: 1