blob: 53a0cf9a0688ad8a3876126c1324b027781b11ec [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 org.apache.ignite.binary.BinaryObject;
import org.apache.ignite.binary.BinaryObjects;
import org.apache.ignite.internal.replicator.ReplicaService;
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.table.impl.DummyInternalTableImpl;
import org.apache.ignite.internal.tx.impl.HeapLockManager;
import org.apache.ignite.lang.NullableValue;
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.apache.ignite.table.mapper.TypeConverter;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
/**
* Example.
*/
@SuppressWarnings({"PMD.EmptyLineSeparatorCheck", "emptylineseparator", "unused", "UnusedAssignment", "InstanceVariableMayNotBeInitialized",
"JoinDeclarationAndAssignmentJava"})
public class Example {
/**
* Returns table implementation.
*/
private static List<Table> tableFactory() {
return Collections.singletonList(
new TableImpl(new DummyInternalTableImpl(Mockito.mock(ReplicaService.class)), new HeapLockManager()));
}
/**
* 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("https://issues.apache.org/jira/browse/IGNITE-19300")
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase1(Table t) {
// Search row will allow nulls even in non-null columns.
Tuple res = t.recordView().get(null, 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(null, 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(null, 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("https://issues.apache.org/jira/browse/IGNITE-19300")
@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(null, 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(null, 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("https://issues.apache.org/jira/browse/IGNITE-19300")
@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(null, 1L);
KeyValueView<Long, BankAccount> backAccKvView = t.keyValueView(Long.class, BankAccount.class);
BankAccount bankAccount = backAccKvView.get(null, 2L);
// Truncated view.
KeyValueView<Long, BillingDetails> billingDetailsKvView = t.keyValueView(Long.class, BillingDetails.class);
BillingDetails billingDetails = billingDetailsKvView.get(null, 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(null, 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("https://issues.apache.org/jira/browse/IGNITE-19300")
@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(OrderKey.class, "key"),
Mapper.builder(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(null, new OrderKey(1, 1));
// Same with direct Row access and BinaryObject wrapper.
Tuple res = t.recordView().get(null, 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(null, 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("https://issues.apache.org/jira/browse/IGNITE-19300")
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase5(Table t) {
Tuple res = t.recordView().get(null, 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(null, 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;
}
// Custom serializer.
TypeConverter<JavaPerson, byte[]> serializer = new TypeConverter<>() {
@Override
public byte[] toColumnType(JavaPerson obj) throws Exception {
return BinaryObjects.serialize(obj).bytes();
}
@Override
public JavaPerson toObjectType(byte[] data) throws Exception {
return BinaryObjects.deserialize(BinaryObjects.wrap(data), JavaPerson.class);
}
};
RecordView<TruncatedRecord> truncatedView = t.recordView(
Mapper.builder(TruncatedRecord.class)
.convert("upgradedObject", serializer)
.map("updradedObject", "updradedObject")
.build());
// Or we can have a custom conditional type selection.
RecordView<TruncatedRecord> truncatedView2 = t.recordView(
Mapper.builder(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 JavaPersonv2 ? 1 : 0)
.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("https://issues.apache.org/jira/browse/IGNITE-19300")
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase6(Table t) {
// Search row will allow nulls even in non-null columns.
Tuple res = t.recordView().get(null, 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(null, 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("https://issues.apache.org/jira/browse/IGNITE-19300")
@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(null, 1L, BinaryObjects.wrap(new byte[0] /* serialized Employee */));
t.keyValueView(Mapper.of(Long.class), Mapper.of(Employee.class, "value"));
}
/**
* Use case 8: Here we show how to use mapper to represent the same data in different ways.
*/
@Disabled("https://issues.apache.org/jira/browse/IGNITE-19300")
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase8(Table t) {
new SchemaDescriptor(
1,
new Column[]{new Column("colId", NativeTypes.INT64, false)},
new Column[]{new Column("colData", NativeTypes.BYTES, true)}
);
// Arbitrary user type.
class UserObject {
double salary;
}
// Domain class, which fields mapped to table columns.
class Employee {
UserObject fieldData;
}
// Domain class, which fields mapped to table columns.
class Employee2 {
byte[] fieldData;
}
Mapper.builder(Employee.class)
.map("fieldData.salary", "colSalary")
.build();
// Actually, any bi-directional converter can be here instead.
// Marshaller is a special case of "UserObject <--> byte[]" converter, just for example.
TypeConverter<UserObject, byte[]> marsh = null; // here, create some marshaller for UserObject.class.
// One-column only supported first-citizen types.
Mapper.of(Long.class);
Mapper.of(byte[].class);
// Automatically maps object fields to columns with same names.
Mapper.of(UserObject.class);
// LongMapper -> long - is it possible?
// Shortcut (supported one-column key and value).
Mapper.of(Long.class, "colId");
// Shortcut (key and value represented by byte array)
Mapper.of(UserObject.class, "colData"); // Does one-column record make sense ??? either one-column table ???
// Keys, Values, and Records
Mapper.builder(Employee.class)
.map("fieldData", "colData")
.map("fieldData2", "colData1")
.build();
Mapper.builder(Employee.class)
.map("fieldData", "colData")
// .automap() // map field->column by names
.build();
Mapper.builder(Employee.class).map(
"fieldData", "colData",
"fieldData2", "colData1"
).build();
// Shortcuts (supported keys and values and records).
Mapper.of(Employee.class, "fieldData", "colData");
Mapper.of(Employee.class, "fieldData", "colData", "fieldData1", "colData1");
// Shortcut (supported one-column key and value) with additional transformation.
Mapper.of(UserObject.class, "data", marsh);
// (supported one-column key and value and records) with additional transformation.
Mapper.builder(Employee.class)
//TODO: Will it be useful to set a bunch of columns that will use same converter (serializer) ???
// if so, then conflicts with the right next case.
.convert("colData", marsh)
.map("fieldData", "colData")
.build();
// OR another way to do the same
Mapper.builder(Employee.class)
.map("fieldData", "colData", marsh)
.build();
// Next views shows different approaches to map user objects to columns.
KeyValueView<Long, Employee> v1 = t.keyValueView(
Mapper.of(Long.class),
Mapper.of(Employee.class, "fieldData", "colData"));
KeyValueView<Long, Employee2> v2 = t.keyValueView(
Mapper.of(Long.class),
Mapper.builder(Employee2.class)
.convert("colData", marsh)
.map("fieldData", "colData")
.build()
);
KeyValueView<Long, Employee2> v3 = t.keyValueView(
Mapper.of(Long.class, "colId"),
Mapper.builder(Employee2.class).map("fieldData", "colData", marsh).build());
KeyValueView<Long, UserObject> v4 = t.keyValueView(
Mapper.of(Long.class, "colId"),
Mapper.of(UserObject.class, "colData", marsh));
KeyValueView<Long, byte[]> v5 = t.keyValueView(
Mapper.of(Long.class, "colId"),
Mapper.of(byte[].class, "colData")
);
// The values in next operations are equivalent, and lead to the same row value part content.
v1.put(null, 1L, new Employee());
v2.put(null, 2L, new Employee2());
v3.put(null, 3L, new Employee2());
v4.put(null, 4L, new UserObject());
v5.put(null, 5L, new byte[]{/* serialized UserObject bytes */});
// Shortcut with classes for simple use-case
KeyValueView<Long, String> v6 = t.keyValueView(
Long.class,
String.class
);
// Shortcut with classes for widely used case
KeyValueView<Long, UserObject> v7 = t.keyValueView(
Long.class,
UserObject.class // obj.salary -> colSalary
);
// do the same as
KeyValueView<Long, UserObject> v8 = t.keyValueView(
Mapper.of(Long.class),
Mapper.builder(UserObject.class).automap().build() // obj.salary -> colSalary
);
KeyValueView<Long, UserObject> v9 = t.keyValueView(
Mapper.of(Long.class),
Mapper.of(UserObject.class, "colData", marsh) // UserObject -> byte[] -> colData
);
// 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 ===============================================
new SchemaDescriptor(
1,
new Column[]{new Column("colId", NativeTypes.INT64, false)},
new Column[]{
new Column("colData", NativeTypes.BYTES, true),
new Column("colSalary", NativeTypes.BYTES, true)
}
);
UserObject obj = v4.get(null, 1L); // indistinguishable absent value and null column
// Optional way
// Optional<UserObject> optionalObj = v4.get(1L); // abuse of Optional type
// NullableValue way
NullableValue<UserObject> nullableValue = v4.getNullable(null, 1L);
UserObject userObject = v4.get(null, 1L); // what if user uses this syntax for nullable column?
// 1. Exception always
// 2. Exception if column value is null (use getNullable)
// Get or default
String str = v6.getOrDefault(null, 1L, "default");
// ============================ PUT ===============================================
v4.put(null, 1L, null);
v4.remove(null, 1L, null);
}
/**
* Fully manual mapping case. Allows users to write powerful functions that will convert an object to a row and vice versa.
*
* <p>For now, it is the only case where conditional mapping (condition on another field) is possible. This case widely used in ORM
* (e.g. Hibernate) to store inherited objects in same table using a condition on special-purpose "discriminator" column.
*
* @param t Table.
*/
@Disabled("https://issues.apache.org/jira/browse/IGNITE-19300")
@ParameterizedTest
@MethodSource("tableFactory")
public void useCase9(Table t) {
// Now assume that we have some POJO classes to deserialize the binary objects.
class Emploee {
String name;
String lastName;
}
// Here we
class EmploeeV2 extends Emploee {
int department;
}
// Or we can have an arbitrary record with custom class selection.
class UserRecord {
Emploee person;
int department; // Discriminator column.
}
RecordView<UserRecord> truncatedView2 = t.recordView(
Mapper.of(
// Next two functions of compatible interfaces parametrized with compatible generic types.
(obj) -> obj == null
? null
: Tuple.create(Map.of(
"colPerson", BinaryObjects.serialize(obj),
"colDepartment", (obj.person instanceof EmploeeV2) ? 1 : 0
)),
(row) -> {
if (row == null) {
return null;
}
UserRecord rec = new UserRecord();
int dep = row.intValue("colDepartment");
rec.department = dep;
rec.person = dep == 0 ? BinaryObjects.deserialize(row.binaryObjectValue("colPerson"), Emploee.class)
: BinaryObjects.deserialize(row.binaryObjectValue("colPerson"), EmploeeV2.class);
return rec;
})
);
}
}