blob: bf6706b569d0b73ca376229bf963d6e51df05532 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.ignite.internal.table;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.ignite.binary.BinaryObject;
import org.apache.ignite.binary.BinaryObjects;
import org.apache.ignite.internal.schema.Column;
import org.apache.ignite.internal.schema.NativeTypes;
import org.apache.ignite.internal.schema.SchemaDescriptor;
import org.apache.ignite.internal.storage.basic.ConcurrentHashMapPartitionStorage;
import org.apache.ignite.internal.table.distributed.storage.VersionedRowStore;
import org.apache.ignite.internal.table.impl.DummyInternalTableImpl;
import org.apache.ignite.internal.tx.impl.HeapLockManager;
import org.apache.ignite.internal.tx.impl.TxManagerImpl;
import org.apache.ignite.table.KeyValueView;
import org.apache.ignite.table.RecordView;
import org.apache.ignite.table.Table;
import org.apache.ignite.table.Tuple;
import org.apache.ignite.table.mapper.Mapper;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Example.
*/
@SuppressWarnings({"PMD.EmptyLineSeparatorCheck", "emptylineseparator", "unused", "UnusedAssignment", "InstanceVariableMayNotBeInitialized",
"JoinDeclarationAndAssignmentJava"})
public class Example {
/**
* Returns table implementation.
*/
private static List<Table> tableFactory() {
TxManagerImpl txManager = new TxManagerImpl(null, new HeapLockManager());
return Collections.singletonList(new TableImpl(new DummyInternalTableImpl(new VersionedRowStore(
new ConcurrentHashMapPartitionStorage(), txManager), txManager), null, null));
}
/**
* Use case 1: a simple one. The table has the structure [ [id int, orgId int] // key [name varchar, lastName varchar, decimal salary,
* int department] // value ] We show how to use the raw TableRow and a mapped class.
*/
@Disabled
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase1(Table t) {
// Search row will allow nulls even in non-null columns.
Tuple res = t.recordView().get(Tuple.create().set("id", 1).set("orgId", 1));
String name = res.value("name");
String lastName = res.value("latName");
BigDecimal salary = res.value("salary");
Integer department = res.value("department");
// We may have primitive-returning methods if needed.
int departmentPrimitive = res.intValue("department");
// Note that schema itself already defined which fields are key field.
class Employee {
final int id;
final int orgId;
String name;
String lastName;
BigDecimal salary;
int department;
Employee(int id, int orgId) {
this.id = id;
this.orgId = orgId;
}
}
RecordView<Employee> employeeView = t.recordView(Employee.class);
Employee e = employeeView.get(new Employee(1, 1));
// As described in the IEP-54, we can have a truncated mapping.
class TruncatedEmployee {
final int id;
final int orgId;
String name;
String lastName;
TruncatedEmployee(int id, int orgId) {
this.id = id;
this.orgId = orgId;
}
}
RecordView<TruncatedEmployee> truncatedEmployeeView = t.recordView(TruncatedEmployee.class);
// salary and department will not be sent over the network during this call.
TruncatedEmployee te = truncatedEmployeeView.get(new TruncatedEmployee(1, 1));
}
/**
* Use case 2: using simple KV mappings The table has structure is [ [id int, orgId int] // key [name varchar, lastName varchar, decimal
* salary, int department] // value ].
*/
@Disabled
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase2(Table t) {
class EmployeeKey {
final int id;
final int orgId;
EmployeeKey(int id, int orgId) {
this.id = id;
this.orgId = orgId;
}
}
class Employee {
String name;
String lastName;
BigDecimal salary;
int department;
}
KeyValueView<EmployeeKey, Employee> employeeKv = t.keyValueView(EmployeeKey.class, Employee.class);
employeeKv.get(new EmployeeKey(1, 1));
// As described in the IEP-54, we can have a truncated KV mapping.
class TruncatedEmployee {
String name;
String lastName;
}
KeyValueView<EmployeeKey, TruncatedEmployee> truncatedEmployeeKv = t.keyValueView(EmployeeKey.class, TruncatedEmployee.class);
TruncatedEmployee te = truncatedEmployeeKv.get(new EmployeeKey(1, 1));
}
/**
* Use case 3: Single table strategy for inherited objects. The table has structure is [ [id long] // key [owner varchar, cardNumber
* long, expYear int, expMonth int, accountNum long, bankName varchar] // value ]
*/
@Disabled
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase3(Table t) {
class BillingDetails {
String owner;
}
class CreditCard extends BillingDetails {
long cardNumber;
int expYear;
int expMonth;
}
class BankAccount extends BillingDetails {
long account;
String bankName;
}
KeyValueView<Long, CreditCard> credCardKvView = t.keyValueView(Long.class, CreditCard.class);
CreditCard creditCard = credCardKvView.get(1L);
KeyValueView<Long, BankAccount> backAccKvView = t.keyValueView(Long.class, BankAccount.class);
BankAccount bankAccount = backAccKvView.get(2L);
// Truncated view.
KeyValueView<Long, BillingDetails> billingDetailsKvView = t.keyValueView(Long.class, BillingDetails.class);
BillingDetails billingDetails = billingDetailsKvView.get(2L);
// Without discriminator it is impossible to deserialize to correct type automatically.
assert !(billingDetails instanceof CreditCard);
assert !(billingDetails instanceof BankAccount);
// Wide record.
class BillingRecord {
final long id;
String owner;
long cardNumber;
int expYear;
int expMonth;
long account;
String bankName;
BillingRecord(long id) {
this.id = id;
}
}
final RecordView<BillingRecord> billingView = t.recordView(BillingRecord.class);
final BillingRecord br = billingView.get(new BillingRecord(1));
}
/**
* Use case 4: Conditional serialization. The table has structure is [ [id int, orgId int] // key [owner varchar, type int,
* conditionalDetails byte[]] // value ]
*/
@Disabled
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase4(Table t) {
class OrderKey {
final int id;
final int orgId;
OrderKey(int id, int orgId) {
this.id = id;
this.orgId = orgId;
}
}
class OrderValue {
String owner;
int type; // Discriminator value.
/* BillingDetails */ Object billingDetails;
}
class CreditCard /* extends BillingDetails */ {
long cardNumber;
int expYear;
int expMonth;
}
class BankAccount /* extends BillingDetails */ {
long account;
String bankName;
}
KeyValueView<OrderKey, OrderValue> orderKvView = t
.keyValueView(Mapper.of("key", OrderKey.class), Mapper.buildFrom(OrderValue.class).map("billingDetails", (row) -> {
BinaryObject binObj = row.binaryObjectValue("conditionalDetails");
int type = row.intValue("type");
return type == 0
? BinaryObjects.deserialize(binObj, CreditCard.class)
: BinaryObjects.deserialize(binObj, BankAccount.class);
}).build());
OrderValue ov = orderKvView.get(new OrderKey(1, 1));
// Same with direct Row access and BinaryObject wrapper.
Tuple res = t.recordView().get(Tuple.create().set("id", 1).set("orgId", 1));
byte[] objData = res.value("billingDetails");
BinaryObject binObj = BinaryObjects.wrap(objData);
// Work with the binary object as in Ignite 2.x
// Additionally, we may have a shortcut similar to primitive methods.
binObj = res.binaryObjectValue("billingDetails");
// Same with RecordAPI.
class OrderRecord {
final int id;
final int orgId;
String owner;
int type;
BinaryObject billingDetails;
OrderRecord(int id, int orgId) {
this.id = id;
this.orgId = orgId;
}
}
final RecordView<OrderRecord> orderRecView = t.recordView(OrderRecord.class);
OrderRecord orderRecord = orderRecView.get(new OrderRecord(1, 1));
binObj = orderRecord.billingDetails;
// Manual deserialization is possible as well.
Object billingDetails = orderRecord.type == 0
? BinaryObjects.deserialize(binObj, CreditCard.class)
: BinaryObjects.deserialize(binObj, BankAccount.class);
}
/**
* Use case 5: using byte[] and binary objects in columns. The table has structure [ [id int, orgId int] // key [originalObject byte[],
* upgradedObject byte[], int department] // value ] Where {@code originalObject} is some value that was originally put to the column,
* {@code upgradedObject} is a version 2 of the object, and department is an extracted field.
*/
@Disabled
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase5(Table t) {
Tuple res = t.recordView().get(Tuple.create().set("id", 1).set("orgId", 1));
byte[] objData = res.value("originalObject");
BinaryObject binObj = BinaryObjects.wrap(objData);
// Work with the binary object as in Ignite 2.x
// Additionally, we may have a shortcut similar to primitive methods.
binObj = res.binaryObjectValue("upgradedObject");
// Plain byte[] and BinaryObject fields in a class are straightforward.
class Record {
final int id;
final int orgId;
byte[] originalObject;
Tuple upgradedObject;
int department;
Record(int id, int orgId) {
this.id = id;
this.orgId = orgId;
}
}
RecordView<Record> recordView = t.recordView(Record.class);
// Similarly work with the binary objects.
Record rec = recordView.get(new Record(1, 1));
// Now assume that we have some POJO classes to deserialize the binary objects.
class JavaPerson {
String name;
String lastName;
}
class JavaPersonV2 extends JavaPerson {
int department;
}
// We can have a compound record deserializing the whole tuple automatically.
class JavaPersonRecord {
JavaPerson originalObject;
JavaPersonV2 upgradedObject;
int department;
}
RecordView<JavaPersonRecord> personRecordView = t.recordView(JavaPersonRecord.class);
// Or we can have an arbitrary record with custom class selection.
class TruncatedRecord {
JavaPerson upgradedObject;
int department;
}
RecordView<TruncatedRecord> truncatedView = t.recordView(
Mapper.buildFrom(TruncatedRecord.class)
.map("upgradedObject", JavaPersonV2.class).build());
// Or we can have a custom conditional type selection.
RecordView<TruncatedRecord> truncatedView2 = t.recordView(
Mapper.buildFrom(TruncatedRecord.class)
.map("upgradedObject", (row) -> {
BinaryObject binObj1 = row.binaryObjectValue("upgradedObject");
int dept = row.intValue("department");
return dept == 0 ? BinaryObjects.deserialize(binObj1, JavaPerson.class)
: BinaryObjects.deserialize(binObj1, JavaPersonV2.class);
})
// TODO: But how to write the columns ??? There is no separate "write mapping" yet.
// .map("person", "colPersol", obj -> BinaryObjects.serialize(obj))
// .map("department", "colDepartment", obj -> obj instanceof JavaPerson ? 0 : 1)
.build());
}
/**
* Use case 6: a simple one. The table has the structure [ [id long] // key [name varchar, lastName varchar, decimal salary, int
* department] // value ] We show how to use the raw TableRow and a mapped class.
*/
@Disabled
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase6(Table t) {
// Search row will allow nulls even in non-null columns.
Tuple res = t.recordView().get(Tuple.create().set("id", 1));
String name = res.value("name");
String lastName = res.value("latName");
BigDecimal salary = res.value("salary");
Integer department = res.value("department");
// We may have primitive-returning methods if needed.
int departmentPrimitive = res.intValue("department");
// Note that schema itself already defined which fields are key field.
class Employee {
String name;
String lastName;
BigDecimal salary;
int department;
}
class Key {
long id;
}
KeyValueView<Long, Employee> employeeView = t.keyValueView(Long.class, Employee.class);
Employee e = employeeView.get(1L);
}
/**
* Use case 7: a simple one. The table has the structure [ [byte[]] // key [name varchar, lastName varchar, decimal salary, int
* department] // value ] We show how to use the raw TableRow and a mapped class.
*/
@Disabled
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase7(Table t) {
// Note that schema itself already defined which fields are key field.
class Employee {
String name;
String lastName;
BigDecimal salary;
int department;
}
KeyValueView<Long, BinaryObject> employeeView = t.keyValueView(Long.class, BinaryObject.class);
employeeView.put(1L, BinaryObjects.wrap(new byte[0] /* serialized Employee */));
t.keyValueView(Mapper.of(Long.class), Mapper.of("value", Employee.class));
}
/**
* Use case 8: Here we show how to use mapper to represent the same data in different ways. Single column case is just for simplicity.
*/
@Disabled
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase8(Table t) {
new SchemaDescriptor(
1,
new Column[]{new Column("key", NativeTypes.INT64, false)},
new Column[]{new Column("val", NativeTypes.BYTES, true)}
);
class UserObject {
}
class Employee {
UserObject data;
}
class Employee2 {
byte[] data;
}
Mapper.of(byte[].class);
// One-column keys only and only native types.
Mapper.of(Long.class);
// LongMapper -> long - is it possible?
class MyKey {
byte[] id;
}
// Values
// -- remove/rename Mapper.buildFrom(Employee.class).map("data", "val").map("data1", "val1");
Mapper.builder(Employee.class)
.map("data", "val")
.map("data1", "val1");
Mapper.builder(Employee.class).map(
"data", "val",
"data1", "val1"
);
// Shortcuts (supported keys and values).
Mapper.of(Employee.class, "data", "val")
Mapper.of(Employee.class, "data", "val", "data1", "val1")
// Shortcut (supported one-column key and value).
Mapper.of(Long.class, "count")
// Shortcut (key and value represented by byte array)
Mapper.of(UserObject.class, "data")
/*
Mapper.of(UserObject.class, "data", marsh)
Mapper.of(Long.class, "count")
*/
Mapper.builder(Employee.class)
.marsh(marsh, "colData")
.map("fieldData", "colData")
// OR
.map(marsh, "fieldData", "colData")
// Class usage without a column name can work correctly only and only when each of key and value parts is single column.
KeyValueView<Long, Employee> v1 = t.keyValueView(Long.class, Employee.class);
KeyValueView<Long, Employee> v2 = t.keyValueView(
Mapper.of(Long.class),
// Class usage without a column name can work correctly only and only when the key part is single column.
Mapper.buildFrom(Employee.class).map("data", "val").build());
KeyValueView<Long, Employee2> v3 = t.keyValueView(
Mapper.of("key", Long.class),
Mapper.buildFrom(Employee2.class).map("data", "val").build());
KeyValueView<Long, UserObject> v4 = t.keyValueView(
Mapper.of("key", Long.class),
Mapper.of("data", UserObject.class));
KeyValueView<Long, byte[]> v5 = t.keyValueView(
Mapper.of("key", Long.class),
Mapper.of("data", byte[].class));
// The values in next operations are equivalent, and lead to the same row value part content.
v1.put(1L, new Employee());
v2.put(2L, new Employee());
v3.put(3L, new Employee2());
v4.put(4L, new UserObject());
v5.put(5L, new byte[]{/* serialized UserObject bytes */});
// Get operations return the same result for all keys for each of row.
// for 1 in 1..5
// v1.get(iL) == v1.get(1L);
// ============================ GET ===============================================
UserObject obj = v4.get(1L); // indistinguishable absent value and null column
// Optional way
Optional<UserObject> obj = v4.get(1L); // abuse of Optional type
// NullableValue way
NullableValue<UserObject> obj = v4.getNullable(1L); //
UserObject obj = v4.get(1L); // what if user uses this syntax for nullable column?
// 1. Exception always
// 2. Exception if column value is null (use getNullable)
// ============================ PUT ===============================================
v4.put(1L, null);
v4.remove(1L, null);
}
interface NullableValue<T> {
T get();
}
/**
* Similar to case 5, but fully manual mapping.
* TODO: Let's drop case 5 and replace with this.
*
* @param t Table.
*/
public void useCase9(Table t) {
// Now assume that we have some POJO classes to deserialize the binary objects.
class JavaPerson {
String name;
String lastName;
}
class JavaPersonV2 extends JavaPerson {
int department;
}
// We can have a compound record deserializing the whole tuple automatically.
class JavaPersonRecord {
JavaPerson originalObject;
JavaPersonV2 upgradedObject;
int department;
}
// Or we can have an arbitrary record with custom class selection.
class TruncatedRecord {
JavaPerson person;
int department; // Discriminator column. Maybe
}
RecordView<TruncatedRecord> truncatedView2 = t.recordView(
Mapper.buildFrom(TruncatedRecord.class)
.map((obj) -> obj == null
? null
: Tuple.create(Map.of(
"colPerson", BinaryObjects.serialize(obj),
"colDepartment", (obj.person instanceof JavaPerson) ? 0 : 1
)),
(row) -> {
if (row == null) {
return null;
}
TruncatedRecord rec = new TruncatedRecord();
int dep = row.intValue("colDepartment");
rec.department = dep;
rec.person = dep == 0 ? BinaryObjects.deserialize(row.binaryObjectValue("colPerson"), JavaPerson.class)
: BinaryObjects.deserialize(row.binaryObjectValue("colPerson"), JavaPersonV2.class);
return rec;
})
.build());
}
}