0

I'm creating a SpringBoot/Angular 8 Application and am having some issues when attempting to update the decoupled frontend/backend objects. When I send a json post request containing the angular model, the @JsonIgnore'd or otherwise missing values are updated to null instead of being ignored.

Related Issue:

This stack overflow question is closely related, and the recommended solution does work, but it breaks/bypasses a bunch of the Jackson/Hibernate annotations (e.g. @Inheritance(strategy=InheritanceType.JOINED) and @Transient), so I would like to look for alternative solutions if possible: hibernate partially update when entity parsed with json

Example Java POJO

@Entity
@Table(name = "users")
@DynamicUpdate
public class User implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String a;

    @Column
    private String b;

    @Column (nullable = false)
    @JsonIgnore
    private String c;

   // standard getters and setters
}

Example Java Rest API

@RequestMapping(value = "/api/user", method = RequestMethod.POST)
public @ResponseBody ResponseEntity<User> saveOrUpdate(@RequestBody User user, BindingResult result, HttpServletRequest request) {
    if(user.getId()!=null){
        user=ws.update(user);
    }else{
        user=us.create(user);
    }
    return new ResponseEntity<>(user,HttpStatus.OK);
}

Example Angular Service Method

saveUser(user:User): Observable<User>{
    const body = JSON.stringify(user);
    return this.http.post<User>('/api/user', body).pipe(map( response => response));
}

Existing Database model:

{ 
  id:1,
  a:"some value a"
  b:"some value b"
  c:"some value c"
}

Angular Post Body:

{ 
  id:1,
  a:"some value a2"
  b:"some value b2"
}

Expected Hibernate Query:

update users set a=?,b=? where id=?

Actual Hibernate Query:

update users set a=?,b=?,c=? where id=? C, in this case, is a null value.

coltonfranco
  • 147
  • 2
  • 15
  • 1
    you will need to find the use first User foundUser=ws.findById(id); after that BeanUtils.copyProperties(newUser, foundUser); and after save it. – Jonathan JOhx Jul 23 '19 at 22:06
  • Don't you have to verify your database if the user actually exists or not? – Coder Jul 23 '19 at 23:17
  • it is still an unresolved ticket @spring-data-jpa... https://stackoverflow.com/q/43780226/592355, Johnatan Fox' approach sounds straightforward! – xerx593 Jul 24 '19 at 00:18

4 Answers4

1

Don't use update method with the object sent from angular as it replaces the whole original object from the database. What i suggest to you in case of update is to retrieve the existent object from database (for example with spring-data repository findOne method), then copy only modified fields ignoring null values. You can use BeanUtils copyProperties method from Apache commons library. The original method doesn't ignore null values when copying, so you have to override it a little :

public class NullAwareBeanUtilsBean extends BeanUtilsBean {
    @Override
    public void copyProperty(Object dest, String name, Object value) throws IllegalAccessException, InvocationTargetException {
        if (value == null)
            return;
        super.copyProperty(dest, name, value);
    }
}

This test reproduces what you want to do :

Class to update :

public class A {

    private String foo;

    private String bar;

    public String getFoo() {
        return foo;
    }

    public void setFoo(String foo) {
        this.foo = foo;
    }

    public String getBar() {
        return bar;
    }

    public void setBar(String bar) {
        this.bar = bar;
    }
}

Test class :

import java.lang.reflect.InvocationTargetException;

import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.lang3.builder.ToStringBuilder;

import com.zpavel.utils.NullAwareBeanUtilsBean;

public class MyTest {

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException {
        A a1 = new A();
        a1.setFoo("foo");
        A a2 = new A();
        a2.setBar("bar");
        BeanUtilsBean notNull = new NullAwareBeanUtilsBean();
        notNull.copyProperties(a2, a1);
        System.out.println(ToStringBuilder.reflectionToString(a2));
    }
}
zpavel
  • 951
  • 5
  • 11
0

Create a separate model if the form is specific to a subset of data on the entity. As you said you want to take users FirstName,LastName,other personal details that should be in Person model and all the login account related part in LoginAccount model

So your model becomes like :

User Entity

id
a
b
c
etc.
FormModel

id
a
b
etc... and other fields that should only be visible and updatable within this specific form.

For example, a User Entity probably contains many different attributes about the user. A login form only requires a username and password. A user registration form would contain only the basic information for that user so sign-up is easy. A profile edit form would contain more attributes a user can update. You might want to keep the password update form on a separate page containing only the password field. Lastly, you might have an admin user who should be able to enable or disable users but shouldn't have access to sensitive information such as a social security number. It wouldn't make sense to use the User Entity that would provide the full User JSON object in each one of these forms. Instead, create a view model for each with the specific fields needed.

bschupbach
  • 103
  • 1
  • 1
  • 9
  • Your suggestion makes sense, but it doesn't address the core problem I'm facing. That is that the @JsonIgnore and Lazy Loaded primitives are set to null when posting. Your suggestion implies that I only use those annotations when retreiving data, not when pushing it which is simply avoiding the issue I'm trying to address. – coltonfranco Jul 24 '19 at 00:28
0

I did not tried this, but setting dynamicUpdate = true should do the work. (Maybe its only clean option if you don't know what properties will be changed, because frontend is sending only changed properties). Please follow this tutorial and let me know if it works correctly: Hibernate – dynamic-update attribute example

Stano
  • 86
  • 4
0

I ended up modifying my existing solution and added exclusions for the problematic annotations using reflection.

This solution basically just pulls the existing model from the session and copies all the non-null properties across. Then the @DynamicUpdate property mentioned above ensures that only the modified properties are updated.

protected String[] getNullPropertyNames (T source) {
        final BeanWrapper src = new BeanWrapperImpl(source);
        java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();
        Set<String> emptyNames = new HashSet<String>();
        boolean skipProperty;
        for(java.beans.PropertyDescriptor pd : pds) {
            skipProperty=false;
            if(pd.getReadMethod()!=null) {
                for (Annotation a : pd.getReadMethod().getAnnotations()) {
                    if (a.toString().equals("@javax.persistence.Transient()")) {/*Skip transient annotated properties*/
                        skipProperty = true;
                    }
                }
            }else{
                skipProperty=true;
            }
            if(!skipProperty){
                Object srcValue = src.getPropertyValue(pd.getName());
                if (srcValue == null) emptyNames.add(pd.getName());
            }
        }
        String[] result = new String[emptyNames.size()];
        return emptyNames.toArray(result);
    }

    // then use Spring BeanUtils to copy and ignore null
    public void copyUpdatedProperties(T newEntity, T existingEntity) {
        BeanUtils.copyProperties(newEntity, existingEntity, getNullPropertyNames(newEntity));
    }

This solves all my immediate concerns, however I suspect the annoation @Inheritance(strategy=InheritanceType.JOINED) may cause issues later.

coltonfranco
  • 147
  • 2
  • 15