/* * Copyright (C) 2012-2015 DataStax Inc. * * 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 com.datastax.driver.mapping; import com.datastax.driver.core.*; import com.datastax.driver.core.utils.CassandraVersion; import com.datastax.driver.core.utils.MoreObjects; import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.mapping.annotations.*; import com.google.common.util.concurrent.ListenableFuture; import org.testng.annotations.Test; import java.net.InetAddress; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; /** * Basic tests for the mapping module. */ @SuppressWarnings({"unused", "WeakerAccess"}) public class MapperTest extends CCMTestsSupport { @Override public void onTestContextInitialized() { // We'll allow to generate those create statement from the annotated entities later, but it's currently // a TODO execute("CREATE TABLE users (user_id uuid PRIMARY KEY, name text, email text, year int, gender text)", "CREATE TABLE posts (user_id uuid, post_id timeuuid, title text, content text, device inet, tags set<text>, PRIMARY KEY(user_id, post_id))"); } /* * Annotates a simple entity. Not a whole lot to see here, all fields are * mapped by default (but there is a @Transcient to have a field non mapped) * to a C* column that have the same name than the field (but you can use @Column * to specify the actual column name in C* if it's different). * * Do note that we support enums (which are mapped to strings by default * but you can map them to their ordinal too with some @Enumerated annotation) * * And the next step will be to support UDT (which should be relatively simple). */ @Table(name = "users", readConsistency = "QUORUM", writeConsistency = "QUORUM") public static class User { // Dummy constant to test that static fields are properly ignored public static final int FOO = 1; @PartitionKey @Column(name = "user_id") private UUID userId; private String name; private String email; @Column // not strictly required, but we want to check that the annotation works without a name private int year; public User() { } public User(String name, String email) { this.userId = UUIDs.random(); this.name = name; this.email = email; } public UUID getUserId() { return userId; } public void setUserId(UUID userId) { this.userId = userId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public int getYear() { return year; } public void setYear(int year) { this.year = year; } @Override public boolean equals(Object other) { if (other == null || other.getClass() != this.getClass()) return false; User that = (User) other; return MoreObjects.equal(userId, that.userId) && MoreObjects.equal(name, that.name) && MoreObjects.equal(email, that.email) && MoreObjects.equal(year, that.year); } @Override public int hashCode() { return MoreObjects.hashCode(userId, name, email, year); } } /* * Another annotated entity, but that correspond to a table that has a * clustering column. Note that if there is more than one clustering column, * the order must be specified (@ClusteringColumn(0), @ClusteringColumn(1), ...). * The same stands for the @PartitionKey. */ @SuppressWarnings("unused") @Table(name = "posts") public static class Post { private String title; private String content; private InetAddress device; @ClusteringColumn @Column(name = "post_id") private UUID postId; @PartitionKey @Column(name = "user_id") private UUID userId; private Set<String> tags; public Post() { } public Post(User user, String title) { this.userId = user.getUserId(); this.postId = UUIDs.timeBased(); this.title = title; } public UUID getUserId() { return userId; } public void setUserId(UUID userId) { this.userId = userId; } public UUID getPostId() { return postId; } public void setPostId(UUID postId) { this.postId = postId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public InetAddress getDevice() { return device; } public void setDevice(InetAddress device) { this.device = device; } public Set<String> getTags() { return tags; } public void setTags(Set<String> tags) { this.tags = tags; } @Override public boolean equals(Object other) { if (other == null || other.getClass() != this.getClass()) return false; Post that = (Post) other; return MoreObjects.equal(userId, that.userId) && MoreObjects.equal(postId, that.postId) && MoreObjects.equal(title, that.title) && MoreObjects.equal(content, that.content) && MoreObjects.equal(device, that.device) && MoreObjects.equal(tags, that.tags); } @Override public int hashCode() { return MoreObjects.hashCode(userId, postId, title, content, device, tags); } } /* * We actually have 2 concepts in the mapping module. The first is the * mapping of entities like User and Post above. From such annotated entity * you can get a Mapper object (see below), which allow to map the Row of * ResultSet to proper object, and that provide a few simple method like * save, delete and a simple get. * * But to remove a bit of boilerplate when you need more complex queries, we * also have the concept of Accesor, which is just a way to associate some * java method calls to queries. Note that you don't have to use those Accessor * if you don't want (and in fact, you can use the Accessor concept even if * you don't map any entity). */ @Accessor public interface PostAccessor { // Note that for implementation reasons, this *needs* to be an interface. // The @Param below is because you can't get the name of parameters of methods // by reflection (you can only have their types), so you have to annotate them // if you want to give them proper names in the query. That being said, if you // don't have @Param annotation like in the 2 other method, we default to some // harcoded arg0, arg1, .... A big annoying, and apparently Java 8 will fix that // somehow, but well, not a huge deal. @Query("SELECT * FROM posts WHERE user_id=:userId AND post_id=:postId") Post getOne(@Param("userId") UUID userId, @Param("postId") UUID postId); // Note that the following method will be asynchronous (it will use executeAsync // underneath) because it's return type is a ListenableFuture. Similarly, we know // that we need to map the result to the Post entity thanks to the return type. @Query("SELECT * FROM posts WHERE user_id=?") @QueryParameters(consistency = "QUORUM") ListenableFuture<Result<Post>> getAllAsync(UUID userId); // The method above actually query stuff, but if a method is declared to return // a Statement, it will not execute anything, but just return you the BoundStatement // ready for execution. That way, you can batch stuff for instance (see usage below). @Query("UPDATE posts SET content=? WHERE user_id=? AND post_id=?") Statement updateContentQuery(String newContent, UUID userId, UUID postId); @Query("SELECT * FROM posts") Result<Post> getAll(); @Query("SELECT * FROM posts") @QueryParameters(idempotent = true) Statement getAllAsStatementIdempotent(); @Query("SELECT * FROM posts") @QueryParameters(idempotent = false) Statement getAllAsStatementNonIdempotent(); @Query("SELECT * FROM posts") Statement getAllAsStatement(); } @Test(groups = "short") public void testStaticEntity() throws Exception { // Very simple mapping a User, saving and getting it. Note that here we // don't use the Accessor stuff since the queries we use are directly // supported by the Mapper object. Mapper<User> m = new MappingManager(session()).mapper(User.class); User u1 = new User("Paul", "paul@yahoo.com"); u1.setYear(2014); m.save(u1); // Do note that m.get() takes the primary key of what we want to fetch // in argument, it doesn't not take a User object because we don't proxy // objects `a la' SpringData/Hibernate. The reason for not doing that // is that we don't want to encourage read-before-write. assertEquals(m.get(u1.getUserId()), u1); } @Test(groups = "short") @CassandraVersion("2.0.0") public void testDynamicEntity() throws Exception { MappingManager manager = new MappingManager(session()); Mapper<Post> m = manager.mapper(Post.class); User u1 = new User("Paul", "paul@gmail.com"); Post p1 = new Post(u1, "Something about mapping"); Post p2 = new Post(u1, "Something else"); Post p3 = new Post(u1, "Something more"); p1.setDevice(InetAddress.getLocalHost()); p2.setTags(new HashSet<String>(Arrays.asList("important", "keeper"))); m.save(p1); m.save(p2); m.save(p3); // Creates the accessor proxy defined above PostAccessor postAccessor = manager.createAccessor(PostAccessor.class); // Note that getOne is really the same than m.get(), it's just there // for demonstration sake. Post p = postAccessor.getOne(p1.getUserId(), p1.getPostId()); assertEquals(p, p1); Result<Post> r = postAccessor.getAllAsync(p1.getUserId()).get(); assertEquals(r.one(), p1); assertEquals(r.one(), p2); assertEquals(r.one(), p3); assertTrue(r.isExhausted()); // No argument call r = postAccessor.getAll(); assertEquals(r.one(), p1); assertEquals(r.one(), p2); assertEquals(r.one(), p3); assertTrue(r.isExhausted()); BatchStatement batch = new BatchStatement(); batch.add(postAccessor.updateContentQuery("Something different", p1.getUserId(), p1.getPostId())); batch.add(postAccessor.updateContentQuery("A different something", p2.getUserId(), p2.getPostId())); manager.getSession().execute(batch); Post p1New = m.get(p1.getUserId(), p1.getPostId()); assertEquals(p1New.getContent(), "Something different"); Post p2New = m.get(p2.getUserId(), p2.getPostId()); assertEquals(p2New.getContent(), "A different something"); m.delete(p1); m.delete(p2); // Check delete by primary key too m.delete(p3.getUserId(), p3.getPostId()); assertTrue(postAccessor.getAllAsync(u1.getUserId()).get().isExhausted()); } @Test(groups = "short") public void should_map_objects_from_partial_queries() throws Exception { MappingManager manager = new MappingManager(session()); Mapper<Post> m = manager.mapper(Post.class); // Insert a few posts User u1 = new User("Paul", "paul@gmail.com"); Post p1 = new Post(u1, "Something about mapping"); Post p2 = new Post(u1, "Something else"); Post p3 = new Post(u1, "Something more"); p1.setDevice(InetAddress.getLocalHost()); p2.setTags(new HashSet<String>(Arrays.asList("important", "keeper"))); m.save(p1); m.save(p2); m.save(p3); // Retrieve posts with a projection query that only retrieves some of the fields ResultSet rs = session().execute("select user_id, post_id, title from posts where user_id = " + u1.getUserId()); Result<Post> result = m.map(rs); for (Post post : result) { assertThat(post.getUserId()).isEqualTo(u1.getUserId()); assertThat(post.getPostId()).isNotNull(); assertThat(post.getTitle()).isNotNull(); assertThat(post.getDevice()).isNull(); assertThat(post.getTags()).isNull(); } // cleanup session().execute("delete from posts where user_id = " + u1.getUserId()); } @Test(groups = "short") public void should_return_table_metadata() throws Exception { MappingManager manager = new MappingManager(session()); Mapper<Post> m = manager.mapper(Post.class); assertThat(m.getTableMetadata()).isNotNull(); assertThat(m.getTableMetadata().getName()).isEqualTo("posts"); assertThat(m.getTableMetadata().getPartitionKey()).hasSize(1); } @Test(groups = "short") public void should_not_initialize_session_when_protocol_version_provided() { Session newSession = cluster().newSession(); // Ensures that a Session is not initialized when a protocol version is provided. MappingManager manager = new MappingManager(newSession, ProtocolVersion.V1); assertThat(newSession.getState().getConnectedHosts()).hasSize(0); // Session should be initialized on first query. newSession.execute("USE " + keyspace); assertThat(newSession.getState().getConnectedHosts()).hasSize(1); } /** * Ensures that if an accessor method has a {@link QueryParameters} annotation with * {@link QueryParameters#idempotent()} as {@code true} that the {@link Statement} it generates returns * {@code true} for {@link Statement#isIdempotent()}. * * @jira_ticket JAVA-923 * @test_category object_mapper */ @Test(groups = "short") @CassandraVersion("2.0.0") public void should_flag_statement_as_idempotent() { MappingManager manager = new MappingManager(session()); PostAccessor post = manager.createAccessor(PostAccessor.class); Statement stmt = post.getAllAsStatementIdempotent(); assertThat(stmt.isIdempotent()).isEqualTo(true); } /** * Ensures that if an accessor method has a {@link QueryParameters} annotation with * {@link QueryParameters#idempotent()} as {@code false} that the {@link Statement} it generates returns * {@code false} for {@link Statement#isIdempotent()}. * * @jira_ticket JAVA-923 * @test_category object_mapper */ @Test(groups = "short") @CassandraVersion("2.0.0") public void should_flag_statement_as_non_idempotent() { MappingManager manager = new MappingManager(session()); PostAccessor post = manager.createAccessor(PostAccessor.class); Statement stmt = post.getAllAsStatementNonIdempotent(); assertThat(stmt.isIdempotent()).isEqualTo(false); } /** * Ensures that if an accessor method lacks a {@link QueryParameters} annotation with * {@link QueryParameters#idempotent()} set that the {@link Statement} it generates returns * {@code null} for {@link Statement#isIdempotent()}. * * @jira_ticket JAVA-923 * @test_category object_mapper */ @Test(groups = "short") @CassandraVersion("2.0.0") public void should_flag_statement_with_null_idempotence() { MappingManager manager = new MappingManager(session()); PostAccessor post = manager.createAccessor(PostAccessor.class); Statement stmt = post.getAllAsStatement(); assertThat(stmt.isIdempotent()).isNull(); } /** * Ensures that all statements generated by the mapper are * flagged as idempotent. * * @jira_ticket JAVA-923 * @test_category object_mapper */ @Test(groups = "short") @CassandraVersion("2.0.0") public void should_flag_all_mapper_generated_statements_as_idempotent() { MappingManager manager = new MappingManager(session()); Mapper<User> mapper = manager.mapper(User.class); User u = new User("Paul", "paul@yahoo.com"); Statement saveQuery = mapper.saveQuery(u); assertThat(saveQuery.isIdempotent()).isTrue(); Statement getQuery = mapper.getQuery(u.getUserId()); assertThat(saveQuery.isIdempotent()).isTrue(); Statement deleteQuery = mapper.deleteQuery(u.getUserId()); assertThat(saveQuery.isIdempotent()).isTrue(); } @Table(name = "users") public static class UserUnknownColumn { @PartitionKey @Column(name = "user_id") private UUID userId; @Column(name = "middle_name") private String middleName; public UserUnknownColumn() { } public UUID getUserId() { return userId; } public void setUserId(UUID userId) { this.userId = userId; } public String getMiddleName() { return middleName; } public void setMiddleName(String middleName) { this.middleName = middleName; } } /** * Ensures that when attempting to create a {@link Mapper} from a class that has a field for a * column that doesn't exist that an {@link IllegalArgumentException} is thrown. * * @jira_ticket JAVA-1126 * @test_category object_mapper */ @Test(groups = "short", expectedExceptions = {IllegalArgumentException.class}) public void should_fail_to_create_mapper_if_class_has_column_not_in_table() { MappingManager manager = new MappingManager(session()); manager.mapper(UserUnknownColumn.class); } @Table(name = "nonexistent") public static class NonExistentTable { public String name; public void setName(String name) { this.name = name; } public String getName() { return this.name; } } /** * Ensures that when attempting to create a {@link Mapper} from a class that has a {@link Table} annotation with * a name that doesn't exist in the current keyspace that an {@link IllegalArgumentException} is thrown. * * @jira_ticket JAVA-1126 * @test_category object_mapper */ @Test(groups = "short", expectedExceptions = {IllegalArgumentException.class}) public void should_fail_to_create_mapper_if_table_does_not_exist() { MappingManager manager = new MappingManager(session()); manager.mapper(NonExistentTable.class); } }