Unblocking The Main Thread Solving ANRs and Frozen Frames
create-netflix-clone-03-server_transcript.pdf
1. Creating a Netflix Clone
III
In the 3rd part we’ll dive right into the model objects representing the server and ultimately the front end code. If you’re unfamiliar with entities, JPA, UUID etc. I suggest
going back to the previous modules and refreshing your memory a bit as we’ll build a lot on top of that.
One way in which this will be different though is the usage of Lombok which will make the code far more terse.
Still the code here is mostly mock. The real world Netflix has a lot of code but most of it applies to algorithmic scheduling, user management, scaling etc. All of these
aren’t applicable here.
In this lesson our focus will be on entities and Data Transfer Objects also known as DTOs which I sometimes mixed with data access objects or DAO. There is overlap
between both of those concepts but what we have is generally DTOs as they transfer data to the client. They don’t just abstract the database layer.
2. codenameone.com github.com/codenameone/CodenameOne
Server Internals
In the 3rd part we’ll dive right into the model objects representing the server and ultimately the front end code. If you’re unfamiliar with entities, JPA, UUID etc. I suggest
going back to the previous modules and refreshing your memory a bit as we’ll build a lot on top of that.
One way in which this will be different though is the usage of Lombok which will make the code far more terse.
Still the code here is mostly mock. The real world Netflix has a lot of code but most of it applies to algorithmic scheduling, user management, scaling etc. All of these
aren’t applicable here.
In this lesson our focus will be on entities and Data Transfer Objects also known as DTOs which I sometimes mixed with data access objects or DAO. There is overlap
between both of those concepts but what we have is generally DTOs as they transfer data to the client. They don’t just abstract the database layer.
3. codenameone.com github.com/codenameone/CodenameOne
Server Internals
Now that we know Lombok basics we can move on to the server
The code is mostly mock code
We’ll start with the entity model code first
In the 3rd part we’ll dive right into the model objects representing the server and ultimately the front end code. If you’re unfamiliar with entities, JPA, UUID etc. I suggest
going back to the previous modules and refreshing your memory a bit as we’ll build a lot on top of that.
One way in which this will be different though is the usage of Lombok which will make the code far more terse.
Still the code here is mostly mock. The real world Netflix has a lot of code but most of it applies to algorithmic scheduling, user management, scaling etc. All of these
aren’t applicable here.
In this lesson our focus will be on entities and Data Transfer Objects also known as DTOs which I sometimes mixed with data access objects or DAO. There is overlap
between both of those concepts but what we have is generally DTOs as they transfer data to the client. They don’t just abstract the database layer.
4. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String description;
@OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
Writing an entity with Lombok is much easier. There are no, getters, setters, constructors, equals, hash code etc. Notice we still use JPA just like we used to so we have
the JPA entity annotation and then the Lombok annotations.
5. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String description;
@OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
Everything works as you would expect including the primary key definition etc. Notice I chose to go with a UUID object as a primary key coupled with auto-generation.
That’s a much simpler trick than the one I picked in previous modules.
6. codenameone.com github.com/codenameone/CodenameOne
UUID Keys
Strings as keys are important, they eliminate scanning attacks
JPA lets us use the UUID class and the rest is pretty much seamless
This could be costly for RDBMS inserts
Unless you’re doing millions of inserts this shouldn’t be a problem
We already talked about using strings for keys. When we use a UUID object we get a long string that isn’t guessable in the database. That means that we can expose
that primary key to the end user without worrying that he might use it to scan through details of other users as the string is pretty long and hard to guess.
As we saw, we just need to use the UUID object type. There are several other strategies for generating a UUID in JPA. I chose the simplest one, it might not be the best
one but it is convenient.
So why doesn’t everyone use this approach?
Turns out it’s much slower than using numeric auto-increment values on a database column. DB’s such as MySQL are heavily optimised for auto-increment fields and
String based primary keys are just slower to insert. Some developers consider that a non-starter especially when looking at the performance graph which is “scary”.
Performance really takes a dive for insert operations while it remains flat when using long auto-increment fields.
Personally I don’t think that’s a problem even for a video app like this. You wouldn’t insert too often and read operations are still pretty fast. This might become an issue if
you have a log or auditing table that might include multiple insert operations per second. At that point you’d need to use a long for the primary key and make sure never
to expose it externally.
7. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String description;
@OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
toMap(Media::getQuality,
Media::getMediaURL));
return new ContentDTO(id.toString(),
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
The name and description fields correspond to these fields in the database. This is the entire definition as the accessors are generated automatically
8. @Data
@AllArgsConstructor
@NoArgsConstructor
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String description;
@OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
toMap(Media::getQuality,
Media::getMediaURL));
return new ContentDTO(id.toString(),
name, description,
getMedia(heroImage), getMedia(icon),
getMedia(logo), qualityUrls);
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
We have three one to one media relations. These include the three images for every content item. Specifically:
- The hero image which is the big picture that appears on top of the application
- The show logo is displayed on top of the hero image. It’s a separate image to support different device aspect ratio and layout
- The icon is the image representing a show within the list
9. @Data
@AllArgsConstructor
@NoArgsConstructor
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String description;
@OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
toMap(Media::getQuality,
Media::getMediaURL));
return new ContentDTO(id.toString(),
name, description,
getMedia(heroImage), getMedia(icon),
getMedia(logo), qualityUrls);
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
Finally we have the actual video files which we store in media objects as well. We can have multiple video files representing different quality levels of the video. In real life
we can have even more options such as different aspect ratios, languages etc.
Normally I would like this to be a Map between quality and media but this is a bit challenging to represent correctly in JPA so I left this as a simple set.
10. @OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
toMap(Media::getQuality,
Media::getMediaURL));
return new ContentDTO(id.toString(),
name, description,
getMedia(heroImage), getMedia(icon),
getMedia(logo), qualityUrls);
}
private byte[] getMedia(Media m) {
return m == null ? null : m.getMedia();
}
}
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
For convenience we place the DTO creation within the entity object. This code is mostly just the construction but it there’s one block where we convert the media objects
11. @OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
toMap(Media::getQuality,
Media::getMediaURL));
return new ContentDTO(id.toString(),
name, description,
getMedia(heroImage), getMedia(icon),
getMedia(logo), qualityUrls);
}
private byte[] getMedia(Media m) {
return m == null ? null : m.getMedia();
}
}
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
Map<VideoQuality, String> qualityUrls = new HashMap<>();
for (Media video : videos) {
qualityUrls.put(video.getQuality(), video.getMediaURL());
}
In the DTO it makes more sense to hold the media as a map instead of a list or set. So we translate the video to to a map.
I find the stream syntax a bit obtuse sometimes. This is how it would look with a standard for loop. Essentially for each element we replace the content with a map where
the key is the quality and the value is the media URL.
12. @OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
toMap(Media::getQuality,
Media::getMediaURL));
return new ContentDTO(id.toString(),
name, description,
getMedia(heroImage), getMedia(icon),
getMedia(logo), qualityUrls);
}
private byte[] getMedia(Media m) {
return m == null ? null : m.getMedia();
}
}
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
Once this is done we create a new DTO object with the automatic constructor and return it.
13. @OneToOne
private Media heroImage;
@OneToOne
private Media icon;
@OneToOne
private Media logo;
@ManyToMany
private Set<Media> videos;
public ContentDTO getDTO() {
Map<VideoQuality, String> qualityUrls = videos.stream().
collect(Collectors.
toMap(Media::getQuality,
Media::getMediaURL));
return new ContentDTO(id.toString(),
name, description,
getMedia(heroImage), getMedia(icon),
getMedia(logo), qualityUrls);
}
private byte[] getMedia(Media m) {
return m == null ? null : m.getMedia();
}
}
Source Listing - Content
codenameone.com github.com/codenameone/CodenameOne
And finally I also added a small helper method to make the code above a bit simpler. So we won’t get a null pointer exception if the media is null.
14. @Data
@AllArgsConstructor
@NoArgsConstructor
public class ContentDTO {
private String id;
private String name;
private String description;
private byte[] heroImage;
private byte[] icon;
private byte[] logo;
private Map<VideoQuality, String> videoUrls;
}
Source Listing - ContentDTO
codenameone.com github.com/codenameone/CodenameOne
This is the DTO object we just created. Notice it’s super simple and mostly consists of the Lombok annotations.
15. @Data
@AllArgsConstructor
@NoArgsConstructor
public class ContentDTO {
private String id;
private String name;
private String description;
private byte[] heroImage;
private byte[] icon;
private byte[] logo;
private Map<VideoQuality, String> videoUrls;
}
Source Listing - ContentDTO
codenameone.com github.com/codenameone/CodenameOne
The strings just map directly to the entity. There’s nothing to say here.
16. @Data
@AllArgsConstructor
@NoArgsConstructor
public class ContentDTO {
private String id;
private String name;
private String description;
private byte[] heroImage;
private byte[] icon;
private byte[] logo;
private Map<VideoQuality, String> videoUrls;
}
Source Listing - ContentDTO
codenameone.com github.com/codenameone/CodenameOne
For the media I chose to include the icons themselves. I could have taken the approach of returning URLs for the media which might have advantages in the future. For
now this is simpler but possibly not as efficient. Using a URL would have had the advantage of caching the data locally for future refreshes. Using the actual icon means
all the data is transferred with one request.
17. @Data
@AllArgsConstructor
@NoArgsConstructor
public class ContentDTO {
private String id;
private String name;
private String description;
private byte[] heroImage;
private byte[] icon;
private byte[] logo;
private Map<VideoQuality, String> videoUrls;
}
Source Listing - ContentDTO
codenameone.com github.com/codenameone/CodenameOne
This is the map we created for the media items. We already discussed this in the stream part before. It maps between the video quality enum and the string URL.
18. package com.codename1.demos.netflixclone.model;
public enum VideoQuality {
NONE,
LOW,
MEDIUM,
HIGH
}
Source Listing - VideoQuality
codenameone.com github.com/codenameone/CodenameOne
For the sake of completeness this is the video quality enum. Pretty simple but matches what we need right now.
19. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Media {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String mimeType;
private Instant modified;
private long size;
@Lob
@Column(name = "media", columnDefinition="BLOB")
private byte[] media;
private String mediaURL;
private VideoQuality quality;
}
Source Listing - Media
codenameone.com github.com/codenameone/CodenameOne
The media entity is another standard Lombok entity with the standard trimmings.
20. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Media {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String mimeType;
private Instant modified;
private long size;
@Lob
@Column(name = "media", columnDefinition="BLOB")
private byte[] media;
private String mediaURL;
private VideoQuality quality;
}
Source Listing - Media
codenameone.com github.com/codenameone/CodenameOne
We use the same UUID primary key generation logic
21. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Media {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String mimeType;
private Instant modified;
private long size;
@Lob
@Column(name = "media", columnDefinition="BLOB")
private byte[] media;
private String mediaURL;
private VideoQuality quality;
}
Source Listing - Media
codenameone.com github.com/codenameone/CodenameOne
Rest of the stuff is pretty standard, notice that we store the modified time as an Instant instead of Date. Instant is a Java 8 date/time API class. It represents a timestamp
and is more convenient to use than date.
22. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Media {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String mimeType;
private Instant modified;
private long size;
@Lob
@Column(name = "media", columnDefinition="BLOB")
private byte[] media;
private String mediaURL;
private VideoQuality quality;
}
Source Listing - Media
codenameone.com github.com/codenameone/CodenameOne
The media data is stored in blob storage in the database
23. @Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Media {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String mimeType;
private Instant modified;
private long size;
@Lob
@Column(name = "media", columnDefinition="BLOB")
private byte[] media;
private String mediaURL;
private VideoQuality quality;
}
Source Listing - Media
codenameone.com github.com/codenameone/CodenameOne
Finally the URL to the media and the video quality enum are stored as well. That means we can have multiple instances of the same media object for various quality
levels.
One thing I didn’t cover here is the repositories for the entity objects. They’re all empty as we don’t need any finder methods for this specific demo so it’s all pretty trivial.