RoomDatabase LiveData JOIN fields - java

I've recently started using LiveData in my project and was facing difficulties JOINing data from related tables. Not being able to find anything related to this on the web - links welcome, please, I started this morning to try and find a solution.
So, I put together a Product Entity class…
#Entity(tableName = "products")
public class Product {
#PrimaryKey(autoGenerate = true)
#NonNull
int productID;
#NonNull
private String productName;
#NonNull
private String supplierName;
private int supplierID;
private int stocklevel;
public Product(#NonNull String productName, int supplierID) {
this.productName = productName;
this.supplierID = supplierID;
this.supplierName = "Dummy Supplier";
stocklevel = 0;
}
and a DAO with the query…
#Query("SELECT products.*, suppliers.supplierName FROM products JOIN suppliers ON products.supplierID = suppliers.supplierID ORDER BY stocklevel DESC")
LiveData<List<Product>> getProductDetails();
so that, when the query is run, the placeholder text is overwritten by the supplierName from the related table. And it works!
So, my question is, 'Is this an acceptable way of proceeding? And, if not - what is?'
Any feedback? Or links?

Related

Android Room Relationship duplicating information

Having the weirdest issue here, all is working fine, except that my 1-to-M query is duplicating the data.
Customer table
#Entity(tableName = "customer_table")
public class Customer {
#ColumnInfo(name = "Customer_Serial", index = true)
#PrimaryKey
private int customerSerial;
#ColumnInfo(name = "Customer_Name")
private String customerName;
public Customer(int customerSerial, String customerName) {
this.customerSerial = customerSerial;
this.customerName = customerName;
}
}
Invoice table
#Entity(tableName = "invoice_table")
public class Invoice {
#ColumnInfo(name = "Invoice_Number", index = true)
#PrimaryKey
private int invoiceNumber;
#ColumnInfo(name = "Customer_Serial")
private int customerSerial;
public Invoice(int invoiceNumber, int customerSerial) {
this.invoiceNumber = invoiceNumber;
this.customerSerial = customerSerial;
}
}
CustomerInvoice relation
public class CustomerInvoice {
#Embedded public Customer customer;
#Relation(
parentColumn = "Customer_Serial",
entityColumn = "Customer_Serial"
)
public List<Invoice> invoices;
}
DAO
#Transaction
#Query("SELECT * FROM customer_table INNER JOIN invoice_table ON invoice_table.Customer_Serial = customer_table.Customer_Serial")
List<CustomerInvoice> getAllCustInvoices();
#Insert
void insertInvoice(Invoice... invoice);
#Insert
void insertCustomer(Customer... customer);
If I debug my application, set a breakpoint to test the Room stuff, then use the 'Evaluate' feature in Android Studio, I do the following
Invoice invoice1 = new Invoice(1234, 1);
Invoice invoice2 = new Invoice(2468, 1);
Customer customer = new Customer(1, "Test Customer");
dao.insertCustomer(customer);
dao.insertInvoice(invoice1);
dao.insertInvoice(invoice2);
If I then retrieve the information using getAllCustInvoices()
The list returned has 2 in it.
It has the customer duplicated for each invoice assigned to them, and then both invoices listed in each 1.
I'm not entirely sure where I am going wrong here, this is a simplified example of what the app itself is actually doing, simplified enough to see if something else in my code was causing the problem or not.
Turns out, even with the simplified example, it has the issue.
The issue
When #Relation is used Room extracts the children (ALL children) per parent (effectively running a second query to extract the children, hence the recommendation for using #Transaction ). By specifying the JOIN you are extracting the same parent for each child (i.e. the cartesian product) and hence the duplication.
i.e. Room does the equivalent of the JOIN internally
The Fix
#Transaction
#Query("SELECT * FROM customer_table")
List<CustomerInvoice> getAllCustInvoices();

How do I set up a Foreign Key

What I have setup are two tables, one for a user created account, and the other that lets the user buy a product.
I have both tables set up like so
Customer Table
#PrimaryKey(autoGenerate = true)
private int custId;
#ColumnInfo(name = "user_name")
private String userName;
#ColumnInfo(name = "password")
private String password;
#ColumnInfo(name = "first_name")
private String firstName;
#ColumnInfo(name = "last_name")
private String lastName;
#ColumnInfo(name = "address")
private String address;
#ColumnInfo(name = "city")
private String city;
#ColumnInfo(name = "postal_code")
private String postalCode;
#ColumnInfo(name = "country")
private String country;
Phone Table
#PrimaryKey(autoGenerate = true)
private int productId;
private String phoneMake;
private String phoneModel;
private String phoneColor;
private String storageCapacity;
private Float price;
What I have set up are two foreign keys, one in each table. My last table is for ordering the phones, which requires using both Primary Keys from each table. What I feel like I need is a ForeignKey, similar in vein to the PrimaryKey already created. The problem is that I am unsure how to implement that into the program. Everything I try doing is not working. I have looked at the documentation, but nothing clicks. I hope you can help me with the correct screenshot. If more is needed let me know (This code is written in Java code)
If you simply want a Customer to have 1 phone, then you have have a single column (member variable) for the relationship that will store the phone's product id.
e.g.
private int mapToPhone; //<<<<< ADDED no need for #ColumnInfo the column name will be as per the variable name.
Obviously you set the value to an appropriate value.
To then get the Customer with the phone's details then you have a POJO that embeds the parent (Customer) using the #Embedded annotation has the child (Phone) using the #Relation annotation.
e.g. :-
class CustomerWithPhoneDetails {
#Embedded
Customer customer;
#Relation(
entity = Phone.class,
parentColumn = "mapToPhone",
entityColumn = "productId"
)
Phone phoneDetails;
}
You can then have a method in the #Dao annotated interface/abstract class which queries the parent table BUT returns the POJO or list/array of the POJO e.g. :-
#Query("SELECT * FROM Customer")
abstract List<CustomerWithPhoneDetails> getAllCustomersWithPhoneDeytails();
Example
Based upon your code, and the additional example code along with an #Database annotated abstract class :-
#Database(entities = {Customer.class,Phone.class}, version = 1, exportSchema = false)
abstract class TheDatabase extends RoomDatabase {
abstract AllDao getAllDao();
private static volatile TheDatabase instance = null;
public static TheDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context,TheDatabase.class,"the_database.db")
.allowMainThreadQueries()
.build();
}
return instance;
}
}
and an Activity e.g. :-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDao dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = TheDatabase.getInstance(this);
dao = db.getAllDao();
long phone01ProductId = dao.insert(new Phone("PhoneMaker001","Model001","Color001","100Mb",111.11F));
long phone02ProductId = dao.insert(new Phone("PhoneMaker002","Model002","Color002","200Mb",222.22F));
dao.insert(new Customer("c001","password001","firstname001","lastname001","address001","city001","country001","postcode001",(int) phone01ProductId));
dao.insert(new Customer("c002","password002","firstname002","lastname002","address002","city002","country002","postcode002",(int) phone02ProductId));
for(CustomerWithPhoneDetails cwpd: dao.getAllCustomersWithPhoneDeytails()) {
Log.d("DBINFO","Customer is " + cwpd.customer.getUserName() + " etc. Phone is " + cwpd.phoneDetails.getProductId() + " etc." );
}
}
}
Note that suitable constructors have been coded in both the Phone and Customer class (default/empty constructor and one, annotated with #Ignore annotation that allows all values bar the id to be passed as used in the example below)
Note that ideally long rather than int should be used for the id columns.
Results
The Log :-
D/DBINFO: Customer is c001 etc. Phone is 1 etc.
D/DBINFO: Customer is c002 etc. Phone is 2 etc.
App Inspection :-
and :-

Is it possible to call a native query and store the result set in a non-entity object?

I am trying to obtain a non-entity object, using a native query, which I'd like to use as a model for a view on one of the pages in my application.
I am following this explanation which doesn't go in much detail, it is more like a cheat-sheet for someone who is already familiar with the topic and only needs a reminder.
There are similar questions here and here from ages ago and since I can’t figure out where exactly the provided code blocks should go I tought I might ask a fresh question.
What ever I try I get this exception:
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type
Here is my model class:
public class ProductForHomeView {
private int id;
private String title;
private EProductType productType;
private double price;
private int quantity;
public ProductForHomeView(int id, String title, EProductType productType, double price, int quantity) {
this.id = id;
this.title = title;
this.productType = productType;
this.price = price;
this.quantity = quantity;
}
The Repository interface:
public interface IProductsRepository extends CrudRepository<Products, Integer> {
#Query(value = "SELECT products.id, products.title, products.the_type AS productType, stock.price, stock.quantity FROM products LEFT JOIN stock on products.id=stock.product_id", nativeQuery = true)
List<ProductForHomeView> getProductsInfoForTheHomePage();
}
And one of the entity classes where I try to map the non-entity class:
#Entity
#Table(name = "stock")
#SqlResultSetMapping(
name="ProductForHomeViewMapping",
classes={
#ConstructorResult(
targetClass=qualified.name.ProductForHomeView.class,
columns={
#ColumnResult(name="id", type=Integer.class),
#ColumnResult(name="title", type=String.class),
#ColumnResult(name="productType", type=EProductType.class),
#ColumnResult(name="price", type=Long.class),
#ColumnResult(name="quantity", type=String.class)
})})
public class Stock {
// mapped fields, constructors, geters and setters...
}
I appreciate any input!
You may declare your view as an interface and then Spring Data will handle it they way you want.
Here is the example:
https://github.com/roberthunt/spring-data-native-query-projection/blob/master/src/main/java/uk/co/rbrt/PersonSummary.java
https://github.com/roberthunt/spring-data-native-query-projection/blob/master/src/main/java/uk/co/rbrt/PersonRepository.java
I used #SqlResultSetMapping with #NamedNativeQuery
Change your Curd repository method with following
public interface IProductsRepository extends CrudRepository<Products, Integer> {
#NamedNativeQuery(value = "SELECT products.id, products.title, products.the_type AS productType, stock.price, stock.quantity FROM products LEFT JOIN stock on products.id=stock.product_id", resultSetMapping = "ProductForHomeViewMapping")
List<ProductForHomeView> getProductsInfoForTheHomePage();
}
More info is available here

JPA Criteria API Specification for Many to Many

I have got three classes as mentioned below. I am trying to create a specification to filter data where there is a match in the linked table.
public class Album {
private Long id;
private List<AlbumTag> albumTags;
}
public class Tag {
private Long id;
private String category;
}
public class AlbumTag{
private Long id;
private Album album;
private Tag tag;
}
In the schema given above what I am trying to find is a list of all albums from Album table with the link in AlbumTag. The SQL that I want to achieve, doesn't have to be same, is below
select *
from Album A
where (A.Id in (select [AT].AlbumId
from AlbumTag [AT]))
What I have tried so far which is not working, of course, is below
public class AlbumWithTagSpecification implements Specification<Album> {
#Override
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
final Subquery<Long> personQuery = cq.subquery(Long.class);
final Root<Album> album = personQuery.from(Album.class);
final Join<Album, AlbumTag> albumTags = album.join("albumTags");
personQuery.select((albumTags.get("album")).get("id"));
personQuery.where(cb.equal(album.get("id"), (albumTags.get("album")).get("id")));
return cb.in(root.get("id")).value(personQuery);
}
}
Using spring boot and spring data JPA, you can prefer entity relationship to fetch the data.
1.Annotate the domain class with the entity relationship which given below:
#Entity
#Table(name="Album")
public class Album {
#Id
#Column(name="id")
private Long id;
#OneToMany(targetEntity = AlbumTag.class, mappedBy = "album")
private List<AlbumTag> albumTags;
//getter and setter
}
#Entity
#Table(name="Tag")
public class Tag {
#Id
#Column(name="id")
private Long id;
#Column(name="category")
private String category;
//getter and setter
}
#Entity
#Table(name="AlbumTag")
public class AlbumTag{
#Id
#Column(name="id")
private Long id;
#ManyToOne(optional = false, targetEntity = Album.class)
#JoinColumn(name = "id", referencedColumnName="id", insertable = false, updatable = false)
private Album album;
#ManyToOne(optional = false, targetEntity = Tag.class)
#JoinColumn(name = "id", referencedColumnName="id", insertable = false, updatable = false)
private Tag tag;
//getter and setter
}
2.use the spring data to fetch the details using the below:
Album album = ablumRepository.findOne(1); // get the complete details about individual album.
List<AlbumTag> albumTags = ablum.getAlbumTags(); // get the all related albumTags details for particular album.
I hope this will help you to solve it.
Subqueries in JPA only really work with CriteriaBuilder.exists() so i would try:
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
final Subquery<Long> subQuery = cq.subquery(Long.class);
final Root<AlbumTag> albumTag = subQuery.from(AlbumTag.class);
// it doesn't really matter what we select
subQuery.select(cb.literal(1));
subQuery.where(cb.equal(root.get("id"), (albumTag.get("album")).get("id")));
return cb.exists(subQuery);
}
which is equivalent to
select *
from Album A
where exists(
select 1 from AlbumTag AT
where AT.AlbumId = A.Id
)
Well, I wouldn't go for in operation in this case - it just complicates the query and the specification. The problem you described is actually matter of joining records from Table A with related records from Table B so the query in your case would be like:
SELECT a from Album a join AlbumTag at on a.id = at.albumId - as you needed it will return all albums that have album tags. Inner join explained
So in your case I would create this "factory" method that would create for you this specification.
public static Specification<Album> withTags() {
return new Specification<Album>() {
#Override
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return root.join("albumTags").getOn();
}
};
}
Also I would suggest you to have a look at static metamodel library from hibernate - link to introduction. It generates for you static model from your entity classes that helps you avoid creating queries/specifications using hardcoded strings.
creteria query for join tables
CriteriaQuery<Album> query = cb.createQuery(Album.class);
Root<Album> album = query.from(Teacher.class);
Join<Album, AlbumTag> tag = teacher.join("id");
query.select(tag).where(cb.equal(album.get("album")));
List<Album> results = em.createQuery(query).getResultList();
for (Album al : results) {
System.out.println("album-->+al.get(name));
}
This looks like a classic many to many example. The three classes you have map directly to the tables you would expect in the database. JPA is an Object Relational Mapping (ORM) library which means we can structure the classes in a more OO style and map to the underlying relational database.
The AlbumTag class can be omitted and the #ManyToMany relationship added to both Album and Tag.
public class Album {
private Long id;
#ManyToMany
#JoinTable(name="AlbumTag",
joinColumns=
#JoinColumn(name="album", referencedColumnName="id"),
inverseJoinColumns=
#JoinColumn(name="tag", referencedColumnName="id"))
private List<Tag> tags;
}
public class Tag {
private Long id;
private String category;
#ManyToMany(mappedBy="tags")
private List<Album> albums;
}
To find albums by Tag you would first retrieve the Tag from the repository using something like findById(1l); or findByCategory("Rock"); and then simply call getAlbums() on the Tag object.
Note: One slight difference here is that the AlbumTag table would have only two columns (album and tag). The extra id column on AlbumTag is unnecessary since the combination of album and tag would be a unique id and you would never need to find by id in this table anyway.
Since you are using spring-data-jpa you should really take advantage of the features it provides.
My first question is related to your entity classes. I do not understand why is it necesary to store a list of album tags in the album class. Since you have a join table this information is reduntant.
Secondly you should adnotate your entity clases:
#Entity
public class Album {
#Id
#Column
private Long id;
}
#Entity
public class Tag {
#Id
#Column
private Long id;
#Column
private String category;
}
#Entity
#Table
public class AlbumTag{
#Id
#Column
private Long id;
#ManyToOne
#JoinColumn
private Album album;
#ManyToOne
#JoinColumn
private Tag tag;
}
Next you should create repositories for your entity classes.
interface AlbumRepository extends JpaRepository<Album, Long>{
#Query
("select DISTINCT(a) from AlbumTag at "+
"join at.album a "
"where at.tag is not null")
List<Album> findAlbumWithTag();
}
Then simply call the repository function which will return a list of albums which have at least one tag.

#OneToMany which is in fact OneToOne / #Formula with Parameters

I have a Table
Products ( ProductId, Name ) and
ProductPrices (ProductId, Market, Price)
ProductPrices has a compositeKey (ProductId, Market). For a given Market, a Product has 0..1 Prices in that Market.
First approach #Formula
The Market is known at runtime, and can possibly be changed per request.
In an first attempt to model the ProductEntity I took an #Formula annotation, like so:
#Entity
#Table(...)
public class Product {
#Id
private int ProductId;
private String name;
#Formula("(SELECT TOP 1 Price FROM ProductPrices p WHERE p.ProductId = ProductId AND p.Market='Berlin')")
private double price;
}
But obviously, the market is then hard-compiled as annotations need to be static final Strings. [ so no #Formula("..." + getCurMarket() ) ].
Second approach, #OneToMany
Take a separate entity class for the prices, and reference them in the product entity as:
#OneToMany(mappedBy = "product")
private List<Price> price;
In a getPrice(), I could always return the first entry (there will never be more...) or nothing if the list is empty.
I then want to create a Predicate/Specification to use from within the ProductService. Example:
public static Specification<Product> marketEquals(final String market) {
return new Specification<Product>() {
#Override
public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
CriteriaQuery<String> q = cb.createQuery(String.class);
Root<Price> price = q.from(Price.class);
return price.get("Market").in("Berlin");
}
};
}
However, that only results in a (and I tried writing "market", "Market", ...)
org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias1.market' [select generatedAlias0 from ...backend.entities.Product as generatedAlias0 where generatedAlias1.market in (:param0)]
Third approach, Hibernate/JPA Filter
This time, I write in the product entity
#OneToMany(mappedBy = "product")
#Filters( {
#Filter(name="marketFilter", condition="Market = :market")
} )
private List<Price> price;
Again, I want to fill this filter in the ProductService, but I cannot gelt hold of the CurrentSession. I tried the Spring-way, adding an #Autowired private SessionFactory sessionFactory; and configuring it through
Filter filter = sessionFactory.getCurrentSession().enableFilter("marketFilter");
filter.setParameter("market", "Berlin" );
but I cannot get hold of the right context, as org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.hibernate.SessionFactory
Who could advise on how to model the database schema as entities, or could point working solutions to approach 2 and 3 ? Thanks!
Second approach is actually correct.Try it changing your entities and creteria query.
Products table:
#Entity
#Table(...)
public class Product {
#Id
private int ProductId;
private String name;
#OneToMany(mappedBy = "products")
private List<ProductPrices> ProductPrices;
}
ProductPrices table:
#Entity
#Table(...)
public class ProductPrices {
#Id
private int ProductPriceId;
private String market;
private double price;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "product_id") //foreign key reference
private Product products
}
ProductService:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> qry = cb.createQuery(Product.class);
Root<Product> root = qry.from(Product.class);
Join<Product, ProductPrices> price = root.join("ProductPrices");
List<Predicate> conditions = new ArrayList<>();
conditions.add(cb.equal(price.get("products"), "Berlin"));
TypedQuery<Product> typedQuery = em.createQuery(qry
.select(root)
.where(conditions.toArray(new Predicate[] {}))
.orderBy(cb.asc(root.get("Berlin")))
.distinct(true)
);
return typedQuery;

Categories