mkarneim / pojobuilder

A Java Code Generator for Pojo Builders
Other
334 stars 44 forks source link

copy only not set fields #166

Closed blackfoxoh closed 4 years ago

blackfoxoh commented 4 years ago

Hi, I would like to see something similar to copy() but with a slight difference. Let me describe the usecase: I want to define some testdata so I use a builder on my jpa entitys. I would like to keep my testdata-definition separat and just want to define the attributes currently necessary for the testcase, e.g. for an article I maybe just want to specify its articlenumber (key) and measurements but the weight is not needed. In the testcase I'd like to define a builder with desired values, then my testframework should lookup the entity by it's key (if present) and just modify the values set in the builder (example above: keep weigth of the article as is). If no article is found a new one will be created and weight is empty then. Of course the lookup of JPA-entitys and persisting them is not part of my request to pojobuilder, just part of usecasedescription.

Best way to achieve this might be to have a buildWithBase(T t) or something like this which uses the passed in object (if not null) instead a new instance of T to set the builder-values. What do you think about that? Or is there already a way to achive this I didn't notice?

best regards Ralf

drekbour commented 4 years ago

That's not a builder, it's an updater :) PB doesn't support update(T t) though it may be possible to trick it into doing so by annotating a factory method that contains some bad code to return the existing T instead of a new one.

private static Thing temp; // NOT thread safe, use ThreadLocal if required
@GeneratePojoBuilder public static Thing factoryMethod() {
  return temp == null ? new Thing() : temp;
}

This is an evil builder but it's worth noting you can generate multiple builders so this can live alongside whatever you have currently.

blackfoxoh commented 4 years ago

Thank you for your feedback. That sounds possible but I agree with you it is really evil. I would have to write a factory method for every entity, generate a seperate builder and even the "api" to use it would require to first set temp before applying the builder... really complex in contrast to what would be necessary if PB would support update(T t).

I agree that "Updater" is the correct term for what I have described. Thinking about this I have to correct my description above: What I want to achieve is a builder, not an updater. In fact the existing copy(T t) method is nearly what I need with the only problem that I need the other values already set in the builder to have precedence. My usecase doesn't allow to call copy(T t) before defining the other data. So a copyNotSetFields(T t) (name of the method might be optimizable) which initializes the builders fields only for not already initialized ones.

I stumbled upon PB in an article in iX magazine by M. Karneim and O. Kraeft (German) which promotes PB for the usecase of providing testdata. I really like this idea but to be really helpful for us this functionality would be necessary for efficently handle our testcases. Maybe there is a chance to add this (as a configurable option).

drekbour commented 4 years ago

I don't think copyNotSetFields is a good API addition in general though I understand your usecase. What counts as "unset"? null might be an intent, what about primitives, is 0 "unset"? If you look inside the PB builder, it explicitly tracks what has been set so on the builder has that knowledge (not the bean).

OPTION 1 Merge Builder A generic api might be ThingBuilder merge(ThingBuilder builder) where the client call would be new ThingBuilder().copy(instance).merge(template).build()

Implementing this to the point of production-worthy code is non trivial though as PB has struggled for a long time figure out builder interfaces and builder inheritance (merge(Builder<? super T> builder)). Features like this only add to the problem :)

OPTION 2 Update Pojo Your first request void update(T item) isn't terrible. Client call is template.update(new ThingBuilder().copy(instance).build())

This is easy to implement but the builder might hold constructor properties that cannot be set

OPTION 3 Use a lambda Use Consumer<Thing> as your template, nothing to do with PB. Client call is template.accept(new ThingBuilder().copy(instance).build())

blackfoxoh commented 4 years ago

let's assume we hav a pojo Article with field nr. If we create a pojobuilder for this it has the following two fields and could have the following method:

  protected int value$nr$int;
  protected boolean isSet$nr$int;

...
  public ArticleBuilder copyNotSetFields(Article pojo) {
    if (!isSet$nr$int) {
      withNr(pojo.getNr());
    }
    ...
  }

Result of new ArticleBuilder().withNr(123).copyNotSetFields(someArticle) would be the same as if you do new ArticleBuilder().copy(someArticle).withNr(123) but with the first (new) solution the copy call could happen later.

Maybe a better API would be

  public ArticleBuilder copy(Article pojo) {
    copy(pojo, true);
  }

  public ArticleBuilder copy(Article pojo, boolean overwrite) {
    if (overwrite || !isSet$nr$int) {
      withNr(pojo.getNr());
    }
    ...
  }

your OPTION 3: I don't really got it so far. your example client call passes a 1:1 copy of the instance to the accept method. So far I haven't won much, the builder in this scenario is just used to produce a clone but nothing of the functionality (merge/update) is actually done here...!? Or please open my eyes if I missed something...

mkarneim commented 4 years ago

Hi @blackfoxoh.

I am not sure if I understand your use case completely, but maybe you can help yourself by using PB's Factory Method feature?

blackfoxoh commented 4 years ago

Hi I'm not sure if I understand your suggestion with the factory method - I had a look at that but at the moment I don't know how this should solve my problem easily. I can try to describe my usecase again:

mkarneim commented 4 years ago

I am sorry, but using the copy method does not help you here since it does not copy values from the builder to the pojo but vice versa. I guess it is valid to say that this is more than just a "slight difference".

Anyway, for your use case I have two things to say. 1) Like @drekbour said, a builder is not well suited for modifying existing objects. This would require a mutator. In can see that at a first glance both operators look alike but in fact aren't. Nevertheless you could use the factory method to redefine the builder's semantics. To do this, just declare your own factory method and annotate it with @GeneratePojoBuilder. Then implement it like you described it yourself (find a suitable object in the database by using the method parameters, or create one if you don't find one, and finally return it).

2) But I'd like to suggest that you rethink your test design. Instead of providing a database full of data before each individual test starts, just clear it completely. Then, as a first step of each individual test, inside the "given" block, insert the minimal set of data that is required to run the test. It's best practice to use a combination of the builder pattern and a builder factory to do this without bloating the test with irrelevant information. In this approach you don't need a mutator at all.

drekbour commented 4 years ago

I think a good solution is Option 1 earlier (merge builders). This doesn't break the basic contract of PojoBuilder and opens up new avenues of flexibility. It is however notably complex and probably unlikely to be implemented!

blackfoxoh commented 4 years ago

Thank you for your feedbacks. I will have a look if I can solve my issue with your suggestions or if I will use a completly different approach.

mkarneim commented 4 years ago

Nevertheless you could use the factory method to redefine the builder's semantics. To do this, just declare your own factory method and annotate it with @GeneratePojoBuilder. Then implement it like you described it yourself (find a suitable object in the database by using the method parameters, or create one if you don't find one, and finally return it).

To clarify what I meant with "redefining the builder's semantics" I created this little example:

The following code defines the semantics of the ContactBuilder as described above.

PojoFactory .java ```java import java.time.LocalDate; import java.util.List; import net.karneim.pojobuilder.GeneratePojoBuilder; public class PojoFactory { private static Database DATABASE = Database.instance(); // This method will be called by the generated builder's "build" method. @GeneratePojoBuilder public static Contact newContact(LocalDate dateOfBirth) { List list = DATABASE.loadContacts(dateOfBirth); if (!list.isEmpty()) { return list.get(0); } else { Contact result = new Contact("dummyName"); result.setDateOfBirth(dateOfBirth); return result; } } } ```

Here is the generated ContactBuilder (click to expand):

ContactBuilder.java ```java import java.time.LocalDate; import javax.annotation.processing.Generated; import net.karneim.pojobuilder.GwtIncompatible; @Generated("PojoBuilder") public class ContactBuilder implements Cloneable { protected ContactBuilder self; protected LocalDate value$dateOfBirth$java$time$LocalDate; protected boolean isSet$dateOfBirth$java$time$LocalDate; protected String value$name$java$lang$String; protected boolean isSet$name$java$lang$String; protected Long value$id$java$lang$Long; protected boolean isSet$id$java$lang$Long; protected String value$profession$java$lang$String; protected boolean isSet$profession$java$lang$String; /** * Creates a new {@link ContactBuilder}. */ public ContactBuilder() { self = (ContactBuilder)this; } /** * Sets the default value for the dateOfBirth property. * * @param value the default value * @return this builder */ public ContactBuilder withDateOfBirth(LocalDate value) { this.value$dateOfBirth$java$time$LocalDate = value; this.isSet$dateOfBirth$java$time$LocalDate = true; return self; } /** * Sets the default value for the name property. * * @param value the default value * @return this builder */ public ContactBuilder withName(String value) { this.value$name$java$lang$String = value; this.isSet$name$java$lang$String = true; return self; } /** * Sets the default value for the id property. * * @param value the default value * @return this builder */ public ContactBuilder withId(Long value) { this.value$id$java$lang$Long = value; this.isSet$id$java$lang$Long = true; return self; } /** * Sets the default value for the profession property. * * @param value the default value * @return this builder */ public ContactBuilder withProfession(String value) { this.value$profession$java$lang$String = value; this.isSet$profession$java$lang$String = true; return self; } /** * Returns a clone of this builder. * * @return the clone */ @Override @GwtIncompatible public Object clone() { try { ContactBuilder result = (ContactBuilder)super.clone(); result.self = result; return result; } catch (CloneNotSupportedException e) { throw new InternalError(e.getMessage()); } } /** * Returns a clone of this builder. * * @return the clone */ @GwtIncompatible public ContactBuilder but() { return (ContactBuilder)clone(); } /** * Creates a new {@link Contact} based on this builder's settings. * * @return the created Contact */ public Contact build() { try { Contact result = PojoFactory.newContact(value$dateOfBirth$java$time$LocalDate); if (isSet$name$java$lang$String) { result.setName(value$name$java$lang$String); } if (isSet$id$java$lang$Long) { result.setId(value$id$java$lang$Long); } if (isSet$profession$java$lang$String) { result.setProfession(value$profession$java$lang$String); } return result; } catch (RuntimeException ex) { throw ex; } catch (Exception ex) { throw new RuntimeException(ex); } } } ```

Below you find the other classes used in this example (click to expand):

Contact.java ```java import java.time.LocalDate; public class Contact { private Long id; private String name; private LocalDate dateOfBirth; private String profession; public Contact(String name) { this.name = name; } 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 LocalDate getDateOfBirth() { return dateOfBirth; } public void setDateOfBirth(LocalDate dateOfBirth) { this.dateOfBirth = dateOfBirth; } public String getProfession() { return profession; } public void setProfession(String profession) { this.profession = profession; } @Override public String toString() { return "Contact [id=" + id + ", name=" + name + ", dateOfBirth=" + dateOfBirth + ", profession=" + profession + "]"; } } ```
Database.java ```java import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class Database { private static Database INSTANCE = new Database(); public static Database instance() { return INSTANCE; } private List storedContacts = new ArrayList<>(); private Database() { storedContacts.add(newContact("Albert Einstein", LocalDate.of(1879, 3, 14), "physicist", 1L)); storedContacts.add(newContact("Max Planck", LocalDate.of(1858, 4, 23), "physicist", 2L)); } private Contact newContact(String name, LocalDate dateOfBirth, String profession, long id) { Contact result = new Contact(name); result.setDateOfBirth(dateOfBirth); result.setProfession(profession); result.setId(id); return result; } public List loadContacts(LocalDate dateOfBirth) { // TODO impl some REAL database access return storedContacts.stream().filter(c -> c.getDateOfBirth().equals(dateOfBirth)).collect(Collectors.toList()); } } ```

The following program demonstrates how you could use the generated ContactBuilder:

Main.java ```java import java.time.LocalDate; public class Main { public static void main(String[] args) { Contact contact1 = $Contact() .withDateOfBirth(LocalDate.of(1879, 3, 14)) .withProfession("Patent examiner") .build(); Contact contact2 = $Contact() .withDateOfBirth(LocalDate.of(2000, 11, 30)) .build(); System.out.println("1 = " + contact1); System.out.println("2 = " + contact2); } public static ContactBuilder $Contact() { return new ContactBuilder(); } } ```

When runnning this program you get the following output:

1 = Contact [id=1, name=Albert Einstein, dateOfBirth=1879-03-14, profession=Patent examiner]
2 = Contact [id=null, name=dummyName, dateOfBirth=2000-11-30, profession=null]