Ayrı Bir Repository Katmanı Şart mı? (2)

Daha önceki bir yazımda ayrı bir repository katmanı şart mı? diye sormuş ve ORM teknolojilerinin ve JPA’nın popülerleşmesi ile CRUD tabanlı bir takım uygulamalarda yazılım geliştiricilerin ayrıca bir DAO/Repository arayüzü oluşturmanın çok da fazla işlevi olmadığını savunarak, doğrudan JPA EntityManager üzerinden servis katmanında veri erişim işlemleri gerçekleştirmeye yöneldiklerinden bahsetmiştim. Bu yazımda bu tür bir pratiğin muhtemel dezavantajlarından bahsetmeye çalışacağım.

Bu tür bir pratiğin en temel zaafı, uygulamanın veriyi saklama ve veriye erişim için ilişkisel veritabanı yöntemine doğrudan bağımlı hale gelmesine neden olmasıdır diyebiliriz. Her ne kadar JPA API’de bize bir arayüz üzerinden çalışmayı sağlasa da, JPA API’nin işlevi uygulamayı ORM üreticilerinin gerçekleştirimlerinden izole kılmaya çalışmaktır. Oysa ki DAO/Repository arayüzleri arkasına gizlenmiş ayrı bir veri erişim katmanı uygulama için bugün ilişkisel yarın ise tamamen farklı, örneğin graph temelli, başka bir veri erişim ve saklama yöntemi ile çalışmayı kolaylaştıran bir fayda sağlamaktadır. Aksi durumda ise servis katmanı doğrudan spesifik bir teknolojiye (JPA/Hibernate) bağımlı kılınmaktadır. Hatta OpenEntityManagerInViewFilter/OpenSessionInViewFilter gibi bazı ORM çözümlerinin spesifik problemleri için geliştirilen çözümler bu bağımlılığın servis katmanı dışında controller ve presentation katmanlarına dahi sızmasına neden olabilmektedir.

Servis katmanında doğrudan JPA API ile çalışmanın bir diğer zaafı ise birim testlerinde ortaya çıkmaktadır. Ayrı bir DAO/Repository arayüzü ile çalışırken daha üst bir düzeyde ve tek bir metot çağrısı ile encapsule edilebilecek bir davranış, doğrudan JPA API ile çalışıldığı vakit EntityManager, TypedQuery, Query gibi birkaç ayrı yapı ve bunlarla ilgili genellikle sayıca tek bir metot çağrısından daha fazla sayıda metot çağrısı ile ifade edilmiş olduğundan, birim testleri içerisinde de servis katmanının bu yapılarla etkileşiminin mocklanması ve ilgili senaryonun testi sırasında bu mock nesnelerin sergilemesi istenen davranışların bu mock nesnelere öğretilmesi birim testlerin özellikle eğitim bölümlerinin çok daha “verbose” bir hale dönüşmesine neden olmaktadır. Birim testlerindeki bu kalabalıklık ve alt düzeyde daha fazla yapı ile etkileşimin söz konusu olması da teste tabi tutulan sınıf ve davranış üzerinde meydana gelen değişikliklerle birlikte testin de çok daha kolay bir biçimde kırılgan hale gelmesine kapı aralamaktadır.

Özetle söylemek gerekirse ayrı bir DAO/Repository katmanı oluşturmaktan kaçınarak kazanılan zaman ve iş gücü projenin biraz ilerleyen safhalarında, farklı gereksinimlerin ortaya çıkması, davranışların çeşitlenmesi ve değişmesi ile avantajını çok çabuk biçimde yitirebilmektedir.

Bu noktada akla Repository katmanını manuel oluşturmak yerine dinamik olarak üretmeyi sağlayan Spring Data gibi çözümler kullanmak da gelebilir. Hatta bu çözümlerin sunduğu kabiliyetlerden etkilenerek, CRUD temelli uygulamalarda bu sefer de servis katmanını tamamen tasviye ederek, Controller katmanından doğrudan Repository katmanı ile erişerek çalışmak da tercih edilebilmektedir. Gelin bir sonraki yazımızda da bu konu üzerinde duralım.

İlginç Bir Transaction Propagation Hikayesi 3

İlginç Bir Transaction Propagation Hikayesi başlıklı yazı dizisinin ikinci bölümünde Foo, Bar ve Baz entity’lerinden hiçbirinin mevcut durumda DB’ye insert edilemediğini söylemiştim. Ancak problemde hedeflenen Bar entity’sini persist eden ifadeyi try/catch bloğuna alarak, mümkünse Baz ve Foo entity’lerinin Bar’ın insert işleminden etkilenmeden DB’ye insert edilmesidir.

Bar entity’si ile ilgili, DB’den gelen constraint violation hatası JPA PersistenceContext’in PersistenceException fırlatmasına neden olmaktadır. JPA’da fırlatılan PersistenceException’lar, birkaçı hariç (NoResultException, NonUniqueResultException, LockTimeoutException, ve QueryTimeoutException) aktif transaction’ın rollback’e set edilmesine neden olurlar. Dolayısı ile Baz entity’si de Bar ile aynı transaction’da persist edildiği için onun insert işlemi de Bar’la birlikte başarısız olmaktadır. Başka bir ifade ile Baz, Bar’dan bağımsız DB’ye insert edilememektedir.

Ancak Foo entity’si için durum böyle olmaz zorunda değildir. Foo entity’sinin insert edildiği transaction ile Bar ve Baz entity’lerinin insert edildikleri transaction birbirlerinden tamamen bağımsızdırlar. Spring’in transaction yönetim altyapısı bir JPA transaction’ı aktif iken, yeni bir JPA transaction’ı başlatmak için mevcut transaction’ı suspend ederek yeni transaction’ı başlatır. Yeni bir JPA transaction’ı başlatmak demek eskisinden bağımsız yeni bir PersistenceContext (yani EntityManager) oluşturmak demektir.

Dolayısı ile ikinci PersistenceContext’de yapılan işlemler ve burada meydana gelen hata ilk PersistenceContext’i hiçbir şekilde etkilemeyecektir. Eğer ikinci servis metodundan (transaction commit/rollback yapan middleware kısmı dahil) fırlatılabilecek olası bütün exception’ları yakalayan bir try/catch bloğunu foo() metodu içerisinde barService.bar() metodunu çağırdığımız yere koyarsak, bar() metodu sonlanırken ortaya çıkabilecek her türlü exception foo() metodu içerisinde yakalanacaktır.

@Transactional(propagation=Propagation.REQUIRED)
public void foo() {
    em.persist(new Foo());
    try {
       barService.bar();
    } catch(Exception ex) {
        //ignore ex...
    }
}

Böylece foo() metodu başarılı biçimde sonlanacak ve transaction’da commit edilebilecektir. Böylece Foo entity’si de DB’ye insert edilmiş olacaktır. Bu noktada BarService.bar() metodu içerisinde Bar entity’sinin persist edilmesi işlemini try/catch bloğu içerisine almak gereksizdir diyebiliriz.

Spring ile çalışırken BarService.bar() metodunun içerisinde  unexpected rollback exception tetiklemeden sessiz biçimde transaction’ı rollback ettirmek de mümkündür.

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void bar() {
    em.persist(new Baz());
    try {
        em.persist(new Bar());
        em.flush();
    } catch(Exception ex) {
        //ignore ex...
        TransactionAspectSupport.currentTransactionStatus()
                 .setRollbackOnly();
    }
}

Bunun için öncelikle Bar entity’si EntityManager.persist() metodu ile persistent hale getirildikten sonra PersistenceContext üzerinde hemen bir flush tetikleyerek insert SQL’inin DB’ye gitmesi sağlanır. Bu aşamada constraint violation hatası nedeni ile PersistenceException meydana gelecektir, ama PersistenceException yakalanıp ignore edilmektedir. Ancak PersistenceContext’in artık bar() metodu başarılı biçimde sonlansa bile Spring’in transaction yönetim altyapısına transaction commit yapamayacağı, sadece rollback yapabileceği belirtilmelidir. Bu da mevcut transaction’a karşılık gelen TransactionStatus nesnesine erişip bunun setRollbackOnly() metodunu çağırarak olabilir. Artık Spring, yakalanan exception ignore edilip bar() metodu başarılı sonlansa bile transaction’ı commit yapmayacak, dolayısı ile transaction yönetim altyapısı da unexpected rollback exception ile karşılaşılmayacaktır.

Spring Boot ve JSF

Java Server Faces (JSF)’i başarısız bir UI framework olarak nitelediğimi değişik ortamlarda ve yazılarımda belirtmişimdir. Ancak ne yapalım ki JSF sıkça kullanılan bir UI framework olarak karşımıza çıkıyor. Spring ekosisteminde JSF’i Spring çözümleri ile entegre etmeye çalışan arkadaşlardan zaman zaman sorular alıyorum.

Spring Boot ile geliştirilen web uygulamalarında UI framework olarak JSF’i kullanmak mümkündür. Spring Boot ile ilgili verdiğimiz kurumsal eğitimimizde opsiyonel olarak Spring Boot JSF entegrasyonuna da değiniyoruz. Bu yazımda da kısaca Spring Boot uygulaması içerisinde JSF kullanabilmek için ne tür ayarlar yapmak gerektiğinden bahsedeceğim.

Spring Boot uygulaması içerisinde JSF ile çalışabilmek için aşağıdaki noktalar üzerinde ayarlamalar ve custom kod yazma ihtiyacı söz konusudur. Bu ayarları tespit ettiğim Spring Boot sürümünün 2.0.x,  JSF ref impl sürümünün ise 2.2.14 olduğunu da belirtmeliyim. Zaman içerisinde hem Spring Boot’un hem de JSF’in sürümlerinden kaynaklanan farklılıklar da söz konusu olabilir. Ancak yine de aşağıdaki adımlar genel olarak Spring Boot – JSF entegrasyonu için temel teşkil edecektir.

  1. JSF bağımlılıklarının pom.xml’e eklenmesi
  2. FacesServlet’in tanımlanması
  3. ConfigureListener’ın tanımlanması
  4. Spring Boot projesinin executable war’a dönüştürülmesi
  5. faces-config.xml içerisinde JSF EL Resolver tanımı ile managed bean ve managed property lookup’ları için Spring Container’a bakılmasının sağlanması
  6. Ayrıca JSF Backing Bean’ları Spring Managed yapacaksanız custom ViewScope konfigürasyonu da yapılmalıdır

Öncelikle JSF bağımlılıklarını pom.xml’e ekleyerek başlayalım.

<dependency>
    <groupId>com.sun.faces</groupId>
    <artifactId>jsf-api</artifactId>
    <version>2.2.14</version>
</dependency>
<dependency>
    <groupId>com.sun.faces</groupId>
    <artifactId>jsf-impl</artifactId>
    <version>2.2.14</version>
</dependency>

Ayrıca JSF Container çalışma zamanında JSP derleyicisine de ihtiyaç duyduğu için pom.xml’e Tomcat’in JSP compiler’ını da eklemeliyiz.

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>

İkinci adımda Spring Boot konfigürasyon sınıfımız içerisinde Java tabanlı olarak FacesServlet’in tanımını yapalım ve uzantısı *.xhtml olan web isteklerinde devreye girmesini sağlayalım.

@Configuration
public class SpringAppConfig implements ServletContextAware {
    @Bean
    public ServletRegistrationBean facesServletRegistration() {
        ServletRegistrationBean registration = 
            new ServletRegistrationBean(new FacesServlet(), "*.xhtml");
        registration.setLoadOnStartup(1);
        return registration;
    }

    ...
}

Şimdi de benzer biçimde JSF referans implementation’a ait olan ConfigureListener’ın java tabanlı konfigürasyon ile devreye girmesini sağlayalım. ConfigureListener JSF ile ilgili konfigürasyon bilgilerinin yüklenmesini ve çalışma ortamının ayağa kaldırılmasını sağlamaktadır.

@Configuration
public class SpringAppConfig implements ServletContextAware {

    @Bean
    public ServletListenerRegistrationBean<ConfigureListener>     
                                               jsfConfigureListener() {
        return new ServletListenerRegistrationBean<ConfigureListener>(
                    new ConfigureListener());
    }

    ...
}

JSF konfigürasyonunun JBoss, Tomcat gibi web container’lar içerisinde sağlıklı biçimde yüklenebilmesi için aşağıdaki servlet context parametre tanımının da yapılması gerekmektedir.

@Configuration
public class SpringAppConfig implements ServletContextAware {
    @Override
    public void setServletContext(ServletContext servletContext) {
        servletContext.setInitParameter(
            "com.sun.faces.forceLoadConfiguration", 
            Boolean.TRUE.toString());
    }

    ...
}

Spring Boot projelerinde JSF sayfaları sadece executable war dosyaları içerisinden erişilebilmektedir. Dolayısı ile Spring Boot projesinin tipini executable war yapmamız gerekmektedir. Bunun için pom.xml’deki packaging elemanının değerini aşağıdaki gibi war yapmamız yeterli olacaktır.

<packaging>war</packaging>

Spring Boot projesinin executable war yapılmasına paralel olarak JSF sayfalarını da src/main/webapp dizini altında bir lokasyona koymamız gerekmektedir.

Son olarak da faces-config.xml içerisinde JSF ELResolver olarak SpringBeanFacesELResolver gerçekleştirimini tanımlayarak JSF managed bean ve managed property lookup’ları için Spring ApplicationContext’e de bakılmasını sağlayalım.

<faces-config ...>
    <application>
        <el-resolver>
            org.springframework.web.jsf.el.SpringBeanFacesELResolver
        </el-resolver>
    </application>
</faces-config>

Bu aşamada Spring Boot projeniz içerisinde JSF ile çalışmaya başlayabilirsiniz. Eğer view scope JSF bean’lerini de Spring ApplicationContext içerisinde yönetmek isterseniz bu durumda son adımda belirttiğim custom view scope konfigürasyonunu da yapmanız gerekecektir. Daha önceki yazılarımdan birisinde JSF için sağlıklı çalışan custom view scope implemantasyonunu ve nasıl tanımlanacağını Harezmi’deki bir blog yazısında anlatmıştım.

JSF ile mücadelenizde başarılar… 😉

 

Ayrı Bir Repository Katmanı Şart mı?

Kurumsal yazılım sistemlerinde üç katmanlı mimari yaklaşımı uygulamak “de facto” olmuştur. Bu tür uygulamalarda sunum (presentation), servis (service/business) ve depo (DAO/repository) ayrı ayrı görevlere sahip katmanlar olarak karşımıza çıkarlar.

Sunum katmanında UI ile ilgili işlemler gerçekleştirilir. Bu katmanda kendi içinde arayüz (UI) ve dönüşüm/kontrol (controller) şeklinde alt katmanlara ayrılabilir. Dolayısı ile JavaEE dünyasında üç katmanlı (3 tiered) mimariler aynı zamanda N katmanlı (N tiered) olarak da adlandırılmaktadır. Sunum katamanına gelen kullanıcı istekleri gerekli dönüşüm ve kontrollerin ardından servis katmanına iletilir. Servis katmanından dönen cevaplarda yine gerekli dönüşümlere tabi tutularak kullanıcıya sunulur.

Servis katmanında uygulamanın iş mantığı ile ilgili işlemler ve süreçler yürütülür. Ayrıca servis katmanı karşımıza transaction yönetimi, güvenlik kontrolleri, validasyon gibi middleware operasyonların yönetim yeri olarak da çıkmaktadır.

Depo (DAO/Repository) katmanında ise veri erişim işlemleri gerçekleştirilir. Servis katmanı iş mantığını yürütürken veri erişimi, verinin saklanması (persist edilmesi) ile ilgili ihtiyaçlarını karşılamak için de depo katmanına başvurmaktadır.

Katmanlı mimarinin en büyük avantajı soyutlama (encapsulation) ve modülerliktir. Her bir katman sadece bir altındaki katmanı bilir ve sadece onunla iletişimde olur. Böylece herhangi bir katmanda yapılan değişikliklerin sistem içerisindeki etkisi sınırlı bir alanda kalmış olacaktır. Bunun yanı sıra herhangi bir katmanda kullanılan spesifik bir teknoloji, kütüphane, framework veya veritabanı o katman arkasında gizlenebilir ve gerektiğinde sistemin geri kalanını etkilemeden uygun diğer bir alternatifi ile değiştirilebilir.

ORM teknolojilerinin gelişmesi ve JPA’nın ortaya çıkması ile birlikte ayrı bir depo katmanının gerekli olup olmadığı tartışılır hale gelmiştir. Uygulama içerisinde veri erişim işlemleri için JPA’yı kullananlar için ORM provider’ın halihazırda veritabanı bağımsızlığı sağladığı ve SQL lehçe farklılıklarından uygulamanın geri kalanını izole ettiği belirtilerek ayrı bir repository arayüzü ve implemantasyon sınıfı oluşturmanın fazlalık olduğu iddia edilmektedir. Bu yaklaşımı savunanlar doğrudan PersistenceContext yani EntityManager nesnesini servis katmanına enjekte ederek de çalışılabileceğini ifade ederler. Bu görüşü savunanlara göre EntityManager üzerindeki operasyonları ayrı bir depo arayüzü ve implemantasyonu ile gizlemenin çok da fazla bir getirisi yoktur.

Gerçekten de entity sınıfları için oluşturulan CRUD arayüz ve sınıflarına baktığımızda, tanımlı metotların aslında JPA PersistenceContext API’sinde sunulan metotlardan çok da farklı olmadığını söyleyebiliriz. Örneğin, User entity sınıfı için oluşturulan CRUD depo arayüzünde genellikle aşağıdaki örneğe benzer metotların yer aldığını görürüz.

public interface UserRepository {    
    List<User> findAll();
    User findById(Long id);
    void create(User user);
    void update(User user);
    void delete(Long id);
}

Farklı entity’ler için benzer depo arayüzlerini ve sınıfları implement ettiklerini görenlerin projelerinde bu entity spesifik depo sınıflarını generic bir depo sınıfına dönüştürmeleri de çoğu zaman karşımıza çıkan bir durumdur.

public interface GenericRepository {
    <T> List<T> findAll(Class<T> entityClass);
    <T> T findById(Class<T> entityClass, Long id);
    <T> void create(T entity);
    <T> void update(T entity);
    <T> void delete(Class<T> entityClass, Long id);
}

JPA EntityManager arayüzünde de bu metotlara benzer metotlar yer almaktadır.

public interface EntityManager {    
    <T> T find(Class<T> entityClass, Object primaryKey);
    void persist(Object entity);
    <T> merge(T entity);
    remove(Object entity);
}

findAll metodu da JPA’nın TypedQuery arayüzü ve JPQL kullanılarak rahatlıkla karşılanabilmektedir.

TypedQuery<User> query = em.createQuery("select u from User as u", User.class);
List<User> users = query.getResultList();

Görüldüğü üzere, uygulama için oluşturulan entity spesifik veya generic depo arayüz ve implemantasyon sınıfları söz konusu ise bunların yaptığı çok fazla birşey olmayacak ve görevleri kabaca işi EntityManager’ın ilgili metoduna havale etmekle sınırlı kalacaktır. Dolayısı ile yukarıda bahsettiğim görüşü savunanlar bu tür ayrı bir depo katmanını tamamen tasviye edip servis katmanına EntityManager’ı enjekte ederek çalışmayı tercih etmektedirler.

Peki bu tür bir tercihin muhtemel dezavantajları nedir? Gelin bunu da bir sonraki yazımızda tartışalım.

İlginç Bir Transaction Propagation Hikayesi 2

İlginç Bir Transaction Propagation Hikayesi isimli blog yazımızın ilk bölümünde Foo, Bar ve Baz entity’lerini insert eden FooService ve BarService bean’lerinin birbirlerini çağırırken, insert işlemlerini iki farklı transaction içerisinde yapmaya çalıştıklarından bahsetmiştik. Önce FooService.foo() metodu içerisinde Foo entity’si, ardından da BarService.bar() metodu içerisinde sıra ile Baz ve Bar entity’leri JPA EntityManager vasıtası ile persist ediliyorlardı. Bar entity’si içerisinde nullable=false şeklinde tanımlı bir property nedeni ile Bar entity’sinin DB’ye insert edilmesi sırasında bir constraint violation exception meydana geliyordu. Sorumuz da bu exception sonucunda, DB’de Foo, Baz ve Bar entity’lerinden hangilerinin insert edilip edilmeyeceği şeklindeydi.

Aslında Bar’ın persist edilme bölümünde bu senaryonun sonucunu etkilemeyen ama constraint violation’ın tam olarak meydana geleceği noktayı veritabanı spesifik hale getiren eksik bir ifadenin olduğunu fark ettim.

JPA, PersistenceContext’de biriken entity state değişikliklerini (insert, update ve delete operasyonları) transaction commit anında topluca gerçekleştirir. Bu yaklaşıma “transaction write behind” adı  da verilmektedir. Dolayısı ile EntityManager.persist() metodunun invoke edildiği tam o anda, gerçekten de DB’ye bir SQL insert ifadesi gitmeyebilir. Bu insert TX commit aşamasına kadar ötelenebilir. Sonuç olarak, constraint violation hatasının, Bar entity’sinin persist edildiği aşağaki kod bloğu tarafından catch edilmesi söz konusu olmayabilir.

try {
    em.persist(new Bar());
} catch(Exception ex) {
//ignore ex...
}

Bu durumda constraint violation hatası bar() metodunun sonlandığı ve Spring TransactionManager bean’inin aktif transaction’ı commit etmeye çalıştığı anda meydana gelecektir.

“Transient entity’yi DB’ye insert edecek SQL insert ifadesinin tam olarak EntityManager.persist() metodu çağrıldığı vakit yürütülmesi hangi durumda söz konusu olmaktadır?” gibi bir soru sorarsak, buna cevabımız “DB’nin sentetik PK üretme stratejisi olarak default yönteminin identity veya autoincrement yöntemlerinden birisi olması durumunda” şeklinde olacaktır. Spesifikasyon gereği JPA, PersistenceContext’e eklenen transient nesnelere hemen bir PK değeri atamak zorundadır. Sentetik PK üretme stratejisinin identity veya autoincrement olduğu durumda PK değerini elde etmenin tek yolu entity’ye karşılık gelen bir kaydı DB’ye o anda insert etmektir. Bu durumda da constraint violation hatası tam olarak EntityManager.persist(new Bar()); ifadesinin çağrıldığı yerde olacak ve exception try..catch bloğu tarafından da yakalanıp ignore edilebilecektir.

DB’nin default sentetik PK üretme stratejisi sequence, UUID vb olursa kayıt DB’ye insert edilmeden de rahatlıkla bir PK değeri üretilebileceği için yeni persist edilen bir entity’ye ait insert SQL’i TX commit anına kadar ötelenir ve bu senaryodaki hata da try..catch bloğuna düşmeyecektir.

Sentetik PK üretme stratejisinin sequence, UUID vb olduğu senaryolarda da EntityManager.persist() metodu çağrılır çağrılmaz entity’ye ait SQL insert ifadesinin DB’ye yansıtılması da mümkündür. Bunun için PersistenceContext’in flush() metodu kullanılır.

try{
    em.persist(new Bar());
    em.flush();
} catch(Exception ex) {
    //ignore ex...
}

Yukarıdaki kod bloğunda olduğu gibi persist() metot çağrısından hemen sonra yapılacak bir flush() metot çağrısı PersistenceContext’de birikmiş state değişikliklerini topluca DB’ye yansıtacaktır. Böylece Bar entity’sinin nullable=false tanımlı property’sinin NULL bırakılmasından kaynaklı constraint violation hatası da hemen o anda ortaya çıkacak ve try..catch bloğu tarafından da handle edilebilecektir.

Ancak DB sentetik PK stratejisinin türü veya persist() işleminden sonra flush()’ın çağrılıp çağrılmaması senaryomuzda ortaya çıkacak sonucu değiştirmemektedir. Exception ister persist anında, isterse TX commit anında fırlatılsın bu senaryo sonucunda Foo, Baz ve Bar entity’lerinden hiç biri DB’ye insert edilmezler. Yani hem FooService.foo() metodunda başlatılan transaction, hem de BarService.bar() metodunda Propagation.REQUIRES_NEW ile tetiklenen yeni transaction’ın her ikisi de rollback olmaktadır. Bunun nedeni de JPA’nın, constraint violation hatası meydana geldiği anda PersistenceContext’e ait aktif transaction’ı ilerleyen adımlarda sadece rollback yapılabilir diye işaretlemesidir. Farz edelim ki EntityManager.persist() metodu çağrıldığı anda veya bu ifadeden sonra yer alan bir flush() metot çağrısı ile birlikte constraint violation hatası fırlatılmış olsun. Hata bizim try..catch bloğumuz tarafından yakalanıp ignore edilse bile, JPA bu hata dolayısı ile transaction’ı rollback’e set etmektedir. Daha sonra bar() metodu başarılı biçimde sonlanıp Spring TransactionManager transaction’ı commit etmeye çalışsa bile JPA/Hibernate’in fiziksel transaction’ı rollback’e işaretlemesinden ötürü Spring’in transaction’ı da fail eder ve commit aşamasında UnexpectedRollbackException şeklinde bir hata fırlatır. Bu hata da bir üstteki foo() metoduna ulaşır ve onun da dışına çıkarak RuntimeException ile sonlanmasına neden olur. Bu durumda foo() metoduna ait Spring transaction’ı da rollback edilir ve sonuç olarak Foo entity’si de DB’ye insert edilememiş olunur.

BarService.bar() metodu içerisinde Bar ve Baz entity’lerinin constraint violation hatası meydana geldiği müddetçe hiçbir biçimde DB’ye insert edilmeleri mümkün olamaz. Ancak FooService.foo() metodu içerisindeki Foo entity’si için aynı durum söz konusu değildir. Yani Foo entity’si diğerlerinden bağımsız olarak DB’ye insert edilebilir. Peki bunun için ne yapabiliriz, hangi yöntemleri kullanabiliriz? Gelin bunu da bir sonraki yazımızda ele alalım…

İlginç Bir Transaction Propagation Hikayesi

Aşağıdaki örnekte Foo, Bar ve Baz şeklinde üç basit entity sınıf görüyorsunuz. Foo ve Baz içerisinde PK dışında hiçbir property mevcut değilken, Bar sınıfında ise not null özelliğinde bir name property’si tanımlı. Ayrıca bu entity’leri persist eden FooService ve BarService servis bean sınıflarımız da var.

@Entity
public class Foo {
    @Id
    @GeneratedValue
    private Long id;
}

@Entity
public class Bar {
    @Id
    @GeneratedValue
    private Long id;
  
    @Column(nullable=false)
    private String name;
}

@Entity
public class Baz {
    @Id
    @GeneratedValue
    private Long id;
}

@Service
public class FooService {
    
    @Autowired
    private BarService barService;

    @PersistenceContext
    private EntityManager em;

    @Transactional(propagation=Propagation.REQUIRED)
    public void foo() {
        em.persist(new Foo());
        barService.bar();
    }
}

@Service
public class BarService {
    @PersistenceContext
    private EntityManager em;
    
    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void bar() {
        em.persist(new Baz());
        try {
            em.persist(new Bar());
        } catch(Exception ex) {
            //ignore ex...
        }
    }
}

Dikkat ederseniz FooService’in transactional foo() metodu içerisinde EntityManager vasıtası ile bir Foo nesnesi persist edildikten sonra barService.bar() metodu çağrılıyor.

BarService’in bar() metodu da transactional, ancak propagation kuralı olarak REQUIRES_NEW tanımlı. Yani bar() metodu çağrıldığında ortamda mevcut bir transaction olup olmamasına bakılmaksızın yeni bir transaction başlatılacak ve Bar entity’sinin persist işlemi bu yeni transaction içerisinde gerçekleştirilecek.

Propagation REQUIRES_NEW kuralına göre, ortamda mevcut bir transaction varsa bu mevcut eski transaction, bar() metodu sonlanana değin suspend edilerek bekletilir, bar() metodu sonlandıktan sonra da kaldığı yerden devam eder.

BarService.bar() metodu içerisinde önce yeni bir Baz nesnesi, ardından da benzer biçimde yeni bir Bar nesnesi yaratılıp EntityManager.persist() metodu ile DB’ye insert ediliyor. Bar entity’sinin persist işlemi sırasında meydana gelebilecek herhangi bir hata da yakalanıp ignore ediliyor. Çünkü Bar nesnesi içerisinde not null tanımlı bir name property’si var ve buna herhangi bir değer set edilmeden insert edilmeye çalışıldığı için insert SQL’i çalıştırılırken DB’den bir constraint violation hatası meydana gelecektir. Uygulama içerisinde bu hata try/catch bloğu ile yakalanıp ignore ediliyor. Yani BarService.bar() metodunun başarılı biçimde sonlanması, böylece Spring’in dekleratif transaction yönetim kurallarına göre başarılı sonlanan metot için de transaction commit tetiklenmesi isteniyor.

Bu noktada sizce tam olarak ne olur? BarService.bar() metodu başarılı sonlandığı vakit Spring transaction’ı commit edip Baz entity’si, ardından da FooService.foo() metodu başarılı sonlanıp Foo entity’si DB’ye insert edilirler mi? Yoksa BarService.bar() metodu içerisinde Bar nesnesinin insertion işlemi sırasında meydana gelen constraint violation hatası ile Baz entity’si de Bar ile aynı transaction içerisinde olduğu için DB’ye eklenemez, ama FooService.foo() metodunun transaction’ı farklı olduğu için Foo entity’si DB’ye insert edilir mi? Ya da bunların dışında bir sonuçla mı karşılaşırız?

Cevap bir sonraki yazımızda 🙂

 

ServiceLoader vs SpringFactoriesLoader, Hangisini Kullanalım?

ServiceLoader, Java 1.6’dan bu yana sunulan ancak pek de bilinmeyen basit bir kabiliyettir. Biraz da Spring’in gölgesinde kalmıştır diyebiliriz. Sonuçta ServiceLoader ile sunulan kabiliyeti de kapsayan ve çok daha fazlasını sunan bir IoC container’ınız varsa uygulama içerisinde farklı servis gerçekleştirimlerini dinamik olarak yükleme ve kullanma ihtiyacı için Spring ApplicationContext içerisinde bean tanımlamamak çok daha tercih edilesi bir yaklaşımdır. Yine de nesneleri ApplicationContext içerisinde bean olarak tanımlamanın uygun veya mümkün olmadığı bir takım senaryolar için ServiceLoader tercih edilebilir.

Nedir ServiceLoader ile yapılan şey? Öncelikle ServiceLoader ile yaratılacak olan servis nesnelerinin sınıflarının sahip olması gereken interface tanımlanmalıdır. Sonuçta ServiceLoader bu interface’i parametre alarak sistemdeki servis gerçekleştirimlerini bulup, yaratıp uygulamanın kullanımına sunmaktadır.

ServiceLoader serviceLoader = ServiceLoader.load(FooService.class);

ServiceLoader.load(FooService.class) ile FooService arayüzüne sahip servis gerçekleştirimlerini classpath’den bulup yüklemeyi sağlayacak yeni bir ServiceLoader nesnesi yaratmış oluruz. Peki, ServiceLoader, FooService gerçekleştirimlerini nasıl buluyor? Bunun için ServiceLoader, classpath’deki META-INF/services/com.example.FooService şeklinde oluşturulmuş dosyaları tespit etmektedir. Bu dosyalar birden fazla olabilir. Her bir dosyanın içerisinde de servis sınıflarının FQN şeklinde isimleri yer almalıdır.

com.example.demo.my.MyFooService
com.example.demo.your.YourFooService

Her bir dosya da da birden fazla gerçekleştirim yer alabilir. Her servisin FQN’si ayrı bir satırda tanımlanır.

Iterator iterator = serviceLoader.iterator();

serviceLoader.iterator() metodu ile mevcut servis dosyalarını lazy biçimde parse ederek servis nesnelerini oluşturacak bir Iterator elde edilir.

while(iterator.hasNext()) {
    FooService fooService = iterator.next();
}

iterator.hasNext() ve next() metotları ile de servis sınıfları yüklenir ve bu sınıfların default no arg constructor’ları çağrılarak servis nesneleri yaratılır ve uygulamaya dönülür. Artık uygulama FooService instance’ları üzerinde istediği işlemi yapabilir.

Eğer uygulama farklı bir servis arayüzü üzerinden başka servis nesnelerini elde edecek ise bu durumda da yeni servis arayüzü için META-INF/services dizini altında yeni dosyalar tanımlanıp, benzer biçimde bu servis arayüzü üzerinden servis nesnelerinin oluşturulması söz konusudur.

Gelelim Spring’in SpringFactoriesLoader sınıfına. ServiceLoader ile hemen hemen aynı işlemi yapmaktadır. Ancak tek farkı birden fazla servis arayüzü için ayrı ayrı servis dosyaları tanımlatmak yerine, bütün servis gerçekleştirimlerini classpath’de META-INF/spring.factories isimli bir dosya içerisinde toplamaya olanak sağlamasıdır. Spring’in bu sınıfı framework’ün dahili kullanımı için olsa bile rahatlıkla Spring enabled bir uygulamada da kullanılabilir.
com.example.demo.FooService=com.example.demo.my.MyFooService,com.example.demo.your.YourFooService

com.example.demo.BarService=com.example.demo.BarServiceImpl

spring.factories dosyası içerisinde servis gerçekleştirimleri servis arayüzü key, servis gerçekleştirimleri de virgüllerle ayrılmış biçimde FQN’li isimleri ile value olacak biçimde tanımlanmalıdır.

List services = SpringFactoriesLoader.loadFactories(
                  FooService.class, FooService.class.getClassLoader());

SpringFactoriesLoader.loadFactories() metodu ile belirtilen servis arayüzüne veya sınıfına karşılık gelen sınıflar yüklendikten sonra, default no arg constructor’ları çağrılarak servis instance’ları elde edilir ve uygulamaya bir liste şeklinde dönülür.

Kısacası Spring kullanan projelerde Java’nın ServiceLoader mekanizması ile ayrı servis dosyaları üzerinden çalışmaya lüzum yoktur. Spring’in SpringFactoriesLoader mekanizması servis tanımlarının tek noktadan yönetimi açısından daha pratik bir yol sunmaktadır.

JPA/Hibernate Pratikleri ve Püf Noktaları

Yaklaşık 2004 yılından bu yana önce Hibernate sonra da JPA üzerinde çalışıyorum. O dönemden bu yana pek çok projenin geliştirilmesinde görev aldım, danışmanlık ve mentörlük yaptım. Yıllar önce Ted Neward, ORM teknolojilerini bilgisayar bilimlerinin Vietnam’ına benzetmişti ve bunda da çok haklıydı. Eğer genel olarak ORM çözümleri, özelde de JPA/Hibernate ile ilgili sağlam bir teorik bilgiye sahip değilseniz projenizin tam manası ile bir bataklığa dönüşmesi işten bile değildir. Bütün bu yıllar boyunca JPA/Hibernate ile çalışmalarım sonucu bir takım temel kullanım pratikleri ve püf noktalar belirledim. Dahil olduğum projelerde de bu pratiklerin uygulanmasına, püf noktalara dikkat edilmesine azami gayret gösteriyorum ve sonuç olarak JPA/Hibernate ile çalışmak çok daha verimli bir hal alıyor. Aşağıda bu temel pratik ve püf noktalardan aklıma gelenlerin bir kısmını sizlerle paylaşmak istedim. Umarım işinize yarar!

  1. 1:1 ve M:1 ilişkiler de dahil olmak üzere bütün ilişkileri LAZY tanımlayın.
  2. 1:M ilişkileri her zaman için çift yönlü tanımlayın ve ilişkiyi sadece M:1 tarafından yönetin.
  3. M:N ilişkileri association table için ayrı bir entity tanımlayarak iki tane çift yönlü 1:M ilişki ile ele alın.
  4. Cascade tanımlarını sadece 1:M ve 1:1 ilişkilerde ve gerçekten hangi işlemlerin cascade etmesini istiyorsanız sadece onlar için kullanın.
  5. Embeddable yerine her zaman için Entity kullanın.
  6. Derin inheritance hiyerarşilerinden kaçının, mümkün olduğunca bütün hiyerarşi için “tek tablo” yönetimini tercih edin.
  7. Bileşke PK yönetiminden uzak durun
  8. Nümerik PK değeri ile çalışılması gerekiyorsa sentetik PK üretme yöntemlerinden SEQUENCE’i tercih edin.
  9. Entity sınıfların equals ve hashCode metotlarını implement ederken proxy ile çalışma ihtimaline karşın attribute değerlerine her zaman getter metotlar vasıtası ile erişin.
  10. HQL, Criteria ve Native SQL sorgularından sadece senaryonun ihtiyaç duyduğu kadar veriyi içeren DTO nesneleri dönün, hiçbir zaman Entity dönmeyin.
  11. LazyInitializationException probleminin en iyi çözüm yolu “fetch join”dir, Hibernate.initialize(), proxy nesnenin veya collection’ın içerisine erişim, OpenSessionInViewFiler, OpenEntityManagerInViewFilter gibi yöntemleri kesinlikle kullanmayın.
  12. Üretim ortamında detached uninitialized lazy ilişkilere veya proxy’lere erişim sırasında lazy hatası ile karşılaşmamak için hibernate.enable_lazy_load_no_trans property tanımının değerini TRUE yapın.
  13. Entity düzeyinde ikincil önbellek kullanıyorsanız, bu entity’nin 1:1 ve M:1 ilişkili hedef entity’lerini de entity düzeyinde önbellek kontrolüne dahil etmeyi unutmayın.
  14. İlişki düzeyinde ikincil önbellek kullanıyorsanız, ilişkinin hedef entity’sini de entity düzeyinde önbellek kontrolüne dahil etmeyi unutmayın.
  15. Sorgu sonuçlarının önbellekte tutulması faydadan çok zarar getirebilir, bunu unutmayın. Gerçekten sorgu sonuçlarını önbelleğe dahil edecekseniz ve sorgu sonucunda entity dönülüyorsa hedef entity’yi de önbellek kontrolüne dahil etmeyi unutmayın.

Eclipse Package Explorer’da Font Büyüklüğü

Uzun zamandır geliştirme platformu olarak Eclipse/Spring Tool Suite’i tercih ediyorum ve Kurumsal Java Eğitimleri’mizde de bunu kullanıyorum. Eğitimler sırasında Eclipse içerisinde açılan editor’lerin fontlarını Window>Preferences>Appearance>Colors and Fonts bölümünden değiştirebiliyoruz. Ancak geçen bir eğitimde projeksiyon cihazının netlik probleminden dolayı Package Explorer’daki paket, sınıf ve dosya isimlerinin de fontlarını büyütmek gerekti. Eclipse’in ayarlarını biraz kurcaladıktan sonra böyle birşeyin Eclipse içerisinden mümkün olmadığını anladım. Kısa bir “googling seansı” ardından cözümün plugins/org.eclipse.ui.themes_XXX/css altındaki css dosyalarını değiştirmekten geçtiğini anladım. Package Explorer’ın font ebatını değiştirmek için hangi temayı kullıyor iseniz ona karşılık gelen css dosyasının içerisine

.MPart Tree { font-size: 16; }

şeklinde bir css kuralı eklemeniz yeterli olacaktır. Bu işlemin ardından Eclipse/STS’i kapatıp açmayı unutmayın 🙂

Online Eğitim Hazırlama Tecrübeleri

Bu blog yazımda size online eğitim hazırlama tecrübelerimden bahsedeceğim. Yaklaşık 3.5 ay önce Udemy’de yayınlanmak üzere bir online eğitim hazırlama işine giriştim. Bu hafta itibari ile Spring Boot 2.0 ile Dinamik Web Uygulamaları isimli eğitimimiz Udemy platformundan yayına girmiş bulunuyor.

Nasıl bir ortam kurdum, eğitim içeriklerini nasıl hazırladım, kayıtları nasıl gerçekleştirdim, Udemy tarafındaki süreç nasıl yürüdü hepsini bu yazımda anlatmayı planlıyorum.

Uzun yıllardır Harezmi Bilişim Çözümleri bünyesinde Kurumsal Java Teknolojileri ile ilgili sınıf eğitimleri düzenliyoruz. Bu konuda önemli bir tecrübemiz olduğunu rahatlıkla söyleyebilirim. Bütün bu yıllar boyunca elimizde belirli bir olgunluğa ulaşmış eğitim materyalleri de mevcut. Bizde bütün bu klasik eğitim deneyiminden elde ettiğimiz çıktıyı, birde Udemy üzerinden online eğitimler şeklinde sunalım diyerek işe koyulduk. Burada eğitimlere online eğitim tabirini kullanıyorum, ama bu eğitimler sınıf eğitimleri gibi eğitmen ve katılımcıların senkron biçimde bir araya geldikleri şekilde olmuyor. Katılımcılar tamamen kendi zaman planlarına göre sistemden eğitim içeriğine erişip, bunların üzerinde çalışabiliyorlar. Aslında bu eğitimlere web platformundan sunulan asenkron eğitimler diyebiliriz, ama online eğitim demek de sanırım ana fikri anlatmak açısından yeterli, kısa ve öz gibi duruyor.

Doğal olarak online eğitimler, fiziksel ortamda, katılımcılarla yüz yüze gerçekleştirilen eğitimlerden daha farklı özelliklere sahip olmak durumundalar. Sınıf eğitimlerinde ortamda bir yazı tahtası kullanma imkanı sayesinde herhangi bir kompleks konuyu çok daha esnek ve farklı açılardan açıklama, izah etme imkanımız var. Online eğitimlerde ise bu araçlar daha çok slide’lar ve development ortamı ile sınırlı kalmış oluyor. Diğer bir farklı nokta ise katılımcılarla kurulan o andaki anlık etkileşime  ve ortam atmosferine göre anlatılan konularda değinilen noktaların derinliğinin dinamik olarak ayarlanabilmesi de mümkün. Online eğitimde ise eğitim kullanıcılara asenkron biçimde sunulduğu için eğitimdeki konu anlatımlarını ve derinliği sürekli olarak sabit bir düzeyde tutma durumu ortaya çıkıyor. Sınıf eğitiminde eğitmenin fiziksel aktivasyonu ve etkileşimini online ortamda mümkün olduğunca araçlar üzerinden sağlamaya çalışmak gerekiyor. Örneğin, bir kod parçacığındaki bölümleri detaylandırmak sınıf eğitiminde eğitmenin fiziksel anlatışı ile oldukça kolay gerçekleşmesine rağmen, online ortamda bu kod parçacığı ile ilgili bölümlemeleri, sözel anlatımları, görsel destekleri hazırlamak için etraflıca bir ön çalışma gerekiyor. Tabi bütün bu farklılıklarda sınıf eğitimlerinde kullanılan materyali olduğu gibi birebir online ortama taşıyarak sunmayı imkansız kılıyor.

Paylaşımıma online eğitimleri kaydetmek için nasıl bir ortam oluşturduğumuzu anlatarak devam etmek istiyorum.

Eğitim içeriğini kaydetmek ve daha sonrasında üzerinde değişiklikler yapabilmek için birkaç aracı ve programı temin etmek ve kurmak gerekti. Bunlardan birisi chroma key perdesi idi. Çektiğimiz eğitim videolarında eğitmenin görüntüsünün yer almasını istediğimiz vakit, video’da eğitmen görüntüsünün dışında arka planda istenmeyen, çekimi amatör göstrebilecek her türlü gereksiz detayın ortadan kalkması önemlidir. Her ne kadar çekim için arkanızı beyaz bir duvara verip, duvar ve eğitmen dışında bir görüntü olmasa bile, eğitmen ile duvar arasındaki derinlik ve gölgeler bile videonuzun amatörce görünmesine yetecektir. Chroma key perde sayesinde görüntü kaydetme programları kolaylıkla arka planı transparan yapabiliyorlar. Böylece video’da sadece eğitmenin görüntüsünden başka arka planla ilgili istenmeyen hiçbir detay da yer almamış oluyor.  Ayrıca transparan bu bölümlerin altında kalan yazı, grafik gibi kısımlarda arka plan transparan olduğu için rahatlıkla okunur kalıyor.

Video kayıt programı olarak açık kaynak kodlu OBS uygulamasını tercih ettik. OBS gerçekten profesyonel ayarda ve pek çok farklı senaryoyu destekleyecek kabiliyette bir screencasting aracı. Benden tam puan aldı diyebilirim. OBS ile farklı “scene” ler tanımlayıp, herbirisine de birden fazla farklı türde “source” ekleyebiliyorsunuz. Bu source’lar bir kamera görüntüsü, bilgisayarınızdaki bir pencere, yada bir imaj veya daha önce çekilmiş başka bir video olabilir. Bu source’ların üzerine, türlerine göre farklı farklı filter’lar tanımlayabiliyorsunuz. Bu filter’lar sayesinde görüntü ve ses üzerinde istediğiniz oynamayı ve iyileştirmeyi yapmanız mümkün.

OBS üzerinden optimum görüntü ve ses elde etmek için kullandığımız bazı ayarları ve değerleri burada paylaşmak istiyorum.

Settings>Output>Recording bölümünde

Recording Format: mp4
Encoder: x264
Rescale output: off
Rate Control: CRF,0,0,ultrafast,None,None

Settings>Audio bölümünde

Sample Rate: 44.1 khz
Desktop Audio Device: <varsa sisteminizdeki gömülü mikrofon>
Mic/Auxiliary Audio Device: <varsa sisteminize bağlı harici mikrofon>

Settings>Video bölümünde

Base (Canvas) Resolution: 1920×1080
Output (Scaled) Resolution: 1920×1080
Downscale Filter: Lanczos
Common FPS Values: 30

Video Capture Device Source üzerinde eklenen filter’lar

Chroma Key
Key Color Type: Green

Scaling/Aspect Ratio
Scale Filtering: Bicubic
Resolution: 1920×1080

Mic/aux source üzerindeki filter’lar

Noise Suppression
Suppression Level: -30 dB

Gain
Gain: 10,20 dB

Ayrıca Settings>Hotkeys bölümünden start/stop recording ve scene switch’leri için hotkey tanımlamak da çekimler sırasında kullanımı oldukça pratik hale getiriyor.

Kamera olarak laptop üzerindeki built-in HD kamerayı kullandım. Ancak harici bir kamera veya webcam kullanacaksanız, yada cep telefonunuzu kamera olarak bilgisayarınıza bağlayacaksanız bir tripod’a ihtiyacınız olacak. Bu noktada çekim sırasında baş hizanıza gelecek bir tripod yüksekliği sizin için ideal olacaktır. Ben çekimler sırasında bir ara android cep telefonunu webcam olarak kullanmaya niyetlendiğim için bir tripod almış bulundum. Bu arada android cep telefonunu webcam olarak bilgisayara tanıtmak için droidcam uygulamasını cep telefonunuza ve bilgisayarınıza kurmanız, ayrıca cep telefonunuz üzerinde de  usb üzerinden kamera olarak erişime izin vermek için “usb debugging” özelliğini açmanız gerekiyor. Beni cep telefonunu webcam olarak kullanmaktan uzaklaştıran bir diğer nokta ise kameranın çekim sırasında belirli bir süre sonra kendiliğinden kapanması oldu. Muhtemelen bu cep telefonunun inaktif kalması ile ilgili idi ve cep telefonu üzerinden bir ayar ile çözülebilirdi, ancak laptop üzerindeki built-in kameranın HD olduğunu gördükten sonra onu kullanmak daha pratik ve kolay geldi.

Mikrofon olarak USB üzerinden bağlanan Trust 20378 modelini tercih ettim. Ses kalitesi gayet güzel. Yalnız Linux üzerinden çalıştırırken biraz zorlandım. Bunun nedeni benim harici mikrofonların, mikrofon ve kulaklık girişi combo olan dizüstü bilgisayar ve Linux (Mint) üzerinde kullanılması ile ilgili deneyim eksikliğim diyebiliriz. Eğer bilgisayarınızdaki audio jack combo ise, yani hem kulaklık hem de mikrofon girişi birlikte ise, bu durumda Trust mikrofonu jack üzerinden değil, kendi USB aparatı ile bilgisayara bağlamanız gerekiyor. İkinci problem noktası ise Linux içerisindeki audio/volume control panelinde sistemdeki bütün mikrofonlar görüntüleniyor ve burada Trust mikrofon sisteme “CM 108 Audio Controller Analog Mono” isimli bir device olarak dahil edilmiş vaziyette. Bunu volume control’de aktif bırakmamız gerekiyor. Yine OBS üzerinde de Settings>Audio bölümünde Mic/Auxiliary Device seçeneğinde de bu şekilde seçili olmalı.

Eğitim içeriği slide ve lab çalışmalarından oluşmuştu. Lab çalışmalarını virtualbox üzerinden çalışan bir Windows 8 guest işletim sisteminde kurulu geliştirme ortamında gerçekleştirip sessiz biçimde OBS ile kaydettim. Daha sonra da bu video’ları OBS üzerinden “video source” olarak tekrar oynatarak üzerine sesli okuma yaptım. Böylece lab çalışmalarının anlatımı çok daha akıcı oldu. Ayrıca lab çalışmalarını çekim öncesi baştan sona implement edip, adımları notlandırdım, çekimler sırasında da bu adımları takip ettim. Böylece lab çalışmalarında beklenmedik hatalar, örnekler arasında birebirleri ile uyumsuz, tutarsız konfigürasyon veya implementasyonlar ortaya çıkmamış oldu. Slide’ları oluşturmak için ise LibreOffice Impress’u kullandım. Slide’ları oluştururken dikkat etmeyip, daha sonra slide’ların teker teker üzerinden geçmemi gerektiren bir husus paper formatını Slide>Page/Slide Properties>Slide PaperFormat bölümünden  “Screen 16:9” şeklinde seçmeyi unutmam oldu. Default Screen 4:3 oranında oluşturduğum slide’ları 1920×1080 çözünürlüğünde kaydettiğim vakit video’unun sağında ve solunda siyah şeritler ortaya çıkınca bu hatamın farkına vardım. Allah’tan page formatı 16:9 olacak biçimde değiştirdiğimde slide’ların içeriğinde çok büyük kaymalar olmadı, birkaç slide’da meydana gelen format problemlerini düzelterek bu sorunu aşmış oldum.

OBS ile çekilen video’ları ham video’lar olarak tanımlayabiliriz. Çünkü bu video’ların başında veya sonunda istemediğimiz kısımlar olabiliyor, yada slide’ları anlatırken hatalar oluyor ve belirli bölümleri tekrar anlatıyoruz veya daha sonra birleştirmek ve tek bir ders yapmak için birkaç ayrı video çekebiliyoruz. Bütün bu ham video’ları işlemek ve yayına hazır hale getirmek için yine açık kaynak kodlu OpenShot video edit programından yararlandım. Amacınız video içerisindeki bazı bölümleri kesmek, video’ları birleştirmek, video’ların başına sonuna imajlar koymak, arka plana ses iliştirmek gibi şeyler ise OpenShot bunları gayet başarılı biçimde yapmamıza imkan veriyor. Video edit sonunda da video’muzu farklı formatlarda export edebiliyoruz.

Export işlemi için benim kullandığım ve OBS’deki çekim kalitesine yakın çıktı aldığım OpenShot ayarları şöyle:

Profile
Width: 1920
Height: 1080
Aspect Ratio: 16:9
Frame Rate: 30.00
Pixel Ratio: 1:1
Progressive: Yes

Export alırken de açılan dialogda Target olarak MP4 (h.264) , Quality olarak da High seçeneklerini seçtiğimizde gayet kaliteli ve sıkıştırılmış bir mp4 çıktısı elde edebildim.

Slide üzerinde konuşurken yapılan çekimde konuşmanın akıcılığını sağlamak için bir prompter kullanmayı düşündüm. Bunun için Internet’te bir arama yaptığımda karşıma TeleKast isimli browser üzerinden çalışan bir teleprompter uygulaması çıktı. Bence oldukça başarılı açık kaynak kodlu bir uygulama. Text editörde metni hazırlıyorsunuz ve teleprompter ile yükleyip oynatmaya başlıyorsunuz. Metnin akışını hızlandırmanız veya yavaşlatmanız, durdurmanız, font büyüklüğünü artırmanız veya azaltmanız mümkün. Teleprompter penceresinin büyüklüğünü ve pozisyonunu da ayarlayabiliyorsunuz. Bütün bunlar iyiydi ama deneme çekimleri sırasında slide’ları oynatırken bir yandan da teleprompter’dan akan bir metni takip etmek, iki tarafı senkronize götürmek hiç kolay değildi. Ayrıca slide’lar ile bu metin arasında bir ilişkilendirme yapılması ve iki tarafın birbiri ile uyumlu ve güncel tutulması gerekiyordu. Bu durumda arayışımı slide’ların kendi içindeki notlar bölümüne kaydırdım. Her bir slide gösterimi sırasında okuyacağım metni o slide’a ait notes bölümüne yazdım ve slide içerisinde animasyonları tetikleme anlarını da yine bu notların arasında kendimce belirlediğim bir karakter dizisi ile işaretledim. Slide Show’a geçtiğimde LibreOffice, dual screen ile çalıştığım için sorunsuz biçimde bir ekranda slide show’u oynattı, diğer ekranda da hem o slide’ın ufak halini, yanında da not bölümünü gösterdi. Notes kısmında metin üzerine uygulanan herhangi bir text formatının ekranda göz ardı edilmesi, satır aralıklarının dikkate alınmaması LibreOffice tarafı için sorunlu noktalar olsa da, bunlar bir showstopper değildi.

Çekimlerin tamamlanması ile birlikte eğitimin Udemy üzerinden yayınlanması aşamasına geçtik. Aslında herhangi bir eğitim içeriğini hazırlamaya başlamadan evvel bu eğitimi yayınlayacağınız online platforma girip eğitim içeriğini hazırlarken dikkat etmeniz veye uymanız gereken kurallar ve ayarları, oluşturacağınız eğitimin sahip olması gereken yapıyı vs. etraflıca incelemeniz çok çok önemli. Bende aslında sürece bu şekilde başladım ve Udemy’de yayınlanan eğitimlerin yapısını vs öğrendikten sonra eğitim içeriğini oluşturmaya ve ardından da çekimlere giriştim. Çekimlere başlamadan evvel kayıt ortamında oluşturduğunuz test videolarını da kalite kontrol için sisteme upload etmeniz gerekli.

Eğitim içeriği bölümlerden, her bir bölüm de derslerden oluşuyor. Ayrıca her bir derse yardımcı kaynak ve harici linkler vs de ekleyebiliyorsunuz. Eğitime ait bir kapak resmi ve tanıtım/promosyon videosu da hazırlamanız gerekiyor. Kapak resminde logolar haricinde metin kullanmanız istenmiyor, çünkü kapak sayfasında salt görsel elemanlar barındıran, metin içermeyen eğitimlerin %10 daha fazla satış yaptığı tespit edilmiş. %10 bizim için çok büyük bir oran gibi durmasa da Udemy gibi devasa bir platform için çok büyük bir oran olduğu kesin.

Udemy’de eğitim yayınlamak için öncelikle bir eğitmen profili oluşturmanız ve bunu verify etmeniz gerekiyor. Doğrulama süreci eğitimi oluşturup, içeriğini upload ettikten sonra da yapılabiliyor, ancak eğitimi bu aşamadan evvel yayınlamanız mümkün değil. Son olarak da eğitim içeriğini tam olarak oluşturduğunuzu düşünüyorsanız, ön izleme sürecini başlatıyorsunuz. Bu süreçte eğitim içeriği ile ilgili Udemy content ekibinden herhangi bir olumsuz geri dönüş olmaz ise eğitiminiz artık yayına giriyor.

Online eğitim hazırlama ve yayınlama süreci ile ilgili daha pek çok nokta ve detay üzerinde konuşulabilir, ancak sanırım bu başlıklar bu süreç içerisindeki en temel adımlar ve sürecin de büyük bir bölümünü çevreliyorlar. Online eğitim hazırlamak isteyenler için umarım buradaki tespitler işe yarar. En azından ileride yeni eğitimler hazırlarken benim işime yarayacağı kesin 🙂