I have a Ignite cache with the two classes below.
MyPerson class looks like this:
public class MyPerson implements Serializable
{
#QuerySqlField()
public String ID;
#QuerySqlField()
public Socials[] socials;
}
And the Socials class looks like this:
public class Socials implements Serializable
{
#QuerySqlField()
public String network;
#QuerySqlField()
public String login;
}
I create and populate the cache like this:
// creating person cache
CacheConfiguration<String, MyPerson> personCacheCfg = new CacheConfiguration<String, MyPerson>();
personCacheCfg.setName("Person");
personCacheCfg.setIndexedTypes(String.class, MyPerson.class);
IgniteCache<String, MyPerson> personCache = ignite.createCache(personCacheCfg);
// adding data
personCache.put("P1", new MyPerson("Person1", new Socials[] { new Socials("fb", "a#my.com"), new Socials("tw", "b.my.com") }));
personCache.put("P2", new MyPerson("Person2", new Socials[] { new Socials("tw", "c.my.com") }));
personCache.put("P3", new MyPerson("Person3", new Socials[] { new Socials("fb", "d.my.com") }));
Now, when I want to run a SqlFieldQuery that returns all Persons that have a specific Socials account, how would I do this with Ignite SQL? Would this be a join over the array field? Or is such case better done by storing the Socials in a separate cache?
You can't run SQL queries over nested collections or arrays. You should create an additional table of pairs (socials, personId) and run joins over these two tables.
Make sure to collocate data of these tables by personId. Otherwise you will need distributed joins, that impact performance dramatically. You can find documentation on affinity collocation here: https://apacheignite.readme.io/docs/affinity-collocation
Another option without use of SQL is a ScanQuery. But it requires a full data lookup. If you are going to select a big part of data anyway, scan query may be a good option too. Documentation: https://apacheignite.readme.io/docs/cache-queries#scan-queries
Related
Hi there I like to use at Apache Ignite a Pojo which has a HashMap attribute so I can work with dynamic models at runtime. Storing and Saving of such objects works fine.
However, I m wondering if there a way exist to access the key / values of such a Hashmap through a SQL query? If this is not supported any other ways I can work in Apache Ignite with dynamic objects?
POJO Class with dynamic attributes
#Data
public class Item {
private static final AtomicLong ID_GEN = new AtomicLong();
#QuerySqlField(index = true)
private Long id;
#QuerySqlField
public Map<String,Serializable> attributes = new HashMap<String,Serializable>();
public Item(Long id, String code, String name) {
this.id = id;
}
public Item() {
this(ID_GEN.incrementAndGet());
}
public void setAttribute(String name,Serializable value) {
attributes.put(name, value);
}
public Serializable getAttribute(String name) {
return attributes.get(name);
}
}
Example Query Feature illstrated
SqlFieldsQuery query = new SqlFieldsQuery("SELECT * FROM Item WHERE attributes('Price') > 100");
SQL in Ignite is not just syntactic sugar, it requires a schema of your models to be defined before you can run SQL queries and this won't work for a collection. Therefore you need to normalize the data just like with a regular DB or rework the model's structure somehow to avoid JOIN.
Apache Ignite has no support for destructuring/collections in its SQL, so you can't peek inside HashMap via SQL.
However, you may define your own SQL functions, so you can implement e.g. SELECT hashmap_get(ATTRIBUTES, 'subkey') FROM ITEM WHERE ID = ?
But you can't have indexes on function application so the usefulness is limited.
Take an example i have three tables like this.
database image
How cloud i map the third table to java object.
class StudentCourse{
Student student;
Course course;
Double score;
}
or
class StudentCourse{
Long studentId;
Long courseId;
Double score;
}
if i use the first one, after i update some datas in databse such as student informations.The next time i query StudentCourse from database(I use mybatis) will the cache cause the incorrect data?
if i use the second one, if i want to list student's course scores,i have to first query the list of StudentCourse and then query the course's information from database through courseId, for each result i need additional queries. I think that will reduce the efficiency of the program.
Is there another way to solve this problem?
For the first one.
The second time mybatis do query, if the data hasn't been updated yet, it will get result from cache.
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
If i have an resultMap like this
<resultMap id="studentcourse" type="StudentCourse">
<association property="student" resultMap="Student" />
<association property="course" resultMap="Course"/>
<result property="score" column="score"/>
</resultMap>
At first i get an StudentCourse object from database, and the localCache cache the object.And then i update the Course in StudentCourse(change the database record).The second time i get the some StudentCourse it will return an result from localcache.So the course information in StudentCourse is dirty data.How to deal with it if i choose the first one.
Ideally you would use a class design that best models your domain and worry about mapping to a data store in a separate persistence layer. If you need to substantially change your model to allow the persistence layer to function then you need a new ORM! While I'm not familiar with mybatis I would hope it wouldn't create a new object each time the underlying data is changed.
The keys in the course and student tables act as foreign keys in the student_course table. Foreign keys are best represented as references in Java. To use the keys at the Java level forces an extra level of indirection and open you to integrity issues (e.g. if the foreign key changes).
So I would suggest:
class StudentCourse {
private final Student student;
private final Course course;
private double score;
}
You could also consider having it inside one of the other classes - that might be more convenient depending on how the classes are used:
class Student {
private final int id;
private String name;
private List<CourseScores> scores = new ArrayList<>();
public void addCourseScore(Course course, double score) {
scores.add(new CourseScore(course, score));
}
private record CourseScores(Course course, double score) { };
}
If your ORM doesn't resolve the keys for you (i.e. look up the referred object automatically when it retrieves data) then you'll need to do that yourself. It's a pretty simple object however:
class College {
private Map<Integer,Student> students;
private Map<Integer,Course> courses;
}
So the code to convert student_course data into the model above might look like:
ResultSet data;
while (!data.isAfterLast()) {
Student student = college.getStudent(data.getInteger("student"));
Course course = college.getCourse(data.getInteger("course"));
double score = data.getDouble("score");
student.addCourseScore(course, score);
data.next();
}
I tried to follow the instructions for mapping a schema in jOOQ.
First, I start with a qualified name and table:
Name myTableName = DSL.name("schema", "myTable");
Table<Record> myTable = DSL.table(myTableName);
Then I build a context with schema mapping:
Configuration configuration = new DefaultConfiguration();
configuration.set(SQLDialect.HSQLDB);
Settings settings = new Settings()
.withRenderNameStyle(RenderNameStyle.QUOTED)
.withRenderSchema(true)
.withRenderMapping(
new RenderMapping()
.withSchemata(
new MappedSchema()
.withInput("schema")
.withOutput("PUBLIC")
)
);
configuration.set(settings);
return DSL.using(configuration);
Then I build an SQL string to create the table:
context.createTable(myTable)....getSQL();
But it fails to map the schema:
invalid schema name: schema in statement [create table "schema"."myTable"(
...
What exactly am I doing wrong here?
For the bigger picture, I'm trying to write SQL that is portable across different dialects, but each of the environments I have to build for uses a different schema. I am trying to abstract a general schema in Java that I can then use jOOQ to map depending on the target environment.
This is a known issue: https://github.com/jOOQ/jOOQ/issues/5344
As of jOOQ 3.9.5, schema mapping and table mapping is not applied to plain SQL tables and custom named tables. While there will not be any mapping applied to plain SQL strings, the latter is fixed as of jOOQ 3.10.
There are two workarounds:
You can perform the mapping manually
You're in full control of table reference construction and can map the table explicitly as such:
Name myTableName = DSL.name(schema(), "myTable");
And then:
public String schema() {
if (something)
return "schema";
else
return "PUBLIC";
}
Use CustomTable
A lesser known feature is the CustomTable which can be used instead of generated tables if you're not using jOOQ's code generator. It is a bit more effort than a plain SQL table or a named table, but if you can abstract the construction of the table, it might be worth while, because CustomTable allows for easily. An example:
public class BookRecord extends CustomRecord<BookRecord> {
protected BookRecord() {
super(BookTable.BOOK);
}
}
public class BookTable extends CustomTable<BookRecord> {
public static final BookTable BOOK = new BookTable();
public static final TableField<BookRecord, Short> ID
= createField("ID", SQLDataType.SMALLINT, BOOK);
public static final TableField<BookRecord, String> TITLE
= createField("TITLE", SQLDataType.VARCHAR, BOOK);
protected BookTable() {
super("BOOK", DSL.schema(DSL.name("schema")));
}
#Override
public Class<? extends BookRecord> getRecordType() {
return BookRecord.class;
}
}
OK, so I have an interesting problem. I am using java/maven/spring-boot/cassandra... and I am trying to create a dynamic instantiation of the Mapper setup they use.
I.E.
//Users.java
import com.datastax.driver.mapping.annotations.Table;
#Table(keyspace="mykeyspace", name="users")
public class Users {
#PartitionKey
public UUID id;
//...
}
Now, in order to use this I would have to explicitly say ...
Users user = (DB).mapper(Users.class);
obviously replacing (DB) with my db class.
Which is a great model, but I am running into the problem of code repetition. My Cassandra database has 2 keyspaces, both keyspaces have the exact same tables with the exact same columns in the tables, (this is not my choice, this is an absolute must have according to my company). So when I need to access one or the other based on a form submission it becomes a mess of duplicated code, example:
//myWebController.java
import ...;
#RestController
public class MyRestController {
#RequestMapping(value="/orders", method=RequestMethod.POST)
public string getOrders(...) {
if(Objects.equals(client, "first_client_name") {
//do all the things to get first keyspace objects like....
FirstClientUsers users = (db).Mapper(FirstClientUsers.class);
//...
} else if(Objects.equals(client, "second_client_name") {
SecondClientUsers users = (db).Mapper(SecondClientUsers.class);
//....
}
return "";
}
I have been trying to use methods like...
Class cls = Class.forName(STRING_INPUT_VARIABLE_HERE);
and that works ok for base classes but when trying to use the Accessor stuff it no longer works because Accessors have to be interfaces, so when you do Class cls, it is no longer an interface.
I am trying to find any other solution on how to dynamically have this work and not have to have duplicate code for every possible client. Each client will have it's own namespace in Cassandra, with the exact same tables as all other ones.
I cannot change the database model, this is a must according to the company.
With PHP this is extremely simple since it doesn't care about typecasting as much, I can easily do...
function getData($name) {
$className = $name . 'Accessor';
$class = new $className();
}
and poof I have a dynamic class, but the problem I am running into is the Type specification where I have to explicitly say...
FirstClientUsers users = new FirstClientUsers();
//or even
FirstClientUsers users = Class.forName("FirstClientUsers");
I hope this is making sense, I can't imagine that I am the first person to have this problem, but I can't find any solutions online. So I am really hoping that someone knows how I can get this accomplished without duplicating the exact same logic for every single keyspace we have. It makes the code not maintainable and unnecessarily long.
Thank you in advance for any help you can offer.
Do not specify the keyspace in your model classes, and instead, use the so-called "session per keyspace" pattern.
Your model class would look like this (note that the keyspace is left undefined):
#Table(name = "users")
public class Users {
#PartitionKey
public UUID id;
//...
}
Your initialization code would have something like this:
Map<String, Mapper<Users>> mappers = new ConcurrentHashMap<String, Mapper<Users>>();
Cluster cluster = ...;
Session firstClientSession = cluster.connect("keyspace_first_client");
Session secondClientSession = cluster.connect("keyspace_second_client");
MappingManager firstClientManager = new MappingManager(firstClientSession);
MappingManager secondClientManager = new MappingManager(secondClientSession);
mappers.put("first_client", firstClientManager.mapper(Users.class));
mappers.put("second_client", secondClientManager.mapper(Users.class));
// etc. for all clients
You would then store the mappers object and make it available through dependency injection to other components in your application.
Finally, your REST service would look like this:
import ...
#RestController
public class MyRestController {
#javax.inject.Inject
private Map<String, Mapper<Users>> mappers;
#RequestMapping(value = "/orders", method = RequestMethod.POST)
public string getOrders(...) {
Mapper<Users> usersMapper = getUsersMapperForClient(client);
// process the request with the right client's mapper
}
private Mapper<Users> getUsersMapperForClient(String client) {
if (mappers.containsKey(client))
return mappers.get(client);
throw new RuntimeException("Unknown client: " + client);
}
}
Note how the mappers object is injected.
Small nit: I would name your class User in the singular instead of Users (in the plural).
Let's say I have a method in java, which looks up a user in a database and returns their address and the team they are on.
I want to return both values from the method, and don't want to split the method in two because it involves a database call and splitting involves twice the number of calls.
Given typical concerns in a moderate to large software project, what's the best option?
whatGoesHere getUserInfo(String name) {
// query the DB
}
I know the question smells of duplication with existing ones, but each other question had some element that made it different enough from this example that I thought it was worth asking again.
you have some options.
The most OOP it will be create a class to encapsulate those 2 properties, something like that
private class UserInfo {
private Address address;
private Team team;
}
Or if you want a simple solution you can return an array of objects:
Object[] getUserInfo(String name) {
// query the DB
return new Object[]{address,team};
}
Or if you want to expose this method to some library you can have some interface that it will consume those properties, something like this:
class APIClass{
interface UserInfo{
public Address getAddress();
public Team getTeam();
}
UserInfo getUserInfo(String name) {
// query the DB
return new UserInfo(){
public Address getAddress(){ return address; }
public Team getTeam(){ return team; }
};
}
}
cant a map help , A MultivalueMap. Where the key is the user name and the 2 values are the adress and the team name. I am assuming both your Address and team are String variables, You can know more about Multivalue Map here
http://commons.apache.org/collections/apidocs/org/apache/commons/collections/map/MultiValueMap.html
http://apachecommonstipsandtricks.blogspot.in/2009/01/multi-value-map-values-are-list.html
First model your abstractions, relationships and multiplicity well (see an e.g. below). Then you can model tables accordingly. Once these two steps are performed you can either leverage JPA that can be configured to load your object graph or you write JDBC code and create the graph your self by running a SQL query with proper SQL JOINs.
A User has an Address
A Team can have 1 or more Users (and can a User play for more teams?)
You can return a String array with user name and group name in it . The method looks like :
public String[] getUserInfo(String name) {
String[] result = new String[2];
// query the DB
...
result[0] = userName;
result[1] = groupName;
return result;
}
A common solution to this kind of issue is to create a custom object with as many attributes as the values you want to return.
If you can't create a new class for this, you can use a Map<String, Object>, but this approach is not type-safe.
I thought Guava had a generic Pair class already, but I cannot find it. You can build your own using generics if you're on Java 1.5+.
public class Pair<X,Y>
{
public final X first;
public final Y second;
public Pair(X first, Y second) {
this.first = first;
this.second = second;
}
}
Feel free to make the fields private and add getters. :) Using it is easy:
return new Pair<Address,Team>(address, team);
Update
Apache Commons Lang has Pair. See this SO question for more options.