Entity beans v JBoss

Martin Večeřa 21. 7. 2008

Nadešel čas seznámit se s dalším typem business komponenty, s entity beans. Entitní komponenty mají za úkol perzistenci dat, bez které se většina aplikací neobejde. Zachovávají vlastnosti objektového programování a přitom se pohodlně ukládají do relační databáze. Jak to celé funguje?

JBoss AS 5.CR1 a EJB3

Od napsání posledního dílu vyšla další verze aplikačního serveru – 5.0.0.CR1. Opravuje řadu chyb, nicméně problém s přímou injekcí business komponent do JSP stránky (viz @EJB QuoteGenerator generator v předminulém dílu) pořád přetrvává. Budeme ho však používat, takže příklady a případné uvedené výstupy budou pocházet z této verze.

AS 5 podporuje business komponenty (Enterprise Java Beans, EJB) ve verzi 3.0. Právě u entity beans je to hodně znát, protože oproti předchozím verzím práci dost zjednodušují. Vše je řízeno pomocí anotací a dependency injection (automatické vkládání závislostí, např. pomocí anotace @EJB). V současné chvíli existuje již návrh specifikace pro verzi 3.1, která jde ještě dál.

Objektově relační mapování

Po několik let byla v Java komunitě datová perzistence poměrně ožehavým tématem. Nebylo možné najít společné řešení – jeden tábor zastával názor, že by se měly ručně vyvolávat i ty nejelementárnější SQL příkazy přes JDBC, druhý tábor měl zase názor, že by vše mělo fungovat plně automaticky. Vyvstávaly otázky, jak zajistit přenositelnost aplikací mezi databázemi a zda nemá dojít k nahrazení relačních databází plně objektovými.

Řešení se objevilo v podobě objektově relačního mapování (Object Relational Mapping, ORM, O/RM, O/R mapping). Jedná se o automatické a transparentní ukládání Java objektů do tabulek v relační databázi s využitím metadat, která toto mapování popisují. ORM pracuje v podstatě tak, že transformuje data z jedné reprezentace do druhé. To samozřejmě přináší určitou ztrátu výkonu. Pokud je však ORM implementováno jako aplikační middleware (což v případě JBossu je), má mnoho možností pro optimalizace, které jsou při manuální implementaci nedosažitelné. ORM také ušetří soustu práce vývojářům aplikací, protože správa metadat pro mapování je mnohem jednodušší a přehlednější, než ručně spravovat spoustu kódu pro práci s databází.

Konkrétní implementaci ORM pak přináší právě entity beans. Otázka vhodného uložení metadat pro mapování je vyřešena pomocí anotací přímo ve zdrojových kódech objektů. Tyto anotace jsou standardizované, což umožňuje nezávislost na použitém aplikačním serveru.

Pro ilustraci si uvedeme příklad se dvěma objekty s vazbou a jejich mapováním do relační databáze. Mějme uživatele a jejich adresy pro vyúčtování.

public class User {
    private String userName;
    private String name;
    private String address;
    private Set billingDetails;
    // metody pro přístup k vlastnostem (get/set pairs), business metody atd.
    ...
}
public class BillingDetails {
    private String accountNumber;
    private String accountName;
    private String accountType;
    private User user;
    // metody...
    ...

Tyto objekty by se mohly ukládat třeba do tabulek uvedených níže.

create table USER (
    USERNAME VARCHAR(15) NOT NULL PRIMARY KEY,
    NAME VARCHAR(50) NOT NULL,
    ADDRESS VARCHAR(100)
)
create table BILLING_DETAILS (
    ACCOUNT_NUMBER VARCHAR(10) NOT NULL PRIMARY KEY,
    ACCOUNT_NAME VARCHAR(50) NOT NULL,
    ACCOUNT_TYPE VARCHAR(2) NOT NULL,
    USERNAME VARCHAR(15) FOREIGN KEY REFERENCES USER
)

Všimněte si, jak se BillingDetails.user transformovalo na cizí klíč v tabulce BILLING_DETAILS.

Co entity beans nabízejí a jak se používají

Na tomto místě bych rád zmínil, že řízení perzistence se většinou nepoužívá přímo v aplikaci, ale deleguje se do session beans, která umožňuje správné strukturování aplikace a šetří psaní totožného kódu stále dokola. Tato session bean pak řídí transakční zpracování, ukládání, vyhledávání a další. Začněme však pěkně od Adama a podívejme se, jak entity bean vypadá a co umí.

Entity bean je obyčejná „stará dobrá javovská třída“ (POJO). Nemusí implementovat žádné rozhraní, ani dědit z žádné třídy. Veřejné metody nejsou finální, členské proměnné jsou private. Persistované vlastnosti mají dvojici metod get/set pro čtení a nastavení hodnoty, které mohou být buď public nebo protected. Existuje public nebo protected bezparametrický konstruktor. Podporován je identifikátor entity a číslo verze (pro optimistické zamykání). Překvapivá může být podpora dědičnosti a polymorfismu.

Jak již bylo zmíněno, je objektově relační mapování (ORM) v podobě entity beans řízeno pomocí anotací. Kompletní seznam můžete najít v API dokumentaci v balíčku javax.persistence. V následujícím přehledu zmíníme ty nejdůležitější a ukážeme si použití některých z nich na příkladu. Jiné zase jen tak odbudeme, ale nemusíte se obávat, protože u entity beans zůstaneme i během několika dalších dílů seriálu a podíváme se podrobněji na vlastnosti jako jsou vazby, dědičnost a transakce.

Mapování na tabulky

@Table určuje, na kterou tabulku (včetně jména schámatu a katalogu) se daná entita mapuje. Pokud tuto anotaci neuvedeme, vygeneruje se jméno tabulky podle názvu entity. Navíc je možné určit další tabulky, které ukládají další vlastnosti entity pomocí @SecondaryTables a @SecondaryTable. Standardně se předpokládá, že sekundární tabulky mají stejný primární klíč jako hlavní tabulka. To je však možné nastavit. Nejlépe to ilustruje příklad v API dokumentaci této anotace.

@Entity
@Table(name="EMPLOYEE")
@SecondaryTables({
    @SecondaryTable(name="EMP_DETAIL", pkJoinColumns=@PrimaryKeyJoinColumn(name="EMPL_ID")),
    @SecondaryTable(name="EMP_HIST", pkJoinColumns=@PrimaryKeyJoinColumn(name="EMPLOYEE_ID"))
})
public class Employee { ... }

Tabulky by pak mohly vypadat třeba takto:

create table EMP_DETAIL (
    EMPL_ID INTEGER NOT NULL PRIMARY KEY,
    ADDRESS1 VARCHAR(80) NOT NULL,
    ADDRESS2 VARCHAR(80) NULL,
    PHONE VARCHAR(30) NOT NULL,
    EMAIL VARCHAR(50) NOT NULL,
    SSN VARCHAR(50) NOT NULL,
)
alter table EMP_DETAIL add foreign key (EMPL_ID) references EMPLOYEE(ID);
create table EMP_HIST (
    EMPLOYEE_ID INTEGER NOT NULL PRIMARY KEY,
    EMPLOYER_NAME VARCHAR(50) NOT NULL,
    EMPLOYER_ADDRESS VARCHAR(150) NOT NULL,
    RECOMMENDATION VARCHAR(4000)
)
alter table EMP_HIST add foreign key (EMPLOYEE_ID) references EMPLOYEE(ID);

Vidíme, že vlastnosti entity Employee jsou uloženy celkem ve třech tabulkách ( EMPLOYEE, EMP_DETAIL, EMP_HIST), přičemž primární klíče ve dvou sekundárních tabulkách jsou uloženy v jiných atributech než v tabulce primární ( EMPL_ID, EMPLOYEE_ID).

Mapování na sloupce

Jméno sloupce, na který se daná vlastnost mapuje, je určeno v anotaci @Column. Je možné určit možnost ukládání NULL hodnot, maximální délku, přesnost (v případě čísel s desetinnou čárkou) a další. Zajímavá je možnost nezahrnout sloupec do SQL příkazů typu UPDATE. Toho se využívá zejména u vazebních sloupců, kde by každá aktualizace mohla trvat opravdu dlouho. Pokud například v objektu User uchováváme množinu objektů Orders v atributu Set<Order> orders, musela by se aktualizovat množina všech vazeb, což by mohlo být časově příliš náročné.

Pro mapování na sloupce se ještě používají anotace @JoinColumns a @JoinColumn pro namapování cizího složeného klíče. Opět si můžete prohlédnout příklad v dokumentaci.

@ManyToOne
@JoinColumns({
    @JoinColumn(name="ADDR_ID", referencedColumnName="ID"),
    @JoinColumn(name="ADDR_ZIP", referencedColumnName="ZIP")
})
public Address getAddress() { return address; }

Vidíme, že cizí klíč Address se skládá ze dvou atributů, které jsou v příslušné tabulce pojmenovány ID a ZIP. Jména atributů v tabulce obsahující tento klíč jsou pak uvedena u atributu  name.

Identifikátor

Pomocí anotace @Id můžeme určit primární klíč entity. Pokud je klíč tvořen více vlastnostmi, můžeme vytvořit zvláštní POJO pro uložení klíče. K tomu slouží anotace @IdClass (více viz příklad v dokumentaci).

@IdClass(com.acme.EmployeePK.class)
@Entity
public class Employee {
    @Id String empName;
    @Id Date birthDay;
     ...
}

Vidíme, že entita Employee má primární klíč složen ze dvou atributů. Oba tyto atributy pak obsahuje třída EmployeePK, kterou je možné použít například při hledání objektu podle primárního klíče.

Jinou možností je využití složených atributů. Můžeme mít například třídu Author složenou ze jména a příjmení. Takovou třídu označíme anotací @Embeddable. Můžeme ji pak použít jako typ atributu jiné třídy. Stačí pouze označit metodu pro čtení atributu anotací @Embedded. Konkrétně to ukazuje příklad níže. Složený atribut je možné použít i pro primární klíč. Primární klíč pak musíme označit anotací  @EmbeddedId.

Častým problémem je automatické generování primárního klíče. To je možné snadno vyřešit pomocí několika anotací. Nejprve se musíme rozhodnout, jakou strategii pro generování hodnot použijeme. První možností je sekvence určená anotací @SequenceGenerator na úrovni třídy nebo u primárního klíče. Generátor si pojmenujeme a můžeme určit i jméno databázové sekvence, ze které se mají číst hodnoty.

@SequenceGenerator(name="EMP_SEQ", sequenceName="EMPLOYEE_SEQ")

Další možností je tabulka pro ukládání generovaných hodnot určená anotací @TableGenerator. Je možné použít jednu tabulku pro více různých klíčů. Pro jednu hodnotu klíče je v tabulce uložen jeden řádek s touto hodnotou a jménem generátoru, kde byla hodnota použita. Příklad v dokumentaci ukazuje konkrétní použití.

@TableGenerator(name="empGen", table="ID_GEN", pkColumnName="GEN_KEY",
    valueColumnName="GEN_VALUE", pkColumnValue="EMP_ID")

Generátor se jménem empGen bude ukládat své hodnoty do tabulky ID_GEN. Do sloupce GEN_KEY uloží vždy hodnotu EMP_ID, aby bylo zřejmé, že klíč patří k dané entitě. Do sloupce GEN_VALUE se uloží poslední vygenerovaný primární klíč.

Některé databáze jako MSSQL umožňují také využít interní identifikátory řádků pro hodnotu klíče. Pro tyto identifikátory není potřeba specifikovat žádný generátor.

Samotnou generovanou hodnotu pak u primárního klíče využijeme anotací @GeneratedValue, kde uvedeme jméno generátoru a použitou strategii ( SEQUENCE  – pro sekvenční generátor, TABLE  – pro tabulkový generátor, IDENTITY  – pro interní identifikátor, AUTO  – pro automatickou konfiguraci vhodného generátoru).

@Id
@GeneratedValue(strategy=SEQUENCE, generator="CUST_SEQ")
public Long getId() { return id; }

Verze a optimistické zamykání

Optimistické zamykání řeší současný přístup k jednomu záznamu v databázi tak, že povolí přidělení zámku pro záznam všem žadatelům a předpokládá bezproblémový průběh. Je potřeba ověřit, že při pokusu o uložení změn nezměnil uzamčená data nikdo jiný. To se provádí pomocí speciální vlastnosti entity nazvané verze. Taková vlastnost je označená anotací @Version a může mít jeden z typů int, Integer, short, Short, long, Long a Timestamp.

Při načtení objektu z databáze dojde k naplnění atributu číslem přečtené verze. Při pokusu o uložení změn pak musí číslo verze v paměti a v databázi souhlasit, aby mohla operace bez problémů proběhnout. Pokud se čísla neshodují dojde k odvolání transakce. O transakcích a typech zámků si podrobně povíme v jednom z příštích dílů seriálu.

Relace mezi entitami/tabulkami

Jedou ze stěžejních vlastností relační databáze jsou právě relace mezi tabulkami. Podle arity relace můžete použít jednu z anotací @ManyToOne, @OneToOne, @OneToMany, @ManyToMany. Použití pěkně ilustruje příklad u OneToMany vazby. Samozřejmně je možné určit, co se stane v případě změny na jedné ze stran vazby – pomocí vlastnosti cascade můžeme určit, které operace se přenesou na druhou stranu vazby. Operace se provádí de facto čtyři:

  • CREATE – vytvoření entity v databázi,
  • MERGE – uložení změn objektů do databáze,
  • REFRESH – zahození změn objektů a načtení stavu z databáze,
  • DELETE – smazání entity z databáze.

Důležitým atributem je fetch, který nám určuje, zda se objekty navázané entity načtou všechny naráz ( EAGER) nebo na požádání ( LAZY). Je důležité si uvědomit, že LAZY vyžaduje jeden SQL SELECT příkaz na každý objekt. To může být při procházení mnoha záznamů mnohem méně efektivní než načtení všech záznamů najednou.

Užitečnou vlastnost přináší anotace @OrderBy, která nám umožňuje definovat pořadí objektů ve vazební množině.

Více podrobností a použití těchto anotací si ukážeme v příštím díle seriálu. Zmíníme se také podrobněji o strategiích načítání objektů ve vazbě.

Dědičnost

Entity beans verze 3 nám umožňují použití dědičnosti a polymorfismu. V praxi to spočívá v uložení nových vlastností potomků základní třídy. Anotace @Inheritance určuje, jakým způsobem budou do databáze ukládáni potomci daného objektu. Možnosti jsou:

  • JOINED  – nové vlastnosti potomků jsou ukládány do zvláštních tabulek (jedna pro jednoho potomka) a pro jeho načtení je provedeno spojení s hlavní tabulkou
  • SINGLE_TABLE  – celá hierarchie objektů je uložena v jedné tabulce
  • TABLE_PER_CLASS  – jedna kompletní tabulka na každou třídu

V případě použití JOINED a SINGLE_TABLE je potřeba uložit do hlavní tabulky, která konkrétní třída je v záznamu uložena. To se provede vložením sloupce, kterému se říká diskriminátor. Standardně se tento sloupec jmenuje DTYPE. To je možné změnit pomocí anotace @DiscriminatorColumn. Konkrétní hodnotu uloženou do tohoto sloupce je pak možné určit o potomků pomocí anotace @DiscriminatorValue. Pokud tuto anotaci nepoužijeme, určí si server sám interní hodnotu. Situace opět hezky vystihuje příklad v dokumentaci.

Další informace o použití dědičnosti si ukážeme v díle, který bude následovat po dílu o relacích.

Příklad entity bean

K dispozici je jako obvykle kompletní zdrojový kód s několika JSP stránkami v úhledném archívu. Kompilace a vystavení na server je možné provést klasicky pomocí ant {build|deploy|undeploy|clean}. Věnujte pozornost nastavení v souboru build.properties, který obsahuje cestu k AS a použitou konfiguraci.

Ukážeme si entitu pro ukládání záznamu o knize. Všimněte si automaticky generovaného primárního klíče, použití čísla verze a omezení délky sloupce.

@Entity
@NamedQuery(name="findBookByTitle", query="select b from Book b where b.title like :bookTitle order by b.title")
public class Book {
        private Long id;
        private int version;
        private String title;
        private String description;
        private String isbn;
        private Short year;
    private Author author;

        @Id
        @GeneratedValue(strategy=AUTO)
        public Long getId() {
                return id;
        }

        public void setId(Long id) {
                this.id = id;
        }

        @Version
        public int getVersion() {
                return version;
        }

        public void setVersion(int version) {
                this.version = version;
        }

        @Column(nullable=false, length=128)
        public String getTitle() {
                return title;
        }

        public void setTitle(String title) {
                this.title = title;
        }

        @Column(nullable=true, length=32768)
        public String getDescription() {
                return description;
        }

        public void setDescription(String description) {
                this.description = description;
        }

        @Column(nullable=true, length=13)
        public String getIsbn() {
                return isbn;
        }

        public void setIsbn(String isbn) {
                this.isbn = isbn;
        }

        public Short getYear() {
                return year;
        }

        public void setYear(Short year) {
                this.year = year;
        }

        @Embedded
        public Author getAuthor() {
                return author;
        }

        public void setAuthor(Author author) {
                this.author = author;
        }
}

@Embeddable
public class Author {
        private String firstName;
        private String lastName;

        @Column(nullable=true, length=128)
        public String getFirstName() {
                return firstName;
        }

        public void setFirstName(String firstName) {
                this.firstName = firstName;
        }

        @Column(nullable=false, length=128)
        public String getLastName() {
                return lastName;
        }

        public void setLastName(String lastName) {
                this.lastName = lastName;
        }
}

Přístup k entity bean

Jak bylo již řečeno v úvodu, pro řízení perzistence zpravidla používá session bean. Pomocí anotace @PersistentContext vložíme do session bean EntityManager, který má toto řízení na starosti.

Kromě základních operací pro uložení (persist), aktualizac (merge), opětovné načtení (refresh) a odstranění (remove) umožňuje EM entity samozřejmě i vyhledávat. Vzhledem k tomu, že entity beans nejsou závislé na konkrétní databázi (tuto abstrakci poskytuje AS), existuje speciální dotazovací jazyk pro hledání – EJB QL (EJB Query Language). Ten je velmi podobný SQL jazyku. Pokud znáte SQL, nemělo by pro vás být obtížné během chvíle zvládnout i EJB QL. Ukážeme si zde několik příkladů. Pro kompletní přehled navštivte stránku s dokumentací na hibernate.org nebo sun.com.

Dotazy je možné vytvářet dvojím způsobem – vložené do kódu, nebo jako pojmenovaný dotaz v anotaci (pomocí metadat). Oba způsoby ukazuje příklad níže.

Příklad session bean pro přístup k entity bean

Následující session bean ukazuje přístup k Entity Manager (pomocí vkládání závislostí – dependency injekcion), vytváření EJB QL staticky v metadatech i dynamicky v kódu, stránkování výsledků a úvodní naplnění databáze daty. Ve zdrojových kódech pak naleznete JSP stránky, které k této session bean přistupují.

@Stateless
@Remote(cz.root.jboss.library.Library.class)
public class LibraryBean implements Library {
    @PersistenceContext
    private EntityManager em;
    private static final int PAGE_SIZE = 3;

    public List<Book> getAllBooksAtPage(int page) {
        return em.createQuery("select book from Book book order by book.title")
            .setMaxResults(PAGE_SIZE)
            .setFirstResult(page * PAGE_SIZE)
            .getResultList();
    }

    public long getAllBooksPages() {
        return Math.round(Math.ceil(
            (Long) em.createQuery("select count(book) from Book book").getSingleResult()
            / (float) PAGE_SIZE));
    }

    public List<Book> findBookByTitle(String title) {
        return em.createNamedQuery("findBookByTitle")
            .setParameter("bookTitle", title)
            .getResultList();
    }

    public void generateData() {
        Author a1 = new Author();
        a1.setFirstName("Karel");
        a1.setLastName("Čapek");

        Book b1 = new Book();
        b1.setAuthor(a1);
        b1.setTitle("Válka s mloky");
        b1.setDescription("Čapkův fejetonní román...");
        b1.setIsbn("1234567890123");
        em.persist(b1);
        ...
    }
}

Před třídou Book jsme viděli anotaci @NamedQuery pro definici dotazu v EJB QL (specifikace pomocí metadat). Všimněte si použití parametru bookTitle, který je před použitím dotazu v metodě findBookByTitle() naplněn konkrétní hodnotou voláním metody  setParameter().

EntityManager je inicializován serverem pomocí anotace @PersistentContext.

V metodě getAllBooksAtPage() vidíme vytvoření dotazu přímo v kódu. Dále je zde použito stránkování. Velikost stránky je nastavena metodou setMaxResults() a první záznam, který nás zajímá nastaven metodou setFirstResult(). Výsledky dostaneme v podobě seznamu objektů Book metodou  getResultList().

Metoda getAllBooksPages() vrací počet stránek. Je v ní ukázáno použití metody getSingleResult() pro přečtení jednoduchého výsledku dotazu, ve kterém zjišťujeme počet záznamů v databázi.

Metoda generateData() generuje ukázková data do databáze. Ukazuje i použití základních operací správce entit ( EntityManager). Její kompletní kód naleznete ve zdrojových kódech.

Kam se data vlastně ukládají?

Každý .jar archív obsahující entity beans musí mít definovánu alespoň jednu perzistenční jednotku (persistence unit), která definuje, jakým způsobem se budou data entit ukládat. Jméno persistence unit se pak musí použít v anotaci @PersistentContext. My však máme definovánu pouze jednu, výchozí, a tak to není potřeba. Jednotka se definuje v souboru META-INF/persistence.xml a specifikuje datový zdroj (jeho definici jsme si ukázali v díle o bezpečnosti), dialekt pro použitou databázi (v ukázce je to výchozí databáze JBoss AS – HypersonicSQL) a styl vytváření tabulek (vytvořit při nasazení aplikace a odstranit při jejím ukončení).

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
          http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
        version="1.0">

        <persistence-unit name="manager1">
                <jta-data-source>java:/DefaultDS</jta-data-source>
                <properties>
                        <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
                        <property name="hibernate.hbm2ddl.auto" value="create-drop"/>

                </properties>
        </persistence-unit>
</persistence>

Data si můžete za běhu aplikace prohlédnout pomocí nástroje HSQL Database Manager. Kde ho v JMX konzoli najít, jsme si ukázali ve druhém dílu seriálu.

Závěr

Ukázali jsme si, jak vytvořit jednoduchou entity bean, použití základních anotací, vyhledávání a přístup k entity beans pomocí session bean. Můžete tedy vytvářet izolované objekty a ukládat je do databáze. Co však přináší opravdový užitek je snadná perzistence vazeb mezi objekty. O vazbách si proto povíme hned v příštím díle.

Našli jste v článku chybu?
Podnikatel.cz: Nepřijímají stravenky? Přicházejí o dost

Nepřijímají stravenky? Přicházejí o dost

120na80.cz: I tuto vodu můžete pít

I tuto vodu můžete pít

Lupa.cz: IT scéna po brexitu: přijde exodus vývojářů?

IT scéna po brexitu: přijde exodus vývojářů?

Podnikatel.cz: Rozhodnuto! Pracující senior penzi nezdaní

Rozhodnuto! Pracující senior penzi nezdaní

Měšec.cz: Co s reklamací, když e-shop krachuje?

Co s reklamací, když e-shop krachuje?

Měšec.cz: Ceny PHM v Evropě. Finty na úspory

Ceny PHM v Evropě. Finty na úspory

DigiZone.cz: Oživení ekonomiky by mělo navýšit reklamu

Oživení ekonomiky by mělo navýšit reklamu

Měšec.cz: Do ostravské MHD bez jízdenky. Stačí vaše karta

Do ostravské MHD bez jízdenky. Stačí vaše karta

Měšec.cz: Kurzy platebních karet: vyplatí se platit? (TEST)

Kurzy platebních karet: vyplatí se platit? (TEST)

DigiZone.cz: Sázka na e-sporty stanici Prima vychází

Sázka na e-sporty stanici Prima vychází

Vitalia.cz: Tohle je Břicháč Tom, co zhubnul 27 kg

Tohle je Břicháč Tom, co zhubnul 27 kg

Podnikatel.cz: Účtenky v rámci EET? Klidně emailem

Účtenky v rámci EET? Klidně emailem

Podnikatel.cz: Fotogalerie: Jesenka už má skoro 50 let

Fotogalerie: Jesenka už má skoro 50 let

Podnikatel.cz: Přiznal prodej padělků. Pokuta ho nemine

Přiznal prodej padělků. Pokuta ho nemine

Měšec.cz: Test: Výběry z bankomatů v cizině a kurzy

Test: Výběry z bankomatů v cizině a kurzy

Vitalia.cz: Je kočka riziko pro těhotnou ženu?

Je kočka riziko pro těhotnou ženu?

Měšec.cz: Platíme NFC mobilem. Konečně to funguje!

Platíme NFC mobilem. Konečně to funguje!

Měšec.cz: Dodavatele energií porovnáte snáz

Dodavatele energií porovnáte snáz

Měšec.cz: Se stavebkem k soudu už (většinou) nemusíte

Se stavebkem k soudu už (většinou) nemusíte

Lupa.cz: eIDAS: Nepřehnali jsme to s výjimkami?

eIDAS: Nepřehnali jsme to s výjimkami?