Menu

#110 Why are subordinated beans always instantiated?

v1.0 (example)
closed
None
5
2022-12-24
2022-11-24
Diego
No

Hi, just reading the documentation (https://opencsv.sourceforge.net/#recursion_into_subordinate_beans) saw this:

opencsv will instantiate the entire hierarchy of subordinate beans while reading data in, even if it does not need a subordinate bean for a particular dataset because all associated input fields are empty

Why is that? Is there a way to overcome this? Because I want subordinate beans to be null when all their binded fields are null...

Discussion

  • Scott Conway

    Scott Conway - 2022-11-25

    The reason this happens is that we separate the creation of the bean and its subornates from the actual sets. This way we don't have to worry at each field if everything has been created we just have to worry about setting one field.

    For your particular instance if a single field is being mapped to an entire object then my suggestion would be to create your own MappingStrategy - or extend whatever MappingStrategy you are using and override the setFieldValue from the AbstractMappingStrategy to allow the field to be nulled out.

     
    • Diego

      Diego - 2022-11-25

      Well, that seems a good approach. However I'm trying to null out the property whenever value == null and I don't know how to do that...

      @Override
      protected void setFieldValue(Map<Class<?>, Object> beanTree, String value, int column)
              throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException, CsvConstraintViolationException,
              CsvValidationException {
          BeanField<MonthlySetting, String> beanField = findField(column);
          if (beanField != null) {
              if (value == null && beanField.getField().getDeclaringClass() != type) {
                  // null out property
              } else {
                  Object subordinateBean = beanTree.get(beanField.getType());
                  beanField.setFieldValue(subordinateBean, value, findHeader(column));
              }
          }
      }
      
       
  • Scott Conway

    Scott Conway - 2022-11-25

    Oh good catch I was focused on the setFieldValue inside and not the beanField check. Honestly if you have null then it should be null so the question is do you have a null to begin with?

    If you look at the javadocs you willsee the CsvToBeanBuilder has a withFieldAsNull to help opencsv determine what should be used as a null https://opencsv.sourceforge.net/apidocs/com/opencsv/bean/CsvToBeanBuilder.html#withFieldAsNull-com.opencsv.enums.CSVReaderNullFieldIndicator- Are you setting it?

    You can see an example in the CsvToBeanTest.

    @DisplayName("Blank lines are ignored when withIgnoreEmptyLine is set to true and withFieldAsNull is set to EMPTY_SEPARATORS.")
    @Test
    public void parseBeanWithIgnoreEmptyLinesAndEmptyIsNull() {
        HeaderColumnNameMappingStrategy<MockBean> strategy = new HeaderColumnNameMappingStrategy<>();
        strategy.setType(MockBean.class);
        List<MockBean> beanList = new CsvToBeanBuilder<MockBean>(new StringReader(TEST_STRING_WITH_BLANK_LINES))
                .withMappingStrategy(strategy)
                .withIgnoreEmptyLine(true)
                .withFieldAsNull(CSVReaderNullFieldIndicator.EMPTY_SEPARATORS)
                .build().parse();
    
        assertEquals(2, beanList.size());
        assertTrue(beanList.contains(new MockBean("kyle", null, "abc123456", 123, 0.0)));
        assertTrue(beanList.contains(new MockBean("jimmy", null, "def098765", 456, 0.0)));
    }
    
     
  • Diego

    Diego - 2022-11-26

    Yeah, I set that field to EMPTY_SEPARATORS. But it only nulls out the field annotated with @CsvBind... not the entire subordinate bean, using documentation language :)

    To clarify the situation:

    public class A {
    
        @CsvRecurse
        private B b;
    
    }
    
    public class B {
    
        @CsvBindByName(column = "string")
        private String string;
    
    }
    

    This returns A with b != null, but string == null. The behaviour I expected (and I think it makes more sense somehow) is an A object with b == null

     

    Last edit: Diego 2022-11-26
  • Andrew Rucker Jones

    Scott's been doing a great job of helping you so far, but I wanted to add a little something about the reasoning for doing it the way we did it.

    It's essentially for the same reason we return an empty bean for an empty line. The line is there, just without data. The bean is nothing more than a vessel. If you want to know about the data, you will have to check the bean fields. Similar to the way some programmers treat getters that return a List: if the field is null, an empty List is created for the field and returned because the getter promises to return a List.

     
    • Diego

      Diego - 2022-11-26

      I completely understand it, it's just in my specific case I need to null out the object, so I'll need to perform additional operations which could be avoided if the behaviour was like I explained... no worries! This is such a nice tool anyway

       
  • Scott Conway

    Scott Conway - 2022-11-26

    AHHHH okay - I see a little better what is going on. I thought you were trying to annotate/populate an entire class (like in our tests where we do a Joda DateTime object).

    No for what you are doing there is no direct solution. In cases like this I suggest you create what I call a DTO - DataTransferObject (I think technically Factory is correct but an old coworker called it DTO and the name stuck with me) to populate all the values and then create the real objects for you.

    So in your case you would have something like this:

    public class ADTO {
    
        @CsvRecurse
        private BDTO bdto;
    
        //  All getters and setters here
    
    
        public A createObject() {
            A  a = new A();
    
            a.setB(bdto.createObject());
            return a;
        }
    
    }
    
    public class BDTO {
    
        @CsvBindByName(column = "string")
        private String string;
    
        // getters and setters defined and if you want to be fancy
        // have each of the setters set a wasSet boolean that you could
        // check in the createObject but I am keeping it semi-simple <BG>
    
        public B createObject() {
            if (string == null) {
                return null;
            }
    
            b = new B();
            b.setString(getString());
    
            return b;
        }
    }
    

    And personally I would just have an single DTO that matches your csv file and use the createObject to create the entire structure. I would only do multiple nexted DTOs if there was some complex business logic/validation/transformation you wanted to perform when create the objects.

     
    • Diego

      Diego - 2022-11-26

      Well, that is a good idea. I'll take it into account. Thanks!

       
  • Scott Conway

    Scott Conway - 2022-12-24
    • status: open --> closed
    • assigned_to: Scott Conway
     

Log in to post a comment.

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.