5
votes

i have model of class City like below:

@Entity
public class City {
    @Id
    Long id;
    String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

I have another model class Person given below:

@Entity
public class Person {
    @Id
    Long id;
    String name;
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    Key<City> city;
}

After that I generate the endpoints for both the class using android studio and deploy it.

here is code for the generated endpoints:

PersonEndpoint

@Api(
        name = "personApi",
        version = "v1",
        resource = "person",
        namespace = @ApiNamespace(
                ownerDomain = "backend.faceattendence.morpho.com",
                ownerName = "backend.faceattendence.morpho.com",
                packagePath = ""
        )
)
public class PersonEndpoint {

    private static final Logger logger = Logger.getLogger(PersonEndpoint.class.getName());

    private static final int DEFAULT_LIST_LIMIT = 20;

    static {
        // Typically you would register this inside an OfyServive wrapper. See: https://code.google.com/p/objectify-appengine/wiki/BestPractices
        ObjectifyService.register(Person.class);
    }

    /**
     * Returns the {@link Person} with the corresponding ID.
     *
     * @param id the ID of the entity to be retrieved
     * @return the entity with the corresponding ID
     * @throws NotFoundException if there is no {@code Person} with the provided ID.
     */
    @ApiMethod(
            name = "get",
            path = "person/{id}",
            httpMethod = ApiMethod.HttpMethod.GET)
    public Person get(@Named("id") Long id) throws NotFoundException {
        logger.info("Getting Person with ID: " + id);
        Person person = ofy().load().type(Person.class).id(id).now();
        if (person == null) {
            throw new NotFoundException("Could not find Person with ID: " + id);
        }
        return person;
    }

    /**
     * Inserts a new {@code Person}.
     */
    @ApiMethod(
            name = "insert",
            path = "person",
            httpMethod = ApiMethod.HttpMethod.POST)
    public Person insert(Person person) {
        // Typically in a RESTful API a POST does not have a known ID (assuming the ID is used in the resource path).
        // You should validate that person.id has not been set. If the ID type is not supported by the
        // Objectify ID generator, e.g. long or String, then you should generate the unique ID yourself prior to saving.
        //
        // If your client provides the ID then you should probably use PUT instead.
        ofy().save().entity(person).now();
        logger.info("Created Person.");

        return ofy().load().entity(person).now();
    }

    /**
     * Updates an existing {@code Person}.
     *
     * @param id     the ID of the entity to be updated
     * @param person the desired state of the entity
     * @return the updated version of the entity
     * @throws NotFoundException if the {@code id} does not correspond to an existing
     *                           {@code Person}
     */
    @ApiMethod(
            name = "update",
            path = "person/{id}",
            httpMethod = ApiMethod.HttpMethod.PUT)
    public Person update(@Named("id") Long id, Person person) throws NotFoundException {
        // TODO: You should validate your ID parameter against your resource's ID here.
        checkExists(id);
        ofy().save().entity(person).now();
        logger.info("Updated Person: " + person);
        return ofy().load().entity(person).now();
    }

    /**
     * Deletes the specified {@code Person}.
     *
     * @param id the ID of the entity to delete
     * @throws NotFoundException if the {@code id} does not correspond to an existing
     *                           {@code Person}
     */
    @ApiMethod(
            name = "remove",
            path = "person/{id}",
            httpMethod = ApiMethod.HttpMethod.DELETE)
    public void remove(@Named("id") Long id) throws NotFoundException {
        checkExists(id);
        ofy().delete().type(Person.class).id(id).now();
        logger.info("Deleted Person with ID: " + id);
    }

    /**
     * List all entities.
     *
     * @param cursor used for pagination to determine which page to return
     * @param limit  the maximum number of entries to return
     * @return a response that encapsulates the result list and the next page token/cursor
     */
    @ApiMethod(
            name = "list",
            path = "person",
            httpMethod = ApiMethod.HttpMethod.GET)
    public CollectionResponse<Person> list(@Nullable @Named("cursor") String cursor, @Nullable @Named("limit") Integer limit) {
        limit = limit == null ? DEFAULT_LIST_LIMIT : limit;
        Query<Person> query = ofy().load().type(Person.class).limit(limit);
        if (cursor != null) {
            query = query.startAt(Cursor.fromWebSafeString(cursor));
        }
        QueryResultIterator<Person> queryIterator = query.iterator();
        List<Person> personList = new ArrayList<Person>(limit);
        while (queryIterator.hasNext()) {
            personList.add(queryIterator.next());
        }
        return CollectionResponse.<Person>builder().setItems(personList).setNextPageToken(queryIterator.getCursor().toWebSafeString()).build();
    }

    private void checkExists(Long id) throws NotFoundException {
        try {
            ofy().load().type(Person.class).id(id).safe();
        } catch (com.googlecode.objectify.NotFoundException e) {
            throw new NotFoundException("Could not find Person with ID: " + id);
        }
    }
}

CityEndpoint

@Api(
        name = "cityApi",
        version = "v1",
        resource = "city",
        namespace = @ApiNamespace(
                ownerDomain = "backend.faceattendence.morpho.com",
                ownerName = "backend.faceattendence.morpho.com",
                packagePath = ""
        )
)
public class CityEndpoint {

    private static final Logger logger = Logger.getLogger(CityEndpoint.class.getName());

    private static final int DEFAULT_LIST_LIMIT = 20;

    static {
        // Typically you would register this inside an OfyServive wrapper. See: https://code.google.com/p/objectify-appengine/wiki/BestPractices
        ObjectifyService.register(City.class);
    }

    /**
     * Returns the {@link City} with the corresponding ID.
     *
     * @param id the ID of the entity to be retrieved
     * @return the entity with the corresponding ID
     * @throws NotFoundException if there is no {@code City} with the provided ID.
     */
    @ApiMethod(
            name = "get",
            path = "city/{id}",
            httpMethod = ApiMethod.HttpMethod.GET)
    public City get(@Named("id") Long id) throws NotFoundException {
        logger.info("Getting City with ID: " + id);
        City city = ofy().load().type(City.class).id(id).now();
        if (city == null) {
            throw new NotFoundException("Could not find City with ID: " + id);
        }
        return city;
    }

    /**
     * Inserts a new {@code City}.
     */
    @ApiMethod(
            name = "insert",
            path = "city",
            httpMethod = ApiMethod.HttpMethod.POST)
    public City insert(City city) {
        // Typically in a RESTful API a POST does not have a known ID (assuming the ID is used in the resource path).
        // You should validate that city.id has not been set. If the ID type is not supported by the
        // Objectify ID generator, e.g. long or String, then you should generate the unique ID yourself prior to saving.
        //
        // If your client provides the ID then you should probably use PUT instead.
        ofy().save().entity(city).now();
        logger.info("Created City.");

        return ofy().load().entity(city).now();
    }

    /**
     * Updates an existing {@code City}.
     *
     * @param id   the ID of the entity to be updated
     * @param city the desired state of the entity
     * @return the updated version of the entity
     * @throws NotFoundException if the {@code id} does not correspond to an existing
     *                           {@code City}
     */
    @ApiMethod(
            name = "update",
            path = "city/{id}",
            httpMethod = ApiMethod.HttpMethod.PUT)
    public City update(@Named("id") Long id, City city) throws NotFoundException {
        // TODO: You should validate your ID parameter against your resource's ID here.
        checkExists(id);
        ofy().save().entity(city).now();
        logger.info("Updated City: " + city);
        return ofy().load().entity(city).now();
    }

    /**
     * Deletes the specified {@code City}.
     *
     * @param id the ID of the entity to delete
     * @throws NotFoundException if the {@code id} does not correspond to an existing
     *                           {@code City}
     */
    @ApiMethod(
            name = "remove",
            path = "city/{id}",
            httpMethod = ApiMethod.HttpMethod.DELETE)
    public void remove(@Named("id") Long id) throws NotFoundException {
        checkExists(id);
        ofy().delete().type(City.class).id(id).now();
        logger.info("Deleted City with ID: " + id);
    }

    /**
     * List all entities.
     *
     * @param cursor used for pagination to determine which page to return
     * @param limit  the maximum number of entries to return
     * @return a response that encapsulates the result list and the next page token/cursor
     */
    @ApiMethod(
            name = "list",
            path = "city",
            httpMethod = ApiMethod.HttpMethod.GET)
    public CollectionResponse<City> list(@Nullable @Named("cursor") String cursor, @Nullable @Named("limit") Integer limit) {
        limit = limit == null ? DEFAULT_LIST_LIMIT : limit;
        Query<City> query = ofy().load().type(City.class).limit(limit);
        if (cursor != null) {
            query = query.startAt(Cursor.fromWebSafeString(cursor));
        }
        QueryResultIterator<City> queryIterator = query.iterator();
        List<City> cityList = new ArrayList<City>(limit);
        while (queryIterator.hasNext()) {
            cityList.add(queryIterator.next());
        }
        return CollectionResponse.<City>builder().setItems(cityList).setNextPageToken(queryIterator.getCursor().toWebSafeString()).build();
    }

    private void checkExists(Long id) throws NotFoundException {
        try {
            ofy().load().type(City.class).id(id).safe();
        } catch (com.googlecode.objectify.NotFoundException e) {
            throw new NotFoundException("Could not find City with ID: " + id);
        }
    }
}

I want to make relationship between City and Person such that many person can belongs to a city. Questions:

  1. Is this the correct modelling of the class for this kind of relationship? if not please tell me the correct model of one-to-one and one-to-many relationship

  2. how to insert record in the datastore for this kind of relationship through java code(endpoints) and through API explorer?

  3. Is there any need of using @Parent annotation or @Index annotation?

  4. After building this relationship, if I delete a city then all the person belonging to that city must be deleted automatically. Is this modelling able to achieve that? please tell me the code for to do this also. if not then how can I achieve that using relationship?

2

2 Answers

3
votes

I can't answer any questions about Google Endpoints, but the basic idea of modeling Person with a Key field pointing at City is probably correct - assuming there are lots and lots of people to a city. You'll want to @Index the key field so that you can query for people in a city. Be aware that this query will be eventually consistent, so if you are adding/removing lots of People in a City, you'll want to put a delay between when you stop adding People and when you execute the delete.

You could model this so that City is the @Parent of Person. This would eliminate the eventual consistency, but it would mean you can never move a Person to a new City. It would also mean that no City or Person in that city can change more than once per second due to the transaction throughput limit of a single entity group. Assuming you're actually talking about Persons and Cities, you probably don't want this. But it depends on your dataset.

1
votes

First of all read the discussion happened with objectify groups on below link: how to insert record in objectify entity having one-to-one or one-to-many relationship in android So its a better way to design your APIs on the basis of keys(i.e. web safe key) not on the basis on ID because ID is not unique in the datastore. so I change my Person model class to:

@Entity
public class Person {
    @Id
    Long id;
    String name;
    @Parent
    @Index
    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    Key<City> city;
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getWebsafeKey() {
        return Key.create(city.getKey(), Person.class, id).getString();
    }

}

the getWebsafeKey() will return me the websafe key for the person class having its parent information. I want this websafe key so that i can directly retrieve stored entity by querying on keys.

for City model class, since it doesn't have parent getWebsafeKey() method will look like:

public String getWebsafeKey() {
        return Key.create( City.class, id).getString();
    }

Answer of question 2: inserting city will be same in the question, means there is no change in the insert() method of city endpoint . inserting person inside a city will look like below:

@ApiMethod(
       name = "insert",
       path = "city/{city_web_safe_key}/person",
       httpMethod = ApiMethod.HttpMethod.POST)
       public Person insert(@Named("city_web_safe_key") String cityWebSafeKey,Person person) {
        Key<City> cityKey = Key.create(cityWebSafeKey);
                person.city = Ref.create(cityKey);
                    ofy().save().entity(person).now();
                    logger.info("Created Person.");
                    return ofy().load().entity(person).now();
                }

Similarly you have to change all other @ApiMethod which have "id" parameter also because when you automatically generate the endpoints from you model class all the @ApiMethod will be on the basis of "id" as shown in the question. So you have to replace "id" parameter with "web safe key" . same thing will apply for City endpoints method also. For better understanding about parameters you should [click here][2]

for example get method of person endpoints will look like:

@ApiMethod(
            name = "get",
            path = "person/{person_websafe_key}",
            httpMethod = ApiMethod.HttpMethod.GET)
    public Site get(@Named("person_websafe_key") String personWSKey) throws NotFoundException {
        logger.info("Getting person with WSkey: " + personWSKey);
        Person person = (Person) ofy().load().key(Key.create(personWSKey)).now();
        if (person == null) {
            throw new NotFoundException("Could not find person with WSKey: " + personWSKey);
        }
        return person;
    }

answer of question 4: Objectify doesn't force developer to such kind of deletion. its all about what kind of requirements you have. here is the remove method of City which will also delete the person present in that city.

@ApiMethod(
            name = "remove",
            path = "city/{city_web_safe_key}",
            httpMethod = ApiMethod.HttpMethod.DELETE)
    public void remove(@Named("city_web_safe_key") String cityWSKey) throws NotFoundException {
        Key<City> cityKey = Key.create(cityWSKey);
        checkExists(cityKey);
        //ofy().delete().key(cityKey).now();
        ofy().delete().keys(ofy().load().ancestor(cityKey).keys().list());

    }

answer of question 1 and 3 already covered in above answer for detail knowledge see documentation