Browse Source

Include transient properties in persistent entity metamodel.

We also ignore transient properties used in constructors. Regular transient properties default to the Java default values (null for object types, 0 for numeric primitives and so on).
Using transient properties allows leveraging Kotlin's defaulting mechanism to infer default values.
Record components can also be annotated with the Transient annotation to allow record construction. While this can be useful, we recommend using the Value annotation to use SpEL expressions to determine a useful value.

Closes: #2985
pull/3471/head
Mark Paluch 2 years ago committed by Christoph Strobl
parent
commit
a3e8b2711f
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 12
      src/main/antora/modules/ROOT/pages/object-mapping.adoc
  2. 2
      src/main/java/org/springframework/data/annotation/PersistenceCreator.java
  3. 13
      src/main/java/org/springframework/data/annotation/Transient.java
  4. 54
      src/main/java/org/springframework/data/mapping/PersistentEntity.java
  5. 4
      src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
  6. 30
      src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java
  7. 13
      src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java
  8. 33
      src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java
  9. 94
      src/test/java/org/springframework/data/mapping/model/EntityInstantiatorIntegrationTests.java
  10. 5
      src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt

12
src/main/antora/modules/ROOT/pages/object-mapping.adoc

@ -154,7 +154,7 @@ By default, Spring Data attempts to use generated property accessors and falls b @@ -154,7 +154,7 @@ By default, Spring Data attempts to use generated property accessors and falls b
Let's have a look at the following entity:
.A sample entity
[source, java]
[source,java]
----
class Person {
@ -165,14 +165,15 @@ class Person { @@ -165,14 +165,15 @@ class Person {
private String comment; <4>
private @AccessType(Type.PROPERTY) String remarks; <5>
private @Transient String summary; <6>
static Person of(String firstname, String lastname, LocalDate birthday) { <6>
static Person of(String firstname, String lastname, LocalDate birthday) { <7>
return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { <6>
Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { <7>
this.id = id;
this.firstname = firstname;
@ -201,7 +202,10 @@ With the design shown, the database value will trump the defaulting as Spring Da @@ -201,7 +202,10 @@ With the design shown, the database value will trump the defaulting as Spring Da
Even if the intent is that the calculation should be preferred, it's important that this constructor also takes `age` as parameter (to potentially ignore it) as otherwise the property population step will attempt to set the age field and fail due to it being immutable and no `with…` method being present.
<4> The `comment` property is mutable and is populated by setting its field directly.
<5> The `remarks` property is mutable and is populated by invoking the setter method.
<6> The class exposes a factory method and a constructor for object creation.
<6> The `summary` property transient and will not be persisted as it is annotated with `@Transient`.
Spring Data doesn't use Java's `transient` keyword to exclude properties from being persisted as `transient` is part of the Java Serialization Framework.
Note that this property can be used with a persistence constructor, but its value will default to `null` (or the respective primitive initial value if the property type was a primitive one).
<7> The class exposes a factory method and a constructor for object creation.
The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through `@PersistenceCreator`.
Instead, defaulting of properties is handled within the factory method.
If you want Spring Data to use the factory method for object instantiation, annotate it with `@PersistenceCreator`.

2
src/main/java/org/springframework/data/annotation/PersistenceCreator.java

@ -22,6 +22,8 @@ import java.lang.annotation.Target; @@ -22,6 +22,8 @@ import java.lang.annotation.Target;
/**
* Marker annotation to declare a constructor or factory method annotation as factory/preferred constructor annotation.
* Properties used by the constructor (or factory method) must refer to persistent properties or be annotated with
* {@link org.springframework.beans.factory.annotation.Value @Value()} to obtain a value for object creation.
*
* @author Mark Paluch
* @author Oliver Drotbohm

13
src/main/java/org/springframework/data/annotation/Transient.java

@ -22,13 +22,20 @@ import java.lang.annotation.RetentionPolicy; @@ -22,13 +22,20 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a field to be transient for the mapping framework. Thus the property will not be persisted and not further
* inspected by the mapping framework.
* Marks a field to be transient for the mapping framework. Thus, the property will not be persisted.
* <p>
* Excluding properties from the persistence mechanism is separate from Java's {@code transient} keyword that serves the
* purpose of excluding properties from being serialized through Java Serialization.
* <p>
* Transient properties can be used in {@link PersistenceCreator constructor creation/factory methods}, however they
* will use Java default values. We highly recommend using {@link org.springframework.beans.factory.annotation.Value
* SpEL expressions through @Value()} to provide a meaningful value.
*
* @author Oliver Gierke
* @author Jon Brisbin
* @author Mark Paluch
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE, RECORD_COMPONENT })
public @interface Transient {
}

54
src/main/java/org/springframework/data/mapping/PersistentEntity.java

@ -49,8 +49,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -49,8 +49,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
*
* @return {@literal null} in case no suitable creation mechanism for automatic construction can be found. This
* usually indicates that the instantiation of the object of that persistent entity is done through either a
* customer {@link org.springframework.data.mapping.model.EntityInstantiator} or handled by custom
* conversion mechanisms entirely.
* customer {@link org.springframework.data.mapping.model.EntityInstantiator} or handled by custom conversion
* mechanisms entirely.
* @since 3.0
*/
@Nullable
@ -110,8 +110,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -110,8 +110,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
}
/**
* Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property
* is available on the entity.
* Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property is
* available on the entity.
*
* @return the version property of the {@link PersistentEntity}.
*/
@ -119,8 +119,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -119,8 +119,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
P getVersionProperty();
/**
* Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property
* is available on the entity.
* Returns the version property of the {@link PersistentEntity}. Can be {@literal null} in case no version property is
* available on the entity.
*
* @return the version property of the {@link PersistentEntity}.
* @throws IllegalStateException if {@link PersistentEntity} does not define a {@literal version} property.
@ -140,7 +140,7 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -140,7 +140,7 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
/**
* Obtains a {@link PersistentProperty} instance by name.
*
* @param name The name of the property. Can be {@literal null}.
* @param name the name of the property. Can be {@literal null}.
* @return the {@link PersistentProperty} or {@literal null} if it doesn't exist.
*/
@Nullable
@ -186,6 +186,28 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -186,6 +186,28 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
*/
Iterable<P> getPersistentProperties(Class<? extends Annotation> annotationType);
/**
* Obtains a transient {@link PersistentProperty} instance by name. You can check with {@link #isTransient(String)}
* whether there is a transient property before calling this method.
*
* @param name the name of the property. Can be {@literal null}.
* @return the {@link PersistentProperty} or {@literal null} if it doesn't exist.
* @since 3.3
* @see #isTransient(String)
*/
@Nullable
P getTransientProperty(String name);
/**
* Returns whether the property is transient.
*
* @param property name of the property.
* @return {@code true} if the property is transient. Applies only for existing properties. {@code false} if the
* property does not exist or is not transient.
* @since 3.3
*/
boolean isTransient(String property);
/**
* Returns whether the {@link PersistentEntity} has an id property. If this call returns {@literal true},
* {@link #getIdProperty()} will return a non-{@literal null} value.
@ -210,8 +232,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -210,8 +232,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
Class<T> getType();
/**
* Returns the alias to be used when storing type information. Might be {@literal null} to indicate that there was
* no alias defined through the mapping metadata.
* Returns the alias to be used when storing type information. Might be {@literal null} to indicate that there was no
* alias defined through the mapping metadata.
*
* @return
*/
@ -241,8 +263,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -241,8 +263,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
void doWithProperties(SimplePropertyHandler handler);
/**
* Applies the given {@link AssociationHandler} to all {@link Association} contained in this
* {@link PersistentEntity}. The iteration order is undefined.
* Applies the given {@link AssociationHandler} to all {@link Association} contained in this {@link PersistentEntity}.
* The iteration order is undefined.
*
* @param handler must not be {@literal null}.
*/
@ -257,8 +279,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -257,8 +279,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
void doWithAssociations(SimpleAssociationHandler handler);
/**
* Applies the given {@link PropertyHandler} to both all {@link PersistentProperty}s as well as all inverse
* properties of all {@link Association}s. The iteration order is undefined.
* Applies the given {@link PropertyHandler} to both all {@link PersistentProperty}s as well as all inverse properties
* of all {@link Association}s. The iteration order is undefined.
*
* @param handler must not be {@literal null}.
* @since 2.5
@ -342,7 +364,7 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -342,7 +364,7 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
*
* @param bean must not be {@literal null}.
* @throws IllegalArgumentException in case the given bean is not an instance of the typ represented by the
* {@link PersistentEntity}.
* {@link PersistentEntity}.
* @return whether the given bean is considered a new instance.
*/
boolean isNew(Object bean);
@ -358,8 +380,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It @@ -358,8 +380,8 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> extends It
boolean isImmutable();
/**
* Returns whether the entity needs properties to be populated, i.e. if any property exists that's not initialized
* by the constructor.
* Returns whether the entity needs properties to be populated, i.e. if any property exists that's not initialized by
* the constructor.
*
* @return
* @since 2.1

4
src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java

@ -646,10 +646,6 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<? @@ -646,10 +646,6 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
P property = createPersistentProperty(input, entity, simpleTypeHolder);
if (property.isTransient()) {
return;
}
if (!input.isFieldBacked() && !property.usePropertyAccess()) {
return;
}

30
src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java

@ -72,11 +72,14 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> @@ -72,11 +72,14 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>>
private final @Nullable InstanceCreatorMetadata<P> creator;
private final TypeInformation<T> information;
private final List<P> properties;
private final List<P> transientProperties;
private final List<P> persistentPropertiesCache;
private final @Nullable Comparator<P> comparator;
private final Set<Association<P>> associations;
private final Map<String, P> propertyCache;
private final Map<String, P> transientPropertyCache;
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
private final MultiValueMap<Class<? extends Annotation>, P> propertyAnnotationCache;
@ -114,12 +117,14 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> @@ -114,12 +117,14 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>>
this.information = information;
this.properties = new ArrayList<>();
this.transientProperties = new ArrayList<>(0);
this.persistentPropertiesCache = new ArrayList<>();
this.comparator = comparator;
this.creator = InstanceCreatorMetadataDiscoverer.discover(this);
this.associations = comparator == null ? new HashSet<>() : new TreeSet<>(new AssociationComparator<>(comparator));
this.propertyCache = new HashMap<>(16, 1.0f);
this.transientPropertyCache = new HashMap<>(0, 1f);
this.annotationCache = new ConcurrentHashMap<>(16);
this.propertyAnnotationCache = CollectionUtils.toMultiValueMap(new ConcurrentHashMap<>(16));
this.propertyAccessorFactory = BeanWrapperPropertyAccessorFactory.INSTANCE;
@ -186,6 +191,18 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> @@ -186,6 +191,18 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>>
Assert.notNull(property, "Property must not be null");
if (property.isTransient()) {
if (transientProperties.contains(property)) {
return;
}
transientProperties.add(property);
transientPropertyCache.put(property.getName(), property);
return;
}
if (properties.contains(property)) {
return;
}
@ -279,6 +296,19 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> @@ -279,6 +296,19 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>>
return propertyCache.get(name);
}
@Override
public P getTransientProperty(String name) {
return transientPropertyCache.get(name);
}
@Override
public boolean isTransient(String property) {
P transientProperty = getTransientProperty(property);
return transientProperty != null && transientProperty.isTransient();
}
@Override
public Iterable<P> getPersistentProperties(Class<? extends Annotation> annotationType) {

13
src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java

@ -17,11 +17,13 @@ package org.springframework.data.mapping.model; @@ -17,11 +17,13 @@ package org.springframework.data.mapping.model;
import org.jspecify.annotations.Nullable;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mapping.InstanceCreatorMetadata;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.util.ReflectionUtils;
/**
* {@link ParameterValueProvider} based on a {@link PersistentEntity} to use a {@link PropertyValueProvider} to lookup
@ -29,6 +31,7 @@ import org.springframework.data.mapping.PersistentProperty; @@ -29,6 +31,7 @@ import org.springframework.data.mapping.PersistentProperty;
*
* @author Oliver Gierke
* @author Johannes Englmeier
* @author Mark Paluch
*/
public class PersistentEntityParameterValueProvider<P extends PersistentProperty<P>>
implements ParameterValueProvider<P> {
@ -45,17 +48,25 @@ public class PersistentEntityParameterValueProvider<P extends PersistentProperty @@ -45,17 +48,25 @@ public class PersistentEntityParameterValueProvider<P extends PersistentProperty
}
@Override
@Nullable
private static Object getTransientDefault(Class<?> parameterType) {
return parameterType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(parameterType) : null;
}
@Nullable
@SuppressWarnings("unchecked")
public <T> T getParameterValue(Parameter<T, P> parameter) {
InstanceCreatorMetadata<P> creator = entity.getInstanceCreatorMetadata();
String name = parameter.getName();
if (creator != null && creator.isParentParameter(parameter)) {
return (T) parent;
}
String name = parameter.getName();
if (parameter.getAnnotations().isPresent(Transient.class) || (name != null && entity.isTransient(name))) {
return (T) getTransientDefault(parameter.getRawType());
}
if (name == null) {
throw new MappingException(String.format("Parameter %s does not have a name", parameter));

33
src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java

@ -32,6 +32,8 @@ import java.util.stream.Stream; @@ -32,6 +32,8 @@ import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ -106,8 +108,7 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> { @@ -106,8 +108,7 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> {
@SuppressWarnings("unchecked")
void considersComparatorForPropertyOrder() {
var entity = createEntity(Person.class,
Comparator.comparing(PersistentProperty::getName));
var entity = createEntity(Person.class, Comparator.comparing(PersistentProperty::getName));
var lastName = (T) Mockito.mock(PersistentProperty.class);
when(lastName.getName()).thenReturn("lastName");
@ -199,8 +200,8 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> { @@ -199,8 +200,8 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> {
assertThat(accessor).isNotInstanceOf(BeanWrapper.class);
assertThat(accessor).isInstanceOfSatisfying(InstantiationAwarePropertyAccessor.class, it -> {
var delegateFunction = (Function<Object, PersistentPropertyAccessor<Object>>) ReflectionTestUtils
.getField(it, "delegateFunction");
var delegateFunction = (Function<Object, PersistentPropertyAccessor<Object>>) ReflectionTestUtils.getField(it,
"delegateFunction");
var delegate = delegateFunction.apply(value);
assertThat(delegate.getClass().getName()).contains("_Accessor_");
@ -360,6 +361,18 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> { @@ -360,6 +361,18 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> {
.forEach(it -> assertThat(createPopulatedPersistentEntity(it).requiresPropertyPopulation()).isFalse());
}
@ParameterizedTest // GH-1432
@ValueSource(classes = { WithTransient.class, RecordWithTransient.class, DataClassWithTransientProperty.class })
void includesTransientProperty(Class<?> classUnderTest) {
PersistentEntity<Object, ?> entity = createPopulatedPersistentEntity(classUnderTest);
assertThat(entity).extracting(PersistentProperty::getName).hasSize(1).containsOnly("firstname");
assertThat(entity.isTransient("firstname")).isFalse();
assertThat(entity.isTransient("lastname")).isTrue();
assertThat(entity.getTransientProperty("lastname").getName()).isEqualTo("lastname");
}
@Test // #2325
void doWithAllInvokesPropertyHandlerForBothAPropertiesAndAssociations() {
@ -476,6 +489,17 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> { @@ -476,6 +489,17 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> {
}
}
private static class WithTransient {
String firstname;
@Transient String lastname;
}
record RecordWithTransient(String firstname, @Transient String lastname) {
}
// #2325
static class WithAssociation {
@ -483,4 +507,5 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> { @@ -483,4 +507,5 @@ class BasicPersistentEntityUnitTests<T extends PersistentProperty<T>> {
String property;
@Reference WithAssociation association;
}
}

94
src/test/java/org/springframework/data/mapping/model/EntityInstantiatorIntegrationTests.java

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mapping.model;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.mapping.context.SamplePersistentProperty;
/**
* Integration tests for {@link EntityInstantiator}.
*
* @author Mark Paluch
*/
public class EntityInstantiatorIntegrationTests {
SampleMappingContext context = new SampleMappingContext();
EntityInstantiators instantiators = new EntityInstantiators();
@Test // GH-2942
void shouldDefaultTransientProperties() {
WithTransientProperty instance = createInstance(WithTransientProperty.class);
assertThat(instance.foo).isEqualTo(null);
assertThat(instance.bar).isEqualTo(0);
}
@Test // GH-2942
void shouldDefaultTransientRecordProperties() {
RecordWithTransientProperty instance = createInstance(RecordWithTransientProperty.class);
assertThat(instance.foo).isEqualTo(null);
assertThat(instance.bar).isEqualTo(0);
}
@Test // GH-2942
void shouldDefaultTransientKotlinProperty() {
DataClassWithTransientProperties instance = createInstance(DataClassWithTransientProperties.class);
// Kotlin defaulting
assertThat(instance.getFoo()).isEqualTo("foo");
// Our defaulting
assertThat(instance.getBar()).isEqualTo(0);
}
@SuppressWarnings("unchecked")
private <E> E createInstance(Class<E> entityType) {
var entity = context.getRequiredPersistentEntity(entityType);
var instantiator = instantiators.getInstantiatorFor(entity);
return (E) instantiator.createInstance(entity,
new PersistentEntityParameterValueProvider<>(entity, new PropertyValueProvider<SamplePersistentProperty>() {
@Override
public <T> T getPropertyValue(SamplePersistentProperty property) {
return null;
}
}, null));
}
static class WithTransientProperty {
@Transient String foo;
@Transient int bar;
public WithTransientProperty(String foo, int bar) {
}
}
record RecordWithTransientProperty(@Transient String foo, @Transient int bar) {
}
}

5
src/test/kotlin/org/springframework/data/mapping/model/DataClasses.kt

@ -18,6 +18,7 @@ package org.springframework.data.mapping.model @@ -18,6 +18,7 @@ package org.springframework.data.mapping.model
import org.jmolecules.ddd.types.AggregateRoot
import org.jmolecules.ddd.types.Identifier
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.Transient
import java.time.LocalDateTime
/**
@ -55,6 +56,10 @@ data class SingleSettableProperty constructor(val id: Double = Math.random()) { @@ -55,6 +56,10 @@ data class SingleSettableProperty constructor(val id: Double = Math.random()) {
val version: Int? = null
}
// note: Kotlin ships also a @Transient annotation to indicate JVM's transient keyword.
data class DataClassWithTransientProperty(val firstname: String, @Transient val lastname: String)
data class DataClassWithTransientProperties(@Transient val foo: String = "foo", @Transient val bar: Int)
data class WithCustomCopyMethod(
val id: String?,
val userId: String,

Loading…
Cancel
Save