DATAREDIS-425 - Preserve item order in list, update documentation.

We now preserve the item order when converting list elements. This does not mean that we place elements at the exact position retrieved from the store but rather maintain their order based on the index value.

Additionally applied some documentation polishing and cluster tests.

Original Pull Request: #156
This commit is contained in:
Christoph Strobl
2016-03-11 09:43:26 +01:00
parent 9eb309c495
commit 8f129b16f9
6 changed files with 627 additions and 357 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2016 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.
@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.core.convert;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.hamcrest.core.IsInstanceOf.*;
@@ -308,7 +309,22 @@ public class MappingRedisConverterUnitTests {
map.put("nicknames.[1]", "lews therin");
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
assertThat(converter.read(Person.class, rdo).nicknames, hasItems("dragon reborn", "lews therin"));
assertThat(converter.read(Person.class, rdo).nicknames, contains("dragon reborn", "lews therin"));
}
/**
* @see DATAREDIS-425
*/
@Test
public void readConvertsUnorderedListOfSimplePropertiesCorrectly() {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("nicknames.[9]", "car'a'carn");
map.put("nicknames.[10]", "lews therin");
map.put("nicknames.[1]", "dragon reborn");
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
assertThat(converter.read(Person.class, rdo).nicknames, contains("dragon reborn", "car'a'carn", "lews therin"));
}
/**
@@ -338,6 +354,7 @@ public class MappingRedisConverterUnitTests {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("coworkers.[0].firstname", "mat");
map.put("coworkers.[0].nicknames.[0]", "prince of the ravens");
map.put("coworkers.[0].nicknames.[1]", "gambler");
map.put("coworkers.[1].firstname", "perrin");
map.put("coworkers.[1].address.city", "two rivers");
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
@@ -348,6 +365,34 @@ public class MappingRedisConverterUnitTests {
assertThat(target.coworkers.get(0).firstname, is("mat"));
assertThat(target.coworkers.get(0).nicknames, notNullValue());
assertThat(target.coworkers.get(0).nicknames.get(0), is("prince of the ravens"));
assertThat(target.coworkers.get(0).nicknames.get(1), is("gambler"));
assertThat(target.coworkers.get(1).firstname, is("perrin"));
assertThat(target.coworkers.get(1).address.city, is("two rivers"));
}
/**
* @see DATAREDIS-425
*/
@Test
public void readUnorderedListOfComplexPropertyCorrectly() {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("coworkers.[10].firstname", "perrin");
map.put("coworkers.[10].address.city", "two rivers");
map.put("coworkers.[1].firstname", "mat");
map.put("coworkers.[1].nicknames.[1]", "gambler");
map.put("coworkers.[1].nicknames.[0]", "prince of the ravens");
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
Person target = converter.read(Person.class, rdo);
assertThat(target.coworkers, notNullValue());
assertThat(target.coworkers.get(0).firstname, is("mat"));
assertThat(target.coworkers.get(0).nicknames, notNullValue());
assertThat(target.coworkers.get(0).nicknames.get(0), is("prince of the ravens"));
assertThat(target.coworkers.get(0).nicknames.get(1), is("gambler"));
assertThat(target.coworkers.get(1).firstname, is("perrin"));
assertThat(target.coworkers.get(1).address.city, is("two rivers"));

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2016 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
*
* http://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.redis.repository;
import static org.springframework.data.redis.connection.ClusterTestVariables.*;
import java.util.Arrays;
import java.util.List;
import org.junit.ClassRule;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.test.util.RedisClusterRule;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class RedisRepositoryClusterIntegrationTests extends RedisRepositoryIntegrationTestBase {
static final List<String> CLUSTER_NODES = Arrays.asList(CLUSTER_NODE_1.asString(), CLUSTER_NODE_2.asString(),
CLUSTER_NODE_3.asString());
/**
* ONLY RUN WHEN CLUSTER AVAILABLE
*/
public static @ClassRule RedisClusterRule clusterRule = new RedisClusterRule();
@Configuration
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
keyspaceConfiguration = MyKeyspaceConfiguration.class,
includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") })
static class Config {
@Bean
RedisTemplate<?, ?> redisTemplate() {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(CLUSTER_NODES);
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(clusterConfig);
connectionFactory.afterPropertiesSet();
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(connectionFactory);
return template;
}
}
}

View File

@@ -0,0 +1,369 @@
/*
* Copyright 2016 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
*
* http://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.redis.repository;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.junit.Assert.*;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.hamcrest.core.IsNull;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.core.index.IndexConfiguration;
import org.springframework.data.redis.core.index.IndexDefinition;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
import org.springframework.data.repository.CrudRepository;
/**
* Base for testing Redis repository support in different configurations.
*
* @author Christoph Strobl
*/
public abstract class RedisRepositoryIntegrationTestBase {
@Autowired PersonRepository repo;
@Autowired KeyValueTemplate kvTemplate;
@Before
public void setUp() {
// flush keyspaces
kvTemplate.delete(Person.class);
kvTemplate.delete(City.class);
}
/**
* @see DATAREDIS-425
*/
@Test
public void simpleFindShouldReturnEntitiesCorrectly() {
Person rand = new Person();
rand.firstname = "rand";
rand.lastname = "al'thor";
Person egwene = new Person();
egwene.firstname = "egwene";
repo.save(Arrays.asList(rand, egwene));
assertThat(repo.count(), is(2L));
assertThat(repo.findOne(rand.id), is(rand));
assertThat(repo.findOne(egwene.id), is(egwene));
assertThat(repo.findByFirstname("rand").size(), is(1));
assertThat(repo.findByFirstname("rand"), hasItem(rand));
assertThat(repo.findByFirstname("egwene").size(), is(1));
assertThat(repo.findByFirstname("egwene"), hasItem(egwene));
assertThat(repo.findByLastname("al'thor"), hasItem(rand));
}
/**
* @see DATAREDIS-425
*/
@Test
public void simpleFindByMultipleProperties() {
Person egwene = new Person();
egwene.firstname = "egwene";
egwene.lastname = "al'vere";
Person marin = new Person();
marin.firstname = "marin";
marin.lastname = "al'vere";
repo.save(Arrays.asList(egwene, marin));
assertThat(repo.findByLastname("al'vere").size(), is(2));
assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").size(), is(1));
assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").get(0), is(egwene));
}
/**
* @see DATAREDIS-425
*/
@Test
public void findReturnsReferenceDataCorrectly() {
// Prepare referenced data entry
City tarValon = new City();
tarValon.id = "1";
tarValon.name = "tar valon";
kvTemplate.insert(tarValon);
// Prepare domain entity
Person moiraine = new Person();
moiraine.firstname = "moiraine";
moiraine.city = tarValon; // reference data
// save domain entity
repo.save(moiraine);
// find and assert current location set correctly
Person loaded = repo.findOne(moiraine.getId());
assertThat(loaded.city, is(tarValon));
// remove reference location data
kvTemplate.delete("1", City.class);
// find and assert the location is gone
Person reLoaded = repo.findOne(moiraine.getId());
assertThat(reLoaded.city, IsNull.nullValue());
}
/**
* @see DATAREDIS-425
*/
@Test
public void findReturnsPageCorrectly() {
Person eddard = new Person("eddard", "stark");
Person robb = new Person("robb", "stark");
Person sansa = new Person("sansa", "stark");
Person arya = new Person("arya", "stark");
Person bran = new Person("bran", "stark");
Person rickon = new Person("rickon", "stark");
repo.save(Arrays.asList(eddard, robb, sansa, arya, bran, rickon));
Page<Person> page1 = repo.findPersonByLastname("stark", new PageRequest(0, 5));
assertThat(page1.getNumberOfElements(), is(5));
assertThat(page1.getTotalElements(), is(6L));
Page<Person> page2 = repo.findPersonByLastname("stark", page1.nextPageable());
assertThat(page2.getNumberOfElements(), is(1));
assertThat(page2.getTotalElements(), is(6L));
}
/**
* @see DATAREDIS-425
*/
@Test
public void findUsingOrReturnsResultCorrectly() {
Person eddard = new Person("eddard", "stark");
Person robb = new Person("robb", "stark");
Person jon = new Person("jon", "snow");
repo.save(Arrays.asList(eddard, robb, jon));
List<Person> eddardAndJon = repo.findByFirstnameOrLastname("eddard", "snow");
assertThat(eddardAndJon, hasSize(2));
assertThat(eddardAndJon, containsInAnyOrder(eddard, jon));
}
public static interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByFirstname(String firstname);
List<Person> findByLastname(String lastname);
Page<Person> findPersonByLastname(String lastname, Pageable page);
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
List<Person> findByFirstnameOrLastname(String firstname, String lastname);
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.<IndexDefinition> singleton(new SimpleIndexDefinition("persons", "lastname"));
}
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
return Collections.singleton(new KeyspaceSettings(City.class, "cities"));
}
}
@RedisHash("persons")
@SuppressWarnings("serial")
public static class Person implements Serializable {
@Id String id;
@Indexed String firstname;
String lastname;
@Reference City city;
public Person() {}
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getLastname() {
return lastname;
}
@Override
public String toString() {
return "Person [id=" + id + ", firstname=" + firstname + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((firstname == null) ? 0 : firstname.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
if (firstname == null) {
if (other.firstname != null) {
return false;
}
} else if (!firstname.equals(other.firstname)) {
return false;
}
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
}
public static class City {
@Id String id;
String name;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof City)) {
return false;
}
City other = (City) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2016 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.
@@ -15,42 +15,14 @@
*/
package org.springframework.data.redis.repository;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.junit.Assert.*;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.hamcrest.core.IsNull;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.core.index.IndexConfiguration;
import org.springframework.data.redis.core.index.IndexDefinition;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.repository.CrudRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -59,7 +31,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class RedisRepositoryIntegrationTests {
public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationTestBase {
@Configuration
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
@@ -78,322 +50,5 @@ public class RedisRepositoryIntegrationTests {
return template;
}
}
@Autowired PersonRepository repo;
@Autowired KeyValueTemplate kvTemplate;
@Before
public void setUp() {
// flush keyspaces
kvTemplate.delete(Person.class);
kvTemplate.delete(City.class);
}
/**
* @see DATAREDIS-425
*/
@Test
public void simpleFindShouldReturnEntitiesCorrectly() {
Person rand = new Person();
rand.firstname = "rand";
rand.lastname = "al'thor";
Person egwene = new Person();
egwene.firstname = "egwene";
repo.save(Arrays.asList(rand, egwene));
assertThat(repo.count(), is(2L));
assertThat(repo.findOne(rand.id), is(rand));
assertThat(repo.findOne(egwene.id), is(egwene));
assertThat(repo.findByFirstname("rand").size(), is(1));
assertThat(repo.findByFirstname("rand"), hasItem(rand));
assertThat(repo.findByFirstname("egwene").size(), is(1));
assertThat(repo.findByFirstname("egwene"), hasItem(egwene));
assertThat(repo.findByLastname("al'thor"), hasItem(rand));
}
/**
* @see DATAREDIS-425
*/
@Test
public void simpleFindByMultipleProperties() {
Person egwene = new Person();
egwene.firstname = "egwene";
egwene.lastname = "al'vere";
Person marin = new Person();
marin.firstname = "marin";
marin.lastname = "al'vere";
repo.save(Arrays.asList(egwene, marin));
assertThat(repo.findByLastname("al'vere").size(), is(2));
assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").size(), is(1));
assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").get(0), is(egwene));
}
/**
* @see DATAREDIS-425
*/
@Test
public void findReturnsReferenceDataCorrectly() {
// Prepare referenced data entry
City tarValon = new City();
tarValon.id = "1";
tarValon.name = "tar valon";
kvTemplate.insert(tarValon);
// Prepare domain entity
Person moiraine = new Person();
moiraine.firstname = "moiraine";
moiraine.city = tarValon; // reference data
// save domain entity
repo.save(moiraine);
// find and assert current location set correctly
Person loaded = repo.findOne(moiraine.getId());
assertThat(loaded.city, is(tarValon));
// remove reference location data
kvTemplate.delete("1", City.class);
// find and assert the location is gone
Person reLoaded = repo.findOne(moiraine.getId());
assertThat(reLoaded.city, IsNull.nullValue());
}
/**
* @see DATAREDIS-425
*/
@Test
public void findReturnsPageCorrectly() {
Person eddard = new Person("eddard", "stark");
Person robb = new Person("robb", "stark");
Person sansa = new Person("sansa", "stark");
Person arya = new Person("arya", "stark");
Person bran = new Person("bran", "stark");
Person rickon = new Person("rickon", "stark");
repo.save(Arrays.asList(eddard, robb, sansa, arya, bran, rickon));
Page<Person> page1 = repo.findPersonByLastname("stark", new PageRequest(0, 5));
assertThat(page1.getNumberOfElements(), is(5));
assertThat(page1.getTotalElements(), is(6L));
Page<Person> page2 = repo.findPersonByLastname("stark", page1.nextPageable());
assertThat(page2.getNumberOfElements(), is(1));
assertThat(page2.getTotalElements(), is(6L));
}
/**
* @see DATAREDIS-425
*/
@Test
public void findUsingOrReturnsResultCorrectly() {
Person eddard = new Person("eddard", "stark");
Person robb = new Person("robb", "stark");
Person jon = new Person("jon", "snow");
repo.save(Arrays.asList(eddard, robb, jon));
List<Person> eddardAndJon = repo.findByFirstnameOrLastname("eddard", "snow");
assertThat(eddardAndJon, hasSize(2));
assertThat(eddardAndJon, containsInAnyOrder(eddard, jon));
}
public static interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByFirstname(String firstname);
List<Person> findByLastname(String lastname);
Page<Person> findPersonByLastname(String lastname, Pageable page);
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
List<Person> findByFirstnameOrLastname(String firstname, String lastname);
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.<IndexDefinition> singleton(new SimpleIndexDefinition("persons", "lastname"));
}
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
return Collections.singleton(new KeyspaceSettings(City.class, "cities"));
}
}
@RedisHash("persons")
@SuppressWarnings("serial")
public static class Person implements Serializable {
@Id String id;
@Indexed String firstname;
String lastname;
@Reference City city;
public Person() {}
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getLastname() {
return lastname;
}
@Override
public String toString() {
return "Person [id=" + id + ", firstname=" + firstname + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((firstname == null) ? 0 : firstname.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
if (firstname == null) {
if (other.firstname != null) {
return false;
}
} else if (!firstname.equals(other.firstname)) {
return false;
}
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
}
public static class City {
@Id String id;
String name;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof City)) {
return false;
}
City other = (City) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
}
}