ivom
ivom

Reputation: 123

Datomic "update" in Java

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:

  1. Comprise real Java (not Clojure) code up to calling connection.transact.
  2. Be reusable / not require copy&paste for other entities.
  3. Only update attributes that have changed (?) - I understand that I should only transact the attributes for which the value has actually changed (correct?).
  4. Resolve concurrent edits by multiple users properly, i.e. users should not overwrite each other's work. This is normally solved by optimistic locking. So how do I do optimistic locking in Datomic in Java? Or is there some other strategy?

(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

Answers (1)

Valentin Waeselynck
Valentin Waeselynck

Reputation: 6051

To answer some of your questions:

  1. you don't have to transact only the fields that have changed, Datomic will elide the attributes that have not changed for you when the transaction is run.
  2. Datomic does not provide a class-mapping layer, and probably never will. I don't know of one that has been developed in the community, nor does it surprise me, since this community tends to favour data-oriented (as opposed to class-based) architectures for the sake of genericity. As a consequence, you won't find utilities to generically translate from Datomic data to POJOs, as provided by ORMs for example.
  3. This does not mean that Java is a second-class citizen at all here - but Datomic will pressure you (for your own good if you ask the creators) to use data structures like lists and maps instead of POJOs to convey information. This is indeed more idiomatic in Clojure than in Java.
  4. I personally strongly recommend to use Entities (i.e instances of the 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

Related Questions